nandi/frqpublic Fork 0
2e24e647a7766c822d13d253777204c3bf92fb7a
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

serve-dir.dart · 84 lines · 3.3 KBDart Blame HistoryRaw
Three tarballs where a devShell was 5ce66d5 nandi 21h ago1// A static file server in one file, for `tools/build-web.sh serve`.
2//
3// `dart:io` and no packages, so there is nothing to `pub get` and nothing to
4// pin: the toolchain already has a Dart, because Flutter ships one, and that
5// is the whole reason this is Dart rather than the `python3 -m http.server`
6// it replaces. Serving a directory should not add a language to the list of
7// things a machine must have.
8//
9// It is the development server and says so: no caching headers, no
10// compression, no range requests. `.modal/flutter-web/serve.py` is the one
11// that faces a browser over the internet, and it has all three.
12//
13// dart tools/serve-dir.dart <directory> [port]
14import 'dart:io';
15
16// Enough of them for a Flutter web bundle, which is HTML, JavaScript, JSON, a
17// wasm blob, fonts and images. Anything else goes out as bytes.
18const _types = <String, String>{
19 '.html': 'text/html; charset=utf-8',
20 '.js': 'application/javascript; charset=utf-8',
21 '.mjs': 'application/javascript; charset=utf-8',
22 '.json': 'application/json; charset=utf-8',
23 '.css': 'text/css; charset=utf-8',
24 '.wasm': 'application/wasm',
25 '.png': 'image/png',
26 '.jpg': 'image/jpeg',
27 '.jpeg': 'image/jpeg',
28 '.gif': 'image/gif',
29 '.svg': 'image/svg+xml',
30 '.ico': 'image/x-icon',
31 '.ttf': 'font/ttf',
32 '.otf': 'font/otf',
33 '.woff': 'font/woff',
34 '.woff2': 'font/woff2',
35 '.map': 'application/json; charset=utf-8',
36};
37
38String _typeOf(String path) {
39 final dot = path.lastIndexOf('.');
40 if (dot < 0) return 'application/octet-stream';
41 return _types[path.substring(dot).toLowerCase()] ?? 'application/octet-stream';
42}
43
44Future<void> main(List<String> args) async {
45 if (args.isEmpty) {
46 stderr.writeln('usage: dart serve-dir.dart <directory> [port]');
47 exit(2);
48 }
49 final root = Directory(args[0]).absolute;
50 final port = args.length > 1 ? int.parse(args[1]) : 8080;
51
52 // 0.0.0.0 and not loopback: in the container this is behind a Modal tunnel,
53 // and a server bound to 127.0.0.1 is one the tunnel cannot reach.
54 final server = await HttpServer.bind(InternetAddress.anyIPv4, port);
55 stdout.writeln('serving ${root.path} on :$port');
56
57 await for (final request in server) {
58 var path = Uri.decodeComponent(request.uri.path);
59 if (path.endsWith('/')) path = '${path}index.html';
60 while (path.startsWith('/')) path = path.substring(1);
61 // The one rule that is not "read the file": a path that escapes the root
62 // is refused rather than resolved. This binds to 0.0.0.0, which in a
63 // container means the internet is one tunnel away.
64 final file = File('${root.path}/$path').absolute;
65 final resolved = file.path;
66 if (!resolved.startsWith(root.path)) {
67 request.response.statusCode = HttpStatus.forbidden;
68 await request.response.close();
69 continue;
70 }
71 if (!await file.exists()) {
72 request.response.statusCode = HttpStatus.notFound;
73 request.response.write('not found: $path');
74 await request.response.close();
75 continue;
76 }
77 request.response.headers.contentType = ContentType.parse(_typeOf(resolved));
78 // A single-page app served from a build directory: nothing here is
79 // versioned by name, so every response is one the browser must re-ask for.
80 request.response.headers.set('cache-control', 'no-store');
81 await request.response.addStream(file.openRead());
82 await request.response.close();
83 }
84}