diff --git a/README.md b/README.md index 76eb9f7..8ddd0b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/kevmoo/dhttpd/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/kevmoo/dhttpd/actions/workflows/ci.yml) [![package publisher](https://img.shields.io/pub/publisher/dhttpd.svg)](https://pub.dev/packages/dhttpd/publisher) -A simple HTTP server that can serve up any directory, built with Dart. +A simple HTTP(S) server that can serve up any directory, built with Dart. Inspired by `python -m SimpleHTTPServer`. ## Install @@ -39,17 +39,35 @@ $ dart run build_runner build -o build $ dhttpd --path build/web/ # Serves app at http://localhost:8080 ``` +### HTTPS + +If you want to use HTTPS you will need to pass in the path of the ssl certificate and the ssl key file as well as the password string, if a password is set on the key: + +``` +$ dart bin/dhttpd.dart --sslcert=sample/server_chain.pem --sslkey=sample/server_key.pem --sslkeypassword=dartdart +Server HTTPS started on port 8080 +``` + ## Configure ```console $ dhttpd --help --p, --port= The port to listen on. - (defaults to "8080") - --path= The path to serve. If not set, the current directory is used. - --headers= HTTP headers to apply to each response. header=value;header2=value - --host= The hostname to listen on. - (defaults to "localhost") --h, --help Displays the help. +-p, --port= The port to listen on. + (defaults to "8080") + --path= The path to serve. If not set, the current directory is used. + --headers= HTTP headers to apply to each response. header=value;header2=value + --host= The hostname to listen on. + (defaults to "localhost") + --sslcert= The SSL certificate to use. + If set along with sslkey, https will be used. + See the dart documentation about SecurityContext.useCertificateChain for more. + --sslkey= The key of the SSL certificate to use. + If set along with sslcert, https will be used. + See the dart documentation about SecurityContext.usePrivateKey for more. + --sslkeypassword= The password for the key of the SSL certificate to use. + Required if the ssl key being used has a password set. + See the dart documentation about SecurityContext.usePrivateKey for more. +-h, --help Displays the help. ``` [path]: https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path diff --git a/bin/dhttpd.dart b/bin/dhttpd.dart index d23156d..3d64edc 100644 --- a/bin/dhttpd.dart +++ b/bin/dhttpd.dart @@ -25,9 +25,13 @@ Future main(List args) async { headers: options.headers != null ? _parseKeyValuePairs(options.headers!) : null, address: options.host, + sslCert: options.sslcert, + sslKey: options.sslkey, + sslPassword: options.sslkeypassword, ); - print('Server started on port ${options.port}'); + print( + 'Server HTTP${Dhttpd.isSSL ? 'S' : ''} started on port ${options.port}'); } Map _parseKeyValuePairs(String str) => { diff --git a/lib/dhttpd.dart b/lib/dhttpd.dart index ae691bd..82a425b 100644 --- a/lib/dhttpd.dart +++ b/lib/dhttpd.dart @@ -10,6 +10,7 @@ import 'src/options.dart'; class Dhttpd { final HttpServer _server; final String path; + static bool _ssl = false; Dhttpd._(this._server, this.path); @@ -19,6 +20,8 @@ class Dhttpd { String get urlBase => 'http://$host:$port/'; + static bool get isSSL => _ssl; + /// [address] can either be a [String] or an /// [InternetAddress]. If [address] is a [String], [start] will /// perform a [InternetAddress.lookup] and use the first value in the @@ -34,15 +37,29 @@ class Dhttpd { int port = defaultPort, Object address = defaultHost, Map? headers, + String? sslCert, + String? sslKey, + String? sslPassword, }) async { path ??= Directory.current.path; + SecurityContext? securityContext; + if (sslCert != null && sslKey != null) { + securityContext = SecurityContext() + ..useCertificateChain(sslCert) + ..usePrivateKey(sslKey, password: sslPassword); + } + if (securityContext != null) { + _ssl = true; + } + final pipeline = const Pipeline() .addMiddleware(logRequests()) .addMiddleware(_headersMiddleware(headers)) .addHandler(createStaticHandler(path, defaultDocument: 'index.html')); - final server = await io.serve(pipeline, address, port); + final server = await io.serve(pipeline, address, port, + securityContext: securityContext); return Dhttpd._(server, path); } diff --git a/lib/src/options.dart b/lib/src/options.dart index e91ecb4..93d5d9a 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -34,6 +34,27 @@ class Options { help: 'The hostname to listen on.') final String host; + @CliOption( + valueHelp: 'sslcert', + help: 'The SSL certificate to use.' + '\r\nIf set along with sslkey, https will be used.' + '\r\nSee the dart documentation about SecurityContext.useCertificateChain for more.') + final String? sslcert; + + @CliOption( + valueHelp: 'sslkey', + help: 'The key of the SSL certificate to use.' + '\r\nIf set along with sslcert, https will be used.' + '\r\nSee the dart documentation about SecurityContext.usePrivateKey for more.') + final String? sslkey; + + @CliOption( + valueHelp: 'sslkeypassword', + help: 'The password for the key of the SSL certificate to use.' + '\r\nRequired if the ssl key being used has a password set.' + '\r\nSee the dart documentation about SecurityContext.usePrivateKey for more.') + final String? sslkeypassword; + @CliOption(abbr: 'h', negatable: false, help: 'Displays the help.') final bool help; @@ -42,6 +63,9 @@ class Options { this.path, this.headers, required this.host, + this.sslcert, + this.sslkey, + this.sslkeypassword, required this.help, }); } diff --git a/lib/src/options.g.dart b/lib/src/options.g.dart index c6bc822..a80c0cd 100644 --- a/lib/src/options.g.dart +++ b/lib/src/options.g.dart @@ -25,6 +25,9 @@ Options _$parseOptionsResult(ArgResults result) => Options( path: result['path'] as String?, headers: result['headers'] as String?, host: result['host'] as String, + sslcert: result['sslcert'] as String?, + sslkey: result['sslkey'] as String?, + sslkeypassword: result['sslkeypassword'] as String?, help: result['help'] as bool, ); @@ -52,6 +55,24 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser valueHelp: 'host', defaultsTo: 'localhost', ) + ..addOption( + 'sslcert', + help: + 'The SSL certificate to use.\r\nIf set along with sslkey, https will be used.\r\nSee the dart documentation about SecurityContext.useCertificateChain for more.', + valueHelp: 'sslcert', + ) + ..addOption( + 'sslkey', + help: + 'The key of the SSL certificate to use.\r\nIf set along with sslcert, https will be used.\r\nSee the dart documentation about SecurityContext.usePrivateKey for more.', + valueHelp: 'sslkey', + ) + ..addOption( + 'sslkeypassword', + help: + 'The password for the key of the SSL certificate to use.\r\nRequired if the ssl key being used has a password set.\r\nSee the dart documentation about SecurityContext.usePrivateKey for more.', + valueHelp: 'sslkeypassword', + ) ..addFlag( 'help', abbr: 'h', diff --git a/sample/server_chain.pem b/sample/server_chain.pem new file mode 100644 index 0000000..341a86f --- /dev/null +++ b/sample/server_chain.pem @@ -0,0 +1,59 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIBATANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVpbnRl +cm1lZGlhdGVhdXRob3JpdHkwHhcNMTUxMDI3MTAyNjM1WhcNMjUxMDI0MTAyNjM1 +WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCkg/Qr8RQeLTOSgCkyiEX2ztgkgscX8hKGHEHdvlkmVK3JVEIIwkvu +/Y9LtHZUia3nPAgqEEbexzTENZjSCcC0V6I2XW/e5tIE3rO0KLZyhtZhN/2SfJ6p +KbOh0HLr1VtkKJGp1tzUmHW/aZI32pK60ZJ/N917NLPCJpCaL8+wHo3+w3oNqln6 +oJsfgxy9SUM8Bsc9WMYKMUdqLO1QKs1A5YwqZuO7Mwj+4LY2QDixC7Ua7V9YAPo2 +1SBeLvMCHbYxSPCuxcZ/kDkgax/DF9u7aZnGhMImkwBka0OQFvpfjKtTIuoobTpe +PAG7MQYXk4RjnjdyEX/9XAQzvNo1CDObAgMBAAGjgbQwgbEwPAYDVR0RBDUwM4IJ +bG9jYWxob3N0ggkxMjcuMC4wLjGCAzo6MYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAA +ATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSvhJo6taTggJQBukEvMo/PDk8tKTAf +BgNVHSMEGDAWgBS98L4T5RaIToE3DkBRsoeWPil0eDAOBgNVHQ8BAf8EBAMCA6gw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAHLOt0mL2S4A +B7vN7KsfQeGlVgZUVlEjem6kqBh4fIzl4CsQuOO8oJ0FlO1z5JAIo98hZinymJx1 +phBVpyGIKakT/etMH0op5evLe9dD36VA3IM/FEv5ibk35iGnPokiJXIAcdHd1zam +YaTHRAnZET5S03+7BgRTKoRuszhbvuFz/vKXaIAnVNOF4Gf2NUJ/Ax7ssJtRkN+5 +UVxe8TZVxzgiRv1uF6NTr+J8PDepkHCbJ6zEQNudcFKAuC56DN1vUe06gRDrNbVq +2JHEh4pRfMpdsPCrS5YHBjVq/XHtFHgwDR6g0WTwSUJvDeM4OPQY5f61FB0JbFza +PkLkXmoIod8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDLjCCAhagAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1yb290 +YXV0aG9yaXR5MB4XDTE1MTAyNzEwMjYzNVoXDTI1MTAyNDEwMjYzNVowIDEeMBwG +A1UEAwwVaW50ZXJtZWRpYXRlYXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6GndRFiXk+2q+Ig7ZOWKKGta+is8137qyXz+eVFs5sA0ajMN +ZBAMWS0TIXw/Yks+y6fEcV/tfv91k1eUN4YXPcoxTdDF97d2hO9wxumeYOMnQeDy +VZVDKQBZ+jFMeI+VkNpMEdmsLErpZDGob/1dC8tLEuR6RuRR8X6IDGMPOCMw1jLK +V1bQjPtzqKadTscfjLuKxuLgspJdTrzsu6hdcl1mm8K6CjTY2HNXWxs1yYmwfuQ2 +Z4/8sOMNqFqLjN+ChD7pksTMq7IosqGiJzi2bpd5f44ek/k822Y0ATncJHk4h1Z+ +kZBnW6kgcLna1gDri9heRwSZ+M8T8nlHgIMZIQIDAQABo3sweTASBgNVHRMBAf8E +CDAGAQH/AgEAMB0GA1UdDgQWBBS98L4T5RaIToE3DkBRsoeWPil0eDAfBgNVHSME +GDAWgBRxD5DQHTmtpDFKDOiMf5FAi6vfbzAOBgNVHQ8BAf8EBAMCAgQwEwYDVR0l +BAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAD+4KpUeV5mUPw5IG/7w +eOXnUpeS96XFGuS1JuFo/TbgntPWSPyo+rD4GrPIkUXyoHaMCDd2UBEjyGbBIKlB +NZA3RJOAEp7DTkLNK4RFn/OEcLwG0J5brL7kaLRO4vwvItVIdZ2XIqzypRQTc0MG +MmF08zycnSlaN01ryM67AsMhwdHqVa+uXQPo8R8sdFGnZ33yywTYD73FeImXilQ2 +rDnFUVqmrW1fjl0Fi4rV5XI0EQiPrzKvRtmF8ZqjGATPOsRd64cwQX6V+P5hNeIR +9pba6td7AbNGausHfacRYMyoGJWWWkFPd+7jWOCPqW7Fk1tmBgdB8GzXa3inWIRM +RUE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1yb290 +YXV0aG9yaXR5MB4XDTE1MTAyNzEwMjYzNFoXDTI1MTAyNDEwMjYzNFowGDEWMBQG +A1UEAwwNcm9vdGF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMl+dcraUM/E7E6zl7+7hK9oUJYXJLnfiMtP/TRFVbH4+2aEN8vXzPbzKdR3 +FfaHczXQTwnTCaYA4u4uSDvSOsFFEfxEwYORsdKmQEM8nGpVX2NVvKsMcGIhh8kh +ZwJfkMIOcAxmGIHGdMhF8VghonJ8uGiuqktxdfpARq0g3fqIjDHsF9/LpfshUfk9 +wsRyTF0yr90U/dsfnE+u8l7GvVl8j2Zegp0sagAGtLaNv7tP17AibqEGg2yDBrBN +9r9ihe4CqMjx+Q2kQ2S9Gz2V2ReO/n6vm2VQxsPRB/lV/9jh7cUcS0/9mggLYrDy +cq1v7rLLQrWuxMz1E3gOhyCYJ38CAwEAAaNQME4wHQYDVR0OBBYEFHEPkNAdOa2k +MUoM6Ix/kUCLq99vMB8GA1UdIwQYMBaAFHEPkNAdOa2kMUoM6Ix/kUCLq99vMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABrhjnWC6b+z9Kw73C/niOwo +9sPdufjS6tb0sCwDjt3mjvE4NdNWt+/+ZOugW6dqtvqhtqZM1q0u9pJkNwIrqgFD +ZHcfNaf31G6Z2YE+Io7woTVw6fFobg/EFo+a/qwbvWL26McmiRL5yiSBjVjpX4a5 +kdZ+aPQUCBaLrTWwlCDqzSVIULWUQvveRWbToMFKPNID58NtEpymAx3Pgir7YjV9 +UnlU2l5vZrh1PTCqZxvC/IdRESUfW80LdHaeyizRUP+6vKxGgSz2MRuYINjbd6GO +hGiCpWlwziW2xLV1l2qSRLko2kIafLZP18N0ThM9zKbU5ps9NgFOf//wqSGtLaE= +-----END CERTIFICATE----- diff --git a/sample/server_key.pem b/sample/server_key.pem new file mode 100644 index 0000000..895b7d2 --- /dev/null +++ b/sample/server_key.pem @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE4zAcBgoqhkiG9w0BDAEBMA4ECBMCjlg8JYZ4AgIIAASCBMFd9cBoZ5xcTock +AVQcg/HzYJtMceKn1gtMDdC7mmXuyN0shoxhG4BpQInHkFARL+nenesXFxEm4X5e +L603Pcgw72/ratxVpTW7hPMjiLTEBqza0GjQm7Sarbdy+Vzdp/6XFrAcPfFl1juY +oyYzbozPsvFHz3Re44y1KmI4HAzU/qkjJUbNTTiPPVI2cDP6iYN2XXxBb1wwp8jR +iqdZqFG7lU/wvPEbD7BVPpmJBHWNG681zb4ea5Zn4hW8UaxpiIBiaH0/IWc2SVZd +RliAFo3NEsGxCcsnBo/n00oudGbOJxdOp7FbH5hJpeqX2WhCyJRxIeHOWmeuMAet +03HFriiEmJ99m2nEJN1x0A3QUUM7ji6vZAb4qb1dyq7LlX4M2aaqixRnaTcQkapf +DOxX35DEBXSKrDpyWp6Rx4wNpUyi1TKyhaVnYgD3Gn0VfC/2w86gSFlrf9PMYGM0 +PvFxTDzTyjOuPBRa728gZOGXgDOL7qvdInU/opVew7kFeRQHXxHzFCLK5dD+Vrig +5fS3m0++f55ODkxqHXB8gbXbd3GMmsW6MrGpU7VsCNtbVPdSMW0FalovEB0M+2lj +1VfuvL+0F5huTe+BgZAt6xgET/CIcZXdNMRPVhraqUjqWtI9Rdk4STPCpU1rDkjG +YDl/fo4W2T6qQWFUpiC9IvVVGkVxaqfZZ4Qu+V5xPUi6vk95QiTNkN1t+m+sCCgS +Llkea8Um0aHMy33Lj3NsfL0LMrnpniqcAks8BvcgIZwk1VRqcj7BQVCygJSYrmAR +DBhMpjWlXuSggnyVPuduZDtnTN+8lCHLOKL3a3bDb6ySaKX49Km6GutDLfpDtEA0 +3mQvmEG4XVm7zy+AlN72qFbtSLDRi/D/uQh2q/ZrFQLOBQBQB56TvEbKouLimUDM +ascQA3aUyhOE7e+d02NOFIFTozwc/C//CIFeA+ZEwxyfha/3Bor6Jez7PC/eHNxZ +w7YMXzPW9NhcCcerhYGebuCJxLwzqJ+IGdukjKsGV2ytWDoB2xZiJNu096j4RKcq +YSJoen0R7IH8N4eDujXR8m9kAl724Uqs1OoAs4VNICvzTutbsgVZ6Z+NMOcfnPw9 +jZkFhot16w8znD+OmhBR7/bzOLpaeUhk7EhNq5M6U0NNWx3WwkDlvU/jx+6/EQe3 +iLEHptH2HYBF1xscaKGbtKNtuQsfdzgWpOX0qK2YbK3yCKvL/xIm1DQmDZDKkWdW +VNh8oGV1H96CivWlvxhAgXKz9F/83CjMw8YXRk7RJvWR4vtNvXFAvGkFIYCN9Jv9 +p+1ukaYoxSLGBik907I6gWSHqumJiCprUyAX/bVfZfNiYh4hzeA3lhwxZSax3JG4 +7QFPvyepOmF/3AAzS/Pusx6jOZnuCMCkfQi6Wpem1o3s4x+fP7kz00Xuj01ErucM +S10ixfIh84kXBN3dTRDtDdeCyoMsBKO0W5jDBBlWL02YfdF6Opo1Q4cPh2DYgXMh +XEszNZSK5LB0y+f3A6Kdx/hkZzHVvMONA70OyrkoZzGyWENhcB0c7ntTJyPPD2qM +s0HRA2VwF/0ypU3OKERM1Ua5NSkTgvnnVTlV9GO90Tkn5v4fxdl8NzIuJLyGguTP +Xc0tRM34Lg== +-----END ENCRYPTED PRIVATE KEY-----