- Add VariableSpec class with magnitude field validation - Add Formula class supporting multiple input/output variables - Support d4rt_code as string or object with code field - Add comprehensive tests for parsing and serialization - Fix broken test import in pruebas_d4rt_test.dart Follows README.md format requirements exactly
290 lines
9.9 KiB
Dart
290 lines
9.9 KiB
Dart
// 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<HttpServer> 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<HttpRequest>] 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<HttpRequest> 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<void> 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 = <String, List<String>>{};
|
|
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<List<int>>) 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<void> _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);
|
|
}
|