nandi/frqpublic Fork 0
87a28ccbc1ad41565d24fa8c4f68e1f9426fd61e
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.

Three tarballs where a devShell was 5ce66d5 · on 87a28ccbc1ad41565d24fa8c4f68e1f9426fd61e · nandi · 21h ago
serve-dir.dart · 84 lines · 3.3 KBDart Blame HistoryRaw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// A static file server in one file, for `tools/build-web.sh serve`.
//
// `dart:io` and no packages, so there is nothing to `pub get` and nothing to
// pin: the toolchain already has a Dart, because Flutter ships one, and that
// is the whole reason this is Dart rather than the `python3 -m http.server`
// it replaces. Serving a directory should not add a language to the list of
// things a machine must have.
//
// It is the development server and says so: no caching headers, no
// compression, no range requests. `.modal/flutter-web/serve.py` is the one
// that faces a browser over the internet, and it has all three.
//
//   dart tools/serve-dir.dart <directory> [port]
import 'dart:io';

// Enough of them for a Flutter web bundle, which is HTML, JavaScript, JSON, a
// wasm blob, fonts and images. Anything else goes out as bytes.
const _types = <String, String>{
  '.html': 'text/html; charset=utf-8',
  '.js': 'application/javascript; charset=utf-8',
  '.mjs': 'application/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.wasm': 'application/wasm',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.ttf': 'font/ttf',
  '.otf': 'font/otf',
  '.woff': 'font/woff',
  '.woff2': 'font/woff2',
  '.map': 'application/json; charset=utf-8',
};

String _typeOf(String path) {
  final dot = path.lastIndexOf('.');
  if (dot < 0) return 'application/octet-stream';
  return _types[path.substring(dot).toLowerCase()] ?? 'application/octet-stream';
}

Future<void> main(List<String> args) async {
  if (args.isEmpty) {
    stderr.writeln('usage: dart serve-dir.dart <directory> [port]');
    exit(2);
  }
  final root = Directory(args[0]).absolute;
  final port = args.length > 1 ? int.parse(args[1]) : 8080;

  // 0.0.0.0 and not loopback: in the container this is behind a Modal tunnel,
  // and a server bound to 127.0.0.1 is one the tunnel cannot reach.
  final server = await HttpServer.bind(InternetAddress.anyIPv4, port);
  stdout.writeln('serving ${root.path} on :$port');

  await for (final request in server) {
    var path = Uri.decodeComponent(request.uri.path);
    if (path.endsWith('/')) path = '${path}index.html';
    while (path.startsWith('/')) path = path.substring(1);
    // The one rule that is not "read the file": a path that escapes the root
    // is refused rather than resolved. This binds to 0.0.0.0, which in a
    // container means the internet is one tunnel away.
    final file = File('${root.path}/$path').absolute;
    final resolved = file.path;
    if (!resolved.startsWith(root.path)) {
      request.response.statusCode = HttpStatus.forbidden;
      await request.response.close();
      continue;
    }
    if (!await file.exists()) {
      request.response.statusCode = HttpStatus.notFound;
      request.response.write('not found: $path');
      await request.response.close();
      continue;
    }
    request.response.headers.contentType = ContentType.parse(_typeOf(resolved));
    // A single-page app served from a build directory: nothing here is
    // versioned by name, so every response is one the browser must re-ask for.
    request.response.headers.set('cache-control', 'no-store');
    await request.response.addStream(file.openRead());
    await request.response.close();
  }
}