Skip to content

Commit 91fa45b

Browse files
unger1984wolfenrainalestiago
authored
feat(dart_frog): add SecurityContext named argument to serve method (#563)
* Add SecurityContext named argument to serve method * Add doc comment * add advanced docs * add test * fix lint * fix CERTIFICATE_VERIFY_FAILED (self signed) * Update docs/docs/advanced/security_context.md Co-authored-by: Jochum van der Ploeg <[email protected]> * Update docs/docs/advanced/security_context.md Co-authored-by: Jochum van der Ploeg <[email protected]> * Update docs/docs/advanced/security_context.md Co-authored-by: Jochum van der Ploeg <[email protected]> * Update packages/dart_frog/test/src/serve_test.dart Co-authored-by: Alejandro Santiago <[email protected]> * fix test * Update packages/dart_frog/test/src/serve_test.dart Co-authored-by: Alejandro Santiago <[email protected]> * fix format --------- Co-authored-by: Jochum van der Ploeg <[email protected]> Co-authored-by: Alejandro Santiago <[email protected]>
1 parent 56640e3 commit 91fa45b

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
sidebar_position: 5
3+
title: 🔑 Security Context
4+
---
5+
6+
# Security Context 🔑
7+
8+
By default, Dart Frog uses the insecure HTTP protocol. To enable the secure HTTPS protocol, you must pass a `SecurityContext` to the `serve` method in a [custom entrypoint](/docs/advanced/custom_entrypoint):
9+
10+
```dart
11+
import 'dart:io';
12+
13+
import 'package:dart_frog/dart_frog.dart';
14+
15+
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) {
16+
final chain = Platform.script.resolve('certificates/server_chain.pem').toFilePath();
17+
final key = Platform.script.resolve('certificates/server_key.pem').toFilePath();
18+
19+
final securityContext = SecurityContext()
20+
..useCertificateChain(chain)
21+
..usePrivateKey(key, password: 'VeryGoodPassword');
22+
23+
return serve(handler, ip, port, securityContext: securityContext);
24+
}
25+
```
26+
27+
For more information about SSL certificates and the `SecurityContext`, see the [`dart:io` documentation](https://api.flutter.dev/flutter/dart-io/SecurityContext-class.html).

packages/dart_frog/lib/src/serve.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ part of '_internal.dart';
88
/// By default, the header will be:
99
///
1010
/// `"X-Powered-By": "Dart with package:dart_frog"`
11+
///
12+
/// If a [securityContext] is provided an HTTPS server will be started
1113
Future<HttpServer> serve(
1214
Handler handler,
1315
Object address,
1416
int port, {
1517
String? poweredByHeader = 'Dart with package:dart_frog',
18+
SecurityContext? securityContext,
1619
}) {
1720
return shelf_io.serve(
1821
(shelf.Request request) async {
@@ -22,5 +25,6 @@ Future<HttpServer> serve(
2225
address,
2326
port,
2427
poweredByHeader: poweredByHeader,
28+
securityContext: securityContext,
2529
);
2630
}

packages/dart_frog/test/src/serve_test.dart

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:convert';
12
import 'dart:io';
23

34
import 'package:dart_frog/dart_frog.dart';
@@ -88,5 +89,95 @@ void main() {
8889
await server.close();
8990
});
9091
});
92+
93+
test('creates an HttpsServer on the provided securityContext', () async {
94+
const chain = '''
95+
-----BEGIN CERTIFICATE-----
96+
MIIEYTCCAsmgAwIBAgIQL36luIvjA/lXJN/q6t6XXDANBgkqhkiG9w0BAQsFADCB
97+
lzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTYwNAYDVQQLDC1jb2Jh
98+
bHRATWFjQm9vay1Qcm8tVW5nZXIubG9jYWwgKFVuZ2VyIEFuZHJleSkxPTA7BgNV
99+
BAMMNG1rY2VydCBjb2JhbHRATWFjQm9vay1Qcm8tVW5nZXIubG9jYWwgKFVuZ2Vy
100+
IEFuZHJleSkwHhcNMjEwMzI5MDgxNTEwWhcNMjMwNjI5MDgxNTEwWjBhMScwJQYD
101+
VQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxNjA0BgNVBAsMLWNv
102+
YmFsdEBNYWNCb29rLVByby1Vbmdlci5sb2NhbCAoVW5nZXIgQW5kcmV5KTCCASIw
103+
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALg8cToisQsz8lKsija4C3WeCa2M
104+
V20Zk6SFs1kQU+/Z/9dyExQlDTnMN8d8Dvkij+2rDa7PoiAFRkWafm0BCtLEv5/+
105+
V4sCApbhbqP4iqntA7VDC6GiDAjL/yqgZ9btaHnvUqKDXYMyuyVL0dWOILFhPqDf
106+
/0r3mwSVXXcFsQvY43lZ1/tteCiONMmWiPL9wuIJa0wqA6LwtGzjLTyXUmO7tYLA
107+
T1Mz1XjqZnhSW41FTLKjA0fsKAMp1tzaZl8DzMnAmrLOEe3E+JVzThy4PfD3J8+a
108+
cC/DsUVxKGkKcSQPXR3GGNp/VWbs7w+qqTD/PHI5J4r69GBR5ZXu8l24i8UCAwEA
109+
AaNeMFwwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1Ud
110+
IwQYMBaAFDptHqlxKExeYwjJh6dg0wXdlaocMBQGA1UdEQQNMAuCCWxvY2FsaG9z
111+
dDANBgkqhkiG9w0BAQsFAAOCAYEANRtOVP1GypaeTXBeKnoOGycPexnVIwpPgysM
112+
v2LCsoQzWbZJkOeZadjOODBoJSCblLAQvx6deo8isGOYJO73lrLuX3CldVBuwP1q
113+
nBlotfJN+kk42ixT+ETxD/vSOY6CxClo4pw4f8SXlyZ6RX3pKTyXLC5vud4z4kIt
114+
X5SUy6moZfagBn2lntBx1qnKpXYPsaQQN23dDJE44PQX9o78ZffUn81E39sGJFQt
115+
V5ktO6WJh+5J+6TdEd1xnhC51QSJhRNuD2H8H5PVhoAWiyMK8te+lMUpK6x4Mlbo
116+
eOjaAAhQ8UcVmvJrtt5GoDh9Bl/jB60gVGL0XFr9E1b3DZWgMpgyzBreGlpXPL87
117+
47P6RMmzpAp/efrY+scKk9UH/6CElsWt2lYrv4XUBjsCXdrGrYpuG3aS5ohowvWd
118+
lM5/EJeo2XQrb5AOE+mGMOZrI1F113IjZkP73OVrUyAKg5vsHDvHwRQ/blz3W2fG
119+
iTz7oBJ+5dtzLcZNFkDgNhBGKSu8
120+
-----END CERTIFICATE-----
121+
''';
122+
const key = '''
123+
-----BEGIN PRIVATE KEY-----
124+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4PHE6IrELM/JS
125+
rIo2uAt1ngmtjFdtGZOkhbNZEFPv2f/XchMUJQ05zDfHfA75Io/tqw2uz6IgBUZF
126+
mn5tAQrSxL+f/leLAgKW4W6j+Iqp7QO1QwuhogwIy/8qoGfW7Wh571Kig12DMrsl
127+
S9HVjiCxYT6g3/9K95sElV13BbEL2ON5Wdf7bXgojjTJlojy/cLiCWtMKgOi8LRs
128+
4y08l1Jju7WCwE9TM9V46mZ4UluNRUyyowNH7CgDKdbc2mZfA8zJwJqyzhHtxPiV
129+
c04cuD3w9yfPmnAvw7FFcShpCnEkD10dxhjaf1Vm7O8Pqqkw/zxyOSeK+vRgUeWV
130+
7vJduIvFAgMBAAECggEAfa3lw7XUtoK6RMGlC4zjbFnh2j0JishO2oXGgfRMfitl
131+
hvAvqadY7VutlWzAvh1gt83faKgFvfg7JtIsemmim4NSAW+9AnvdjlW8ZyjuVtrz
132+
k5xn+9wSf5HBwK9qBskvYzbqVShuC0j5N4kQXLE3BioDUjVb7yUX37mQ59e/Hgev
133+
TlmBRlQ1m6lXTu7slsOhrCF830acFullZcFQhhPeh9d4f32vMiCyCd3BhC0r50Jd
134+
45qdsQO1IGXKT4HYhdwI66BzV03kBM6rlsOltuhw0e+4ygXbLB5NOiuHNXz8Vju3
135+
d7Q13rc79udiXLcLxN+RgdVRR0F+imgRu/zq/gRGBQKBgQDUdfkFvxTYwmLV4OUD
136+
c8I/hjRX+RDgRfze0JhByp5VfGZ4NRhfo3JORmUU2fI+3jKxFineabtIl31ebBCc
137+
yK7hZPsdgluiLjrt/bAxOq4v47hOpyMQI9qxiBrJYDpID+emT/K3jfA0gYmZfl/c
138+
vYR5jSSD3d29UW3zqHNxni+d1wKBgQDd/cGVkWCH5k3k3Y94fSn//vbfECx0xltp
139+
025+rJ+go7T0ywSfNM3g5n1lt8jE0yJziiWF5PIM0qRo4YBWcPoI8sdFIaS+rHv7
140+
Oh1vSnX7vL5t0RNTjVpxpcxqAut2v03tMcuJo+8llEq+midp8beAmzsufKBONauX
141+
zCEk30MXwwKBgQC9qF5XEd9DLCtcb7kgHsrtOBkr2wuEmRWFtcHlIUG8YCN89TC/
142+
10EnvNFpDrGgC2xHBtjzUYE86PaiPmeJ/d+XFzTPf9na6dfzMX6CQ7bQy0Bw/eRf
143+
+RG1XyFCWKNORtxsa3vo/UzLIkO6AMUEYS2L8EIDcSALa1ByrRH4/9PT2wKBgQDZ
144+
f+2ysIxmuoQpL9eJEwEam+GfbgZQp6QbDJgfLtz7lEoQ6fTuU9s/djT4e1gPWFpR
145+
39Gh3U42uA9z3zVR/EFOkSgimLMESpTy8d6zEr6EVkox6H5KB53M6chdOd0gLJGa
146+
S4aDpgYCyMdu9jSVvcmwDOewRVT/K+CiytLSgJkI5wKBgE9Lun9U4sW8uKDL0q9g
147+
GhAvwnx67SU85gBKK+C1P9yBp8QGQEjFHtRBLUh5R4lXBvPW9e+VuS0vcj6QKGuy
148+
4fWk4JDF7cwbR/HqlyNQrynnSIm1qats3Oe2AiJzKNXMzQj/IiVgF/cp5z1qjkKZ
149+
SFTrELxay/xfdivEUxK9wEIG
150+
-----END PRIVATE KEY-----
151+
''';
152+
153+
final securityContext = SecurityContext()
154+
..useCertificateChainBytes(utf8.encode(chain))
155+
..usePrivateKeyBytes(utf8.encode(key));
156+
final server = await serve(
157+
(_) => Response(),
158+
'localhost',
159+
3000,
160+
securityContext: securityContext,
161+
);
162+
final client = HttpClient()
163+
..badCertificateCallback =
164+
(X509Certificate cert, String host, int port) => cert.pem == chain;
165+
final request = await client.getUrl(Uri.parse('https://localhost:3000'));
166+
final response = await request.close();
167+
expect(response.statusCode, equals(HttpStatus.ok));
168+
await server.close();
169+
});
170+
171+
test(
172+
'''throws a HandshakeException when trying to use https without a securityContext''',
173+
() async {
174+
await serve((_) => Response(), 'localhost', 3000);
175+
final client = HttpClient();
176+
177+
await expectLater(
178+
() => client.getUrl(Uri.parse('https://localhost:3000')),
179+
throwsA(isA<HandshakeException>()),
180+
);
181+
});
91182
});
92183
}

0 commit comments

Comments
 (0)