shelf_letsencrypt
brings support for Let's Encrypt to the shelf package.
LetsEncrypt provide a few challenges for your development enviroment. Read on for a few hints.
LetsEncrypt rate limits the issuing of production certificates. It is very easy to get locked out of letsencrypt for an extended period of time (days) leaving you in the situation where you can't issue a production certificate.
CRITICAL: you could end up with your production systems down for days!!!!
I would advise you to read up on the Lets Encrypt rate limits:
https://letsencrypt.org/docs/rate-limits/
To avoid this (potentially) major issue make certain that you test with a STAGING certificate.
You do this by passing in 'production: false' (the default) when creating the LetsEncrypt certificate. Staging certificates still have rate limits but they are much more generours
final LetsEncrypt letsEncrypt = LetsEncrypt(certificatesHandler, production: false);
On Linux you need to be root (sudo) to open a port below 1024. If you try to start your server with the default ports (80, 443) you will fail.
To issue a certificate LetsEncrypt needs to be able to connect to your webserver on port 80. This will work fine in production (with the write firewall rules) but in a development environment can be a bit tricky.
The above Permission limitations add to the complication.
The easist way to do this is (for dev): 1) start your server on ports 8080 and 8443 (or any pair above 1024) 2) set up two NATS on your router that forward ports to your dev machine. 80 -> 8080 443 -> 8443
For Lets Encrypt to issue a certificate it must be able to resolve the domain name of the certificate that you are requesting.
To avoid tampering with your production DNS I keep a cheap domain name that I use in test. I then use cloudflare's free DNS hosting service to host the domain name which allows me to add the necessary A record which points to my WFH router on which I've configured the above NAT.
Starting with shelf_letsencrypt: 2.0.0
, support for multiple domains on the same HTTPS port has been introduced. This
enhancement allows shelf_letsencrypt
to manage certificate requests and automatically serve multiple domains
seamlessly.
This functionality is powered by the multi_domain_secure_server package (developed
by gmpassos), specifically created for shelf_letsencrypt
. It enables a SecureServerSocket
to handle
different SecurityContext
(certificates) on the same listening port. For more details, check out the source code
on GitHub.
To use the LetsEncrypt
class
import 'dart:io';
import 'package:cron/cron.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_letsencrypt/shelf_letsencrypt.dart';
/// Start the example with a list of domains and a reciprocal
/// e-mail address for the domain admin:
/// ```dart
/// dart shelf_letsencrypt_example.dart \
/// www.domain.com:www2.domain.com \
/// info@domain.com:info2@domain.com
/// ```
void main(List<String> args) async {
final domainNamesArg = args[0]; // Domains for the HTTPS certificate.
final domainEmailsArg = args[1]; // The domains e-mail.
var certificatesDirectory = args.length > 2
? args[2] // Optional argument.
: '/tmp/shelf-letsencrypt-example/'; // Default directory.
final domains =
Domain.fromDomainsNamesAndEmailsArgs(domainNamesArg, domainEmailsArg);
// The Certificate handler, storing at `certificatesDirectory`.
final certificatesHandler =
CertificatesHandlerIO(Directory(certificatesDirectory));
// The Let's Encrypt integration tool in `staging` mode:
final letsEncrypt = LetsEncrypt(
certificatesHandler,
production: false, // If `true` uses Let's Encrypt production API.
port: 80,
securePort: 443,
);
var servers = await _startServer(letsEncrypt, domains);
await _startRenewalService(letsEncrypt, domains, servers.http, servers.https);
}
Future<({HttpServer http, HttpServer https})> _startServer(
LetsEncrypt letsEncrypt, List<Domain> domains) async {
// Build `shelf` Pipeline:
final pipeline = const Pipeline().addMiddleware(logRequests());
final handler = pipeline.addHandler(_processRequest);
// Start the HTTP and HTTPS servers:
final servers = await letsEncrypt.startServer(
handler,
domains,
loadAllHandledDomains: true,
);
var server = servers.http; // HTTP Server.
var serverSecure = servers.https; // HTTPS Server.
// Enable gzip:
server.autoCompress = true;
serverSecure.autoCompress = true;
print('Serving at http://${server.address.host}:${server.port}');
print('Serving at https://${serverSecure.address.host}:${serverSecure.port}');
return servers;
}
/// Check every hour if any of the certificates need to be renewed.
Future<void> _startRenewalService(LetsEncrypt letsEncrypt, List<Domain> domains,
HttpServer server, HttpServer secureServer) async {
Cron().schedule(
Schedule(hours: '*/1'), // every hour
() => refreshIfRequired(letsEncrypt, domains, server, secureServer));
}
Future<void> refreshIfRequired(
LetsEncrypt letsEncrypt,
List<Domain> domains,
HttpServer server,
HttpServer secureServer,
) async {
print('-- Checking if any certificates need to be renewed');
var restartRequired = false;
for (final domain in domains) {
final result =
await letsEncrypt.checkCertificate(domain, requestCertificate: true);
if (result.isOkRefreshed) {
print('** Certificate for ${domain.name} was renewed');
restartRequired = true;
} else {
print('-- Renewal not required');
}
}
if (restartRequired) {
// Restart the servers:
await Future.wait<void>([server.close(), secureServer.close()]);
await _startServer(letsEncrypt, domains);
print('** Services restarted');
}
}
Response _processRequest(Request request) =>
Response.ok('Requested: ${request.requestedUri}');
Each time your call startServer it will check if any certificates need to be renewed in the next 5 days (or if they are expired) and renew the certificate.
This however isn't sufficient for any long running service.
The example includes a renewal service that does a daily check if any certificate need renewing. If a cert needs to be renewed, it will renew it and then gracefully restart the server with the new certs.
The official source code is hosted @ GitHub:
Please file feature requests and bugs at the issue tracker.
Any help from the open-source community is always welcome and needed:
If you donate 1 hour of your time, you can contribute a lot, because others will do the same, just be part and start with your 1 hour.
Graciliano M. Passos: gmpassos@GitHub. Brett Sutton bsutton@GitHub
Don't be shy, show some love, and become our GitHub Sponsor (gmpassos, bsutton). Your support means the world to us, and it keeps the code caffeinated! ☕✨
Thanks a million! 🚀😄