From c1159d42b926f38474caa28fdda02ce9a7742bc9 Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Fri, 26 Jul 2019 10:18:00 -0600 Subject: [PATCH 1/2] Add ability to extend the shelf server cascade. --- example/pubspec.yaml | 12 +++++ example/tool/webdev_proxy.dart | 13 ++++++ example/web/index.html | 1 + lib/src/command_runner.dart | 5 ++- lib/src/dart_tool_cache.dart | 16 +++++++ lib/src/ensure_process_exit.dart | 30 +++++++++++++ lib/src/executable.dart | 70 +++++++++++++++++++++++++++++- lib/src/exit_process_signals.dart | 9 ++++ lib/src/logging.dart | 2 +- lib/src/serve_command.dart | 7 ++- lib/src/webdev_proc_utils.dart | 2 +- lib/src/webdev_proxy_server.dart | 8 +++- test/webdev_proxy_server_test.dart | 16 +++++++ 13 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 example/pubspec.yaml create mode 100644 example/tool/webdev_proxy.dart create mode 100644 example/web/index.html create mode 100644 lib/src/dart_tool_cache.dart create mode 100644 lib/src/ensure_process_exit.dart create mode 100644 lib/src/exit_process_signals.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..437d0ac --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,12 @@ +name: webdev_proxy_example +version: 0.0.0 +private: true + +dev_dependencies: + # These two build deps are required by webdev. + build_runner: ^1.5.1 + build_web_compilers: '>=1.2.2 <3.0.0' + + shelf: ^0.7.2 + webdev_proxy: + path: .. diff --git a/example/tool/webdev_proxy.dart b/example/tool/webdev_proxy.dart new file mode 100644 index 0000000..79b1a06 --- /dev/null +++ b/example/tool/webdev_proxy.dart @@ -0,0 +1,13 @@ +import 'package:shelf/shelf.dart'; + +Iterable get handlers => [ + customStatusHandler, + ]; + +Response customStatusHandler(Request request) { + final customStatus = request.requestedUri.queryParameters['status']; + if (customStatus != null) { + return Response(int.parse(customStatus)); + } + return Response.notFound(''); +} diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..e534282 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1 @@ +

Hello!

\ No newline at end of file diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index c2b6717..6c70727 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -15,6 +15,7 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:logging/logging.dart'; +import 'package:shelf/shelf.dart'; import 'package:webdev_proxy/src/logging.dart'; import 'package:webdev_proxy/src/serve_command.dart'; @@ -23,10 +24,10 @@ import 'package:webdev_proxy/src/serve_command.dart'; class WebdevProxy extends CommandRunner { static const verboseFlag = 'verbose'; - WebdevProxy() + WebdevProxy({Iterable customHandlers}) : super('webdev_proxy', 'A simple dart proxy for `webdev serve` (uses the `shelf_proxy` package).') { - addCommand(new ServeCommand()); + addCommand(ServeCommand(customHandlers: customHandlers)); argParser.addFlag(verboseFlag, abbr: 'v', help: 'Enable verbose output.'); } diff --git a/lib/src/dart_tool_cache.dart b/lib/src/dart_tool_cache.dart new file mode 100644 index 0000000..c3c946e --- /dev/null +++ b/lib/src/dart_tool_cache.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +const cacheDirPath = '.dart_tool/webdev_proxy'; + +void createCacheDir({String subPath}) { + var path = cacheDirPath; + if (subPath != null) { + path = p.join(path, subPath); + } + final dir = Directory(path); + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } +} diff --git a/lib/src/ensure_process_exit.dart b/lib/src/ensure_process_exit.dart new file mode 100644 index 0000000..bba54aa --- /dev/null +++ b/lib/src/ensure_process_exit.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import 'exit_process_signals.dart'; + +/// Ensures that the current process does not exit until the given [process] +/// exits. +/// +/// This function prevents the current process from exiting by watching the exit +/// signals for the current platform (SIGINT on Windows, SIGINT and SIGTERM for +/// other platforms) and consuming them until [process] exits. +/// +/// If [forwardExitSignals] is true, the exit signals received by the current +/// process will be forwarded via [process.kill]. This is only needed if the +/// given [process] was started in either the [ProcessStartMode.detached] or +/// [ProcessStartMode.detachedWithStdio] modes. +void ensureProcessExit(Process process, {bool forwardExitSignals, Logger log}) { + forwardExitSignals ??= false; + var signalsSub = exitProcessSignals.listen((signal) async { + log?.info('Waiting for subprocess to exit...'); + if (forwardExitSignals) { + process.kill(signal); + } + }); + process.exitCode.then((_) { + signalsSub?.cancel(); + signalsSub = null; + }); +} diff --git a/lib/src/executable.dart b/lib/src/executable.dart index dfe0fe9..d73d5de 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -13,20 +13,86 @@ // limitations under the License. import 'dart:async'; +import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:io/ansi.dart'; import 'package:io/io.dart' show ExitCode; +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart'; -import 'package:webdev_proxy/src/command_runner.dart'; +import 'command_runner.dart'; +import 'dart_tool_cache.dart'; +import 'ensure_process_exit.dart'; + +typedef _HandlersGetter = Iterable Function(); + +const _customWebdevProxyDartPath = 'tool/webdev_proxy.dart'; +final _customEntrypointPath = p.join(cacheDirPath, 'custom_executable.dart'); +final _customEntrypoint = File(_customEntrypointPath); +final _relativeWebdevProxyDartPath = p.relative( + p.absolute(_customWebdevProxyDartPath), + from: p.absolute(p.dirname(_customEntrypointPath)), +); +final _customEntrypointContents = ''' +import 'dart:io'; + +import 'package:webdev_proxy/src/executable.dart' as executable; +import '$_relativeWebdevProxyDartPath' as custom_webdev_proxy; + +void main(List args) async { + exit(await executable.runWithConfig(args, () => custom_webdev_proxy.handlers)); +} +'''; /// Runs the [WebdevProxy] command-runner and returns its exit code. /// /// Also catches and prints [UsageException]s. Future run(List args) async { + if (File(_customWebdevProxyDartPath).existsSync()) { + return runViaCustomEntrypoint(args); + } + return _run(args); +} + +Future runWithConfig( + List args, _HandlersGetter handlersGetter) async { + Iterable customHandlers; + try { + customHandlers = handlersGetter(); + } catch (_) {} + return _run(args, customHandlers: customHandlers); +} + +Future runViaCustomEntrypoint(List args) async { + _generateCustomEntrypoint(); + final process = await Process.start( + Platform.executable, + [ + _customEntrypointPath, + ...args, + ], + mode: ProcessStartMode.inheritStdio); + ensureProcessExit(process); + return process.exitCode; +} + +void _generateCustomEntrypoint() { + if (_shouldWriteCustomEntrypoint()) { + createCacheDir(); + _customEntrypoint.writeAsStringSync(_customEntrypointContents); + } +} + +bool _shouldWriteCustomEntrypoint() { + return !_customEntrypoint.existsSync() || + _customEntrypoint.readAsStringSync() != _customEntrypointContents; +} + +Future _run(List args, {Iterable customHandlers}) async { try { // Explicitly `await` here so we can catch any usage exceptions. - return await WebdevProxy().run(args); + return await WebdevProxy(customHandlers: customHandlers).run(args); } on UsageException catch (e) { print(red.wrap(e.message)); print(''); diff --git a/lib/src/exit_process_signals.dart b/lib/src/exit_process_signals.dart new file mode 100644 index 0000000..9d1ee03 --- /dev/null +++ b/lib/src/exit_process_signals.dart @@ -0,0 +1,9 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; + +Stream get exitProcessSignals => Platform.isWindows + ? ProcessSignal.sigint.watch() + : StreamGroup.merge( + [ProcessSignal.sigterm.watch(), ProcessSignal.sigint.watch()]); diff --git a/lib/src/logging.dart b/lib/src/logging.dart index a75b559..2ab484c 100644 --- a/lib/src/logging.dart +++ b/lib/src/logging.dart @@ -50,7 +50,7 @@ StringBuffer colorLog(LogRecord record, {bool verbose}) { } if (record.stackTrace != null && verbose) { - final trace = new Trace.from(record.stackTrace).terse; + final trace = Trace.from(record.stackTrace).terse; lines.add(trace); } diff --git a/lib/src/serve_command.dart b/lib/src/serve_command.dart index e359ed6..7585708 100644 --- a/lib/src/serve_command.dart +++ b/lib/src/serve_command.dart @@ -19,6 +19,7 @@ import 'package:args/command_runner.dart'; import 'package:io/ansi.dart'; import 'package:io/io.dart'; import 'package:pedantic/pedantic.dart'; +import 'package:shelf/shelf.dart'; import 'package:webdev_proxy/src/command_runner.dart'; import 'package:webdev_proxy/src/command_utils.dart'; @@ -35,7 +36,10 @@ import 'package:webdev_proxy/src/webdev_server.dart'; class ServeCommand extends Command { static const rewrite404sFlag = 'rewrite-404s'; - ServeCommand() { + final Iterable _customHandlers; + + ServeCommand({Iterable customHandlers}) + : _customHandlers = customHandlers { argParser.addFlag(rewrite404sFlag, defaultsTo: true, help: 'Rewrite every request that returns a 404 to /index.html'); @@ -169,6 +173,7 @@ class ServeCommand extends Command { for (final dir in portsToServeByDir.keys) { try { proxies.add(await WebdevProxyServer.start( + customHandlers: _customHandlers, dir: dir, hostname: hostname, portToProxy: portsToProxyByDir[dir], diff --git a/lib/src/webdev_proc_utils.dart b/lib/src/webdev_proc_utils.dart index 8177093..5840708 100644 --- a/lib/src/webdev_proc_utils.dart +++ b/lib/src/webdev_proc_utils.dart @@ -21,7 +21,7 @@ import 'package:pub_semver/pub_semver.dart'; /// The range of `webdev` versions with which this `webdev_proxy` package is /// compatible. -final webdevCompatibility = new VersionConstraint.parse('>=1.0.1 <3.0.0'); +final webdevCompatibility = VersionConstraint.parse('>=1.0.1 <3.0.0'); @visibleForTesting ProcessResult cachedWebdevVersionResult; diff --git a/lib/src/webdev_proxy_server.dart b/lib/src/webdev_proxy_server.dart index a5173b6..730e18a 100644 --- a/lib/src/webdev_proxy_server.dart +++ b/lib/src/webdev_proxy_server.dart @@ -18,6 +18,7 @@ import 'package:http_multi_server/http_multi_server.dart'; import 'package:io/ansi.dart'; import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_proxy/shelf_proxy.dart' as shelf_proxy; import 'package:webdev_proxy/src/proxy_root_index_handler.dart'; @@ -54,6 +55,7 @@ class WebdevProxyServer { @required String dir, @required String hostname, @required int portToProxy, + Iterable customHandlers, int portToServe, bool rewrite404s, }) async { @@ -67,7 +69,11 @@ class WebdevProxyServer { final proxyHandler = shelf_proxy.proxyHandler(serverUri, proxyName: 'webdev_proxy'); - var cascade = shelf.Cascade() + var cascade = shelf.Cascade(); + for (final handler in customHandlers ?? []) { + cascade = cascade.add(handler); + } + cascade = cascade .add(SseProxyHandler(sseUri, serverSseUri).handler) .add(proxyHandler); if (rewrite404s) { diff --git a/test/webdev_proxy_server_test.dart b/test/webdev_proxy_server_test.dart index 1012438..a5c595e 100644 --- a/test/webdev_proxy_server_test.dart +++ b/test/webdev_proxy_server_test.dart @@ -121,4 +121,20 @@ void main() { await http.get('http://localhost:${proxy.port}/path/to/nothing'); expect(response.statusCode, 404); }); + + test('Prepends custom handlers to the cascade', () async { + shelf.Response serverErrorHandler(shelf.Request request) => + shelf.Response.internalServerError(); + + proxy = await WebdevProxyServer.start( + customHandlers: [serverErrorHandler], + dir: 'test', + hostname: 'localhost', + portToProxy: server.port, + rewrite404s: false, + ); + + final response = await http.get('http://localhost:${proxy.port}/'); + expect(response.statusCode, 500); + }); } From b921020d2e0b30222ae60515334f99ba1e5b13f7 Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Fri, 26 Jul 2019 17:58:10 -0600 Subject: [PATCH 2/2] Allow custom middleware. --- example/tool/webdev_proxy.dart | 24 +++++++++++++++++++++++ example/web/rewrite.html | 1 + lib/src/command_runner.dart | 6 ++++-- lib/src/executable.dart | 24 +++++++++++++++++------ lib/src/serve_command.dart | 8 ++++++-- lib/src/webdev_proxy_server.dart | 33 ++++++++++++++++++++++++-------- 6 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 example/web/rewrite.html diff --git a/example/tool/webdev_proxy.dart b/example/tool/webdev_proxy.dart index 79b1a06..be9b86a 100644 --- a/example/tool/webdev_proxy.dart +++ b/example/tool/webdev_proxy.dart @@ -4,6 +4,10 @@ Iterable get handlers => [ customStatusHandler, ]; +Iterable get middleware => [ + rewriteMiddleware, + ]; + Response customStatusHandler(Request request) { final customStatus = request.requestedUri.queryParameters['status']; if (customStatus != null) { @@ -11,3 +15,23 @@ Response customStatusHandler(Request request) { } return Response.notFound(''); } + +Handler rewriteMiddleware(Handler innerHandler) { + return (originalReq) { + var req = originalReq; + if (req.requestedUri.queryParameters['rewrite'] != null) { + req = _copyRequest(req, req.requestedUri.replace(path: '/rewrite.html')); + } + return innerHandler(req); + }; +} + +Request _copyRequest(Request oldRequest, [Uri newPath]) { + newPath ??= oldRequest.url; + return Request(oldRequest.method, newPath, + protocolVersion: oldRequest.protocolVersion, + headers: oldRequest.headers, + body: oldRequest.read(), + encoding: oldRequest.encoding, + context: oldRequest.context); +} diff --git a/example/web/rewrite.html b/example/web/rewrite.html new file mode 100644 index 0000000..e83db4c --- /dev/null +++ b/example/web/rewrite.html @@ -0,0 +1 @@ +

Rewrite!

\ No newline at end of file diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 6c70727..9993c2e 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -24,10 +24,12 @@ import 'package:webdev_proxy/src/serve_command.dart'; class WebdevProxy extends CommandRunner { static const verboseFlag = 'verbose'; - WebdevProxy({Iterable customHandlers}) + WebdevProxy( + {Iterable customHandlers, Iterable customMiddleware}) : super('webdev_proxy', 'A simple dart proxy for `webdev serve` (uses the `shelf_proxy` package).') { - addCommand(ServeCommand(customHandlers: customHandlers)); + addCommand(ServeCommand( + customHandlers: customHandlers, customMiddleware: customMiddleware)); argParser.addFlag(verboseFlag, abbr: 'v', help: 'Enable verbose output.'); } diff --git a/lib/src/executable.dart b/lib/src/executable.dart index d73d5de..4182406 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -26,6 +26,7 @@ import 'dart_tool_cache.dart'; import 'ensure_process_exit.dart'; typedef _HandlersGetter = Iterable Function(); +typedef _MiddlewareGetter = Iterable Function(); const _customWebdevProxyDartPath = 'tool/webdev_proxy.dart'; final _customEntrypointPath = p.join(cacheDirPath, 'custom_executable.dart'); @@ -41,7 +42,9 @@ import 'package:webdev_proxy/src/executable.dart' as executable; import '$_relativeWebdevProxyDartPath' as custom_webdev_proxy; void main(List args) async { - exit(await executable.runWithConfig(args, () => custom_webdev_proxy.handlers)); + exit(await executable.runWithConfig(args, + () => custom_webdev_proxy.handlers, + () => custom_webdev_proxy.middleware)); } '''; @@ -55,13 +58,18 @@ Future run(List args) async { return _run(args); } -Future runWithConfig( - List args, _HandlersGetter handlersGetter) async { +Future runWithConfig(List args, _HandlersGetter handlersGetter, + _MiddlewareGetter middlewareGetter) async { Iterable customHandlers; + Iterable customMiddleware; try { customHandlers = handlersGetter(); } catch (_) {} - return _run(args, customHandlers: customHandlers); + try { + customMiddleware = middlewareGetter(); + } catch (_) {} + return _run(args, + customHandlers: customHandlers, customMiddleware: customMiddleware); } Future runViaCustomEntrypoint(List args) async { @@ -89,10 +97,14 @@ bool _shouldWriteCustomEntrypoint() { _customEntrypoint.readAsStringSync() != _customEntrypointContents; } -Future _run(List args, {Iterable customHandlers}) async { +Future _run(List args, + {Iterable customHandlers, + Iterable customMiddleware}) async { try { // Explicitly `await` here so we can catch any usage exceptions. - return await WebdevProxy(customHandlers: customHandlers).run(args); + return await WebdevProxy( + customHandlers: customHandlers, customMiddleware: customMiddleware) + .run(args); } on UsageException catch (e) { print(red.wrap(e.message)); print(''); diff --git a/lib/src/serve_command.dart b/lib/src/serve_command.dart index 7585708..06a3911 100644 --- a/lib/src/serve_command.dart +++ b/lib/src/serve_command.dart @@ -37,9 +37,12 @@ class ServeCommand extends Command { static const rewrite404sFlag = 'rewrite-404s'; final Iterable _customHandlers; + final Iterable _customMiddleware; - ServeCommand({Iterable customHandlers}) - : _customHandlers = customHandlers { + ServeCommand( + {Iterable customHandlers, Iterable customMiddleware}) + : _customHandlers = customHandlers, + _customMiddleware = customMiddleware { argParser.addFlag(rewrite404sFlag, defaultsTo: true, help: 'Rewrite every request that returns a 404 to /index.html'); @@ -174,6 +177,7 @@ class ServeCommand extends Command { try { proxies.add(await WebdevProxyServer.start( customHandlers: _customHandlers, + customMiddleware: _customMiddleware, dir: dir, hostname: hostname, portToProxy: portsToProxyByDir[dir], diff --git a/lib/src/webdev_proxy_server.dart b/lib/src/webdev_proxy_server.dart index 730e18a..91a1dfa 100644 --- a/lib/src/webdev_proxy_server.dart +++ b/lib/src/webdev_proxy_server.dart @@ -56,32 +56,49 @@ class WebdevProxyServer { @required String hostname, @required int portToProxy, Iterable customHandlers, + Iterable customMiddleware, int portToServe, bool rewrite404s, }) async { + customHandlers ??= []; + customMiddleware ??= []; portToServe ??= 0; rewrite404s ??= true; + // Construct our handler using a Cascade and starting with custom handlers + // if any were provided. + var cascade = shelf.Cascade(); + for (final customHandler in customHandlers ?? []) { + cascade = cascade.add(customHandler); + } + + // Add the proxy handler for the `/$sseHandler` route to the cascade. final serverHostname = hostname == 'any' ? 'localhost' : hostname; final serverUri = Uri.parse('http://$serverHostname:$portToProxy'); final serverSseUri = serverUri.replace(path: r'/$sseHandler'); final sseUri = Uri.parse(r'/$sseHandler'); + cascade = cascade.add(SseProxyHandler(sseUri, serverSseUri).handler); + // Add the default handler for all requests that proxies the webdev server. final proxyHandler = shelf_proxy.proxyHandler(serverUri, proxyName: 'webdev_proxy'); - var cascade = shelf.Cascade(); - for (final handler in customHandlers ?? []) { - cascade = cascade.add(handler); - } - cascade = cascade - .add(SseProxyHandler(sseUri, serverSseUri).handler) - .add(proxyHandler); + cascade = cascade.add(proxyHandler); + + // Add the 404-to-root-index rewrite handler, if enabled. if (rewrite404s) { cascade = cascade.add(proxyRootIndexHandler(proxyHandler)); } + // Construct our final handler by creating a pipeline with custom middleware + // (if any were provided) and our cascade handler. + var pipeline = Pipeline(); + for (final middleware in customMiddleware) { + pipeline = pipeline.addMiddleware(middleware); + } + final handler = pipeline.addHandler(cascade.handler); + final server = await HttpMultiServer.bind(hostname, portToServe); - shelf_io.serveRequests(server, cascade.handler); + shelf_io.serveRequests(server, handler); final proxyHostname = hostname == 'any' ? '::' : hostname; log.info(green.wrap('Serving `$dir` proxy on ' 'http://$proxyHostname:$portToServe') +