// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// A Shelf adapter for handling [HttpRequest] objects from `dart:io`'s /// [HttpServer]. /// /// One can provide an instance of [HttpServer] as the `requests` parameter in /// [serveRequests]. /// /// This adapter supports request hijacking; see [Request.hijack]. /// /// [Request]s passed to a [Handler] will contain the [Request.context] key /// `"shelf.io.connection_info"` containing the [HttpConnectionInfo] object from /// the underlying [HttpRequest]. /// /// When creating [Response] instances for this adapter, you can set the /// `"shelf.io.buffer_output"` key in [Response.context]. If `true`, /// (the default), streamed responses will be buffered to improve performance. /// If `false`, all chunks will be pushed over the wire as they're received. /// See [HttpResponse.bufferOutput] for more information. library; import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:http_parser/http_parser.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:stream_channel/stream_channel.dart'; import 'shelf.dart'; import 'src/util.dart'; export 'src/io_server.dart' show IOServer; /// Starts an [HttpServer] that listens on the specified [address] and /// [port] and sends requests to [handler]. /// /// If a [securityContext] is provided an HTTPS server will be started. /// /// See the documentation for [HttpServer.bind] and [HttpServer.bindSecure] /// for more details on [address], [port], [backlog], and [shared]. /// /// {@template shelf_io_header_defaults} /// Every response will get a "date" header and an "X-Powered-By" header. /// If the either header is present in the `Response`, it will not be /// overwritten. /// Pass [poweredByHeader] to set the default content for "X-Powered-By", /// pass `null` to omit this header. /// {@endtemplate} Future serve( Handler handler, Object address, int port, { SecurityContext? securityContext, int? backlog, bool shared = false, String? poweredByHeader = 'Dart with package:shelf', }) async { backlog ??= 0; var server = await (securityContext == null ? HttpServer.bind(address, port, backlog: backlog, shared: shared) : HttpServer.bindSecure( address, port, securityContext, backlog: backlog, shared: shared, )); serveRequests(server, handler, poweredByHeader: poweredByHeader); return server; } /// Serve a [Stream] of [HttpRequest]s. /// /// [HttpServer] implements [Stream] so it can be passed directly /// to [serveRequests]. /// /// Errors thrown by [handler] while serving a request will be printed to the /// console and cause a 500 response with no body. Errors thrown asynchronously /// by [handler] will be printed to the console or, if there's an active error /// zone, passed to that zone. /// /// {@macro shelf_io_header_defaults} void serveRequests( Stream requests, Handler handler, { String? poweredByHeader = 'Dart with package:shelf', }) { catchTopLevelErrors(() { requests.listen((request) => handleRequest(request, handler, poweredByHeader: poweredByHeader)); }, (error, stackTrace) { _logTopLevelError('Asynchronous error\n$error', stackTrace); }); } /// Uses [handler] to handle [request]. /// /// Returns a [Future] which completes when the request has been handled. /// /// {@macro shelf_io_header_defaults} Future handleRequest( HttpRequest request, Handler handler, { String? poweredByHeader = 'Dart with package:shelf', }) async { Request shelfRequest; try { shelfRequest = _fromHttpRequest(request); // ignore: avoid_catching_errors } on ArgumentError catch (error, stackTrace) { if (error.name == 'method' || error.name == 'requestedUri') { // TODO: use a reduced log level when using package:logging _logTopLevelError('Error parsing request.\n$error', stackTrace); final response = Response( 400, body: 'Bad Request', headers: {HttpHeaders.contentTypeHeader: 'text/plain'}, ); await _writeResponse(response, request.response, poweredByHeader); } else { _logTopLevelError('Error parsing request.\n$error', stackTrace); final response = Response.internalServerError(); await _writeResponse(response, request.response, poweredByHeader); } return; } catch (error, stackTrace) { _logTopLevelError('Error parsing request.\n$error', stackTrace); final response = Response.internalServerError(); await _writeResponse(response, request.response, poweredByHeader); return; } // TODO(nweiz): abstract out hijack handling to make it easier to implement an // adapter. Response? response; try { response = await handler(shelfRequest); } on HijackException catch (error, stackTrace) { // A HijackException should bypass the response-writing logic entirely. if (!shelfRequest.canHijack) return; // If the request wasn't hijacked, we shouldn't be seeing this exception. response = _logError( shelfRequest, "Caught HijackException, but the request wasn't hijacked.", stackTrace, ); } catch (error, stackTrace) { response = _logError( shelfRequest, 'Error thrown by handler.\n$error', stackTrace, ); } if ((response as dynamic) == null) { // Handle nulls flowing from opt-out code await _writeResponse( _logError( shelfRequest, 'null response from handler.', StackTrace.current), request.response, poweredByHeader); return; } if (shelfRequest.canHijack) { await _writeResponse(response, request.response, poweredByHeader); return; } var message = StringBuffer() ..writeln('Got a response for hijacked request ' '${shelfRequest.method} ${shelfRequest.requestedUri}:') ..writeln(response.statusCode); response.headers.forEach((key, value) => message.writeln('$key: $value')); throw Exception(message.toString().trim()); } /// Creates a new [Request] from the provided [HttpRequest]. Request _fromHttpRequest(HttpRequest request) { var headers = >{}; request.headers.forEach((k, v) { headers[k] = v; }); // Remove the Transfer-Encoding header per the adapter requirements. headers.remove(HttpHeaders.transferEncodingHeader); void onHijack(void Function(StreamChannel>) callback) { request.response .detachSocket(writeHeaders: false) .then((socket) => callback(StreamChannel(socket, socket))); } return Request( request.method, request.requestedUri, protocolVersion: request.protocolVersion, headers: headers, body: request, onHijack: onHijack, context: {'shelf.io.connection_info': request.connectionInfo!}, ); } Future _writeResponse( Response response, HttpResponse httpResponse, String? poweredByHeader) { if (response.context.containsKey('shelf.io.buffer_output')) { httpResponse.bufferOutput = response.context['shelf.io.buffer_output'] as bool; } httpResponse.statusCode = response.statusCode; // An adapter must not add or modify the `Transfer-Encoding` parameter, but // the Dart SDK sets it by default. Set this before we fill in // [response.headers] so that the user or Shelf can explicitly override it if // necessary. httpResponse.headers.chunkedTransferEncoding = false; response.headersAll.forEach((header, value) { httpResponse.headers.set(header, value); }); var coding = response.headers['transfer-encoding']; if (coding != null && !equalsIgnoreAsciiCase(coding, 'identity')) { // If the response is already in a chunked encoding, de-chunk it because // otherwise `dart:io` will try to add another layer of chunking. // // TODO(nweiz): Do this more cleanly when sdk#27886 is fixed. response = response.change( body: chunkedCoding.decoder.bind(response.read()), ); httpResponse.headers.set(HttpHeaders.transferEncodingHeader, 'chunked'); } else if (response.statusCode >= 200 && response.statusCode != 204 && response.statusCode != 304 && response.contentLength == null && response.mimeType != 'multipart/byteranges') { // If the response isn't chunked yet and there's no other way to tell its // length, enable `dart:io`'s chunked encoding. httpResponse.headers.set(HttpHeaders.transferEncodingHeader, 'chunked'); } if (poweredByHeader != null && !response.headers.containsKey(_xPoweredByResponseHeader)) { httpResponse.headers.set(_xPoweredByResponseHeader, poweredByHeader); } if (!response.headers.containsKey(HttpHeaders.dateHeader)) { httpResponse.headers.date = DateTime.now().toUtc(); } return httpResponse .addStream(response.read()) .then((_) => httpResponse.close()); } /// Common header to advertise the server technology being used. /// /// See https://webtechsurvey.com/response-header/x-powered-by const _xPoweredByResponseHeader = 'X-Powered-By'; // TODO(kevmoo) A developer mode is needed to include error info in response // TODO(kevmoo) Make error output plugable. stderr, logging, etc Response _logError(Request request, String message, StackTrace stackTrace) { // Add information about the request itself. var buffer = StringBuffer(); buffer.write('${request.method} ${request.requestedUri.path}'); if (request.requestedUri.query.isNotEmpty) { buffer.write('?${request.requestedUri.query}'); } buffer.writeln(); buffer.write(message); _logTopLevelError(buffer.toString(), stackTrace); return Response.internalServerError(); } void _logTopLevelError(String message, StackTrace stackTrace) { final chain = Chain.forTrace(stackTrace) .foldFrames((frame) => frame.isCore || frame.package == 'shelf') .terse; stderr.writeln('ERROR - ${DateTime.now()}'); stderr.writeln(message); stderr.writeln(chain); }