platform/lib/src/http/response_context.dart

529 lines
14 KiB
Dart
Raw Normal View History

library angel_framework.http.response_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
2016-10-22 20:41:36 +00:00
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
2017-11-28 18:14:50 +00:00
import 'package:pool/pool.dart';
2017-03-02 04:04:37 +00:00
import 'server.dart' show Angel;
import 'controller.dart';
2017-08-15 23:01:16 +00:00
import 'request_context.dart';
2016-04-18 03:27:23 +00:00
2016-12-21 03:10:03 +00:00
final RegExp _contentType =
2017-06-19 01:53:51 +00:00
new RegExp(r'([^/\n]+)\/\s*([^;\n]+)\s*(;\s*charset=([^$;\n]+))?');
2016-12-21 03:10:03 +00:00
2016-11-28 00:49:27 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2016-12-31 01:46:41 +00:00
/// Serializes response data into a String.
///
/// Prefer the String Function(dynamic) syntax.
@deprecated
2016-12-31 01:46:41 +00:00
typedef String ResponseSerializer(data);
2016-04-18 03:27:23 +00:00
/// A convenience wrapper around an outgoing HTTP request.
2017-08-15 23:01:16 +00:00
class ResponseContext implements StreamSink<List<int>>, StringSink {
final Map properties = {};
2017-08-15 23:01:16 +00:00
final BytesBuilder _buffer = new _LockableBytesBuilder();
final Map<String, String> _headers = {HttpHeaders.SERVER: 'angel'};
final RequestContext _correspondingRequest;
Completer _done;
bool _isOpen = true, _isClosed = false, _useStream = false;
2017-05-27 12:39:45 +00:00
int _statusCode = 200;
2016-10-22 20:41:36 +00:00
2016-04-18 03:27:23 +00:00
/// The [Angel] instance that is sending a response.
2017-03-02 04:04:37 +00:00
Angel app;
2016-04-18 03:27:23 +00:00
2016-12-19 04:32:36 +00:00
/// Is `Transfer-Encoding` chunked?
bool chunked;
2016-12-19 01:38:23 +00:00
/// Any and all cookies to be sent to the user.
final List<Cookie> cookies = [];
2017-08-15 23:01:16 +00:00
/// A set of [Converter] objects that can be used to encode response data.
///
/// At most one encoder will ever be used to convert data.
final Map<String, Converter<List<int>, List<int>>> encoders = {};
/// Points to the [RequestContext] corresponding to this response.
RequestContext get correspondingRequest => _correspondingRequest;
@override
Future get done => (_done ?? new Completer()).future;
2016-12-19 01:38:23 +00:00
/// Headers that will be sent to the user.
2017-03-28 23:29:22 +00:00
Map<String, String> get headers {
2017-04-03 19:43:27 +00:00
/// If the response is closed, then this getter will return an immutable `Map`.
2017-04-25 03:19:36 +00:00
if (_isClosed)
2017-03-28 23:29:22 +00:00
return new Map<String, String>.unmodifiable(_headers);
2017-04-25 02:32:16 +00:00
else
return _headers;
2017-03-28 23:29:22 +00:00
}
2016-12-19 01:38:23 +00:00
2016-12-31 01:46:41 +00:00
/// Serializes response data into a String.
///
2017-03-28 23:29:22 +00:00
/// The default is conversion into JSON via `package:json_god`.
///
/// If you are 100% sure that your response handlers will only
/// be JSON-encodable objects (i.e. primitives, `List`s and `Map`s),
/// then consider setting [serializer] to `JSON.encode`.
///
/// To set it globally for the whole [app], use the following helper:
/// ```dart
/// app.injectSerializer(JSON.encode);
/// ```
String Function(dynamic) serializer = god.serialize;
2016-12-31 01:46:41 +00:00
2016-12-19 01:38:23 +00:00
/// This response's status code.
2017-05-27 12:39:45 +00:00
int get statusCode => _statusCode;
void set statusCode(int value) {
if (_isClosed)
throw _closed();
else
_statusCode = value ?? 200;
}
2016-12-19 01:38:23 +00:00
2016-04-18 03:27:23 +00:00
/// Can we still write to this response?
bool get isOpen => _isOpen && !_isClosed;
2016-04-18 03:27:23 +00:00
/// A set of UTF-8 encoded bytes that will be written to the response.
2017-03-28 23:29:22 +00:00
BytesBuilder get buffer => _buffer;
2016-04-18 03:27:23 +00:00
/// The underlying [HttpResponse] under this instance.
2016-11-23 19:50:17 +00:00
final HttpResponse io;
2016-04-18 03:27:23 +00:00
2016-12-21 03:10:03 +00:00
/// Gets the Content-Type header.
ContentType get contentType {
if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return null;
var header = headers[HttpHeaders.CONTENT_TYPE];
var match = _contentType.firstMatch(header);
if (match == null)
throw new Exception('Malformed Content-Type response header: "$header".');
if (match[4]?.isNotEmpty != true)
return new ContentType(match[1], match[2]);
else
return new ContentType(match[1], match[2], charset: match[4]);
}
/// Sets the Content-Type header.
void set contentType(ContentType contentType) {
headers[HttpHeaders.CONTENT_TYPE] = contentType.toString();
}
2017-08-15 23:01:16 +00:00
ResponseContext(this.io, this.app, [this._correspondingRequest]);
2016-04-18 03:27:23 +00:00
/// Set this to true if you will manually close the response.
2016-12-31 01:46:41 +00:00
///
2016-12-21 18:18:26 +00:00
/// If `true`, all response finalizers will be skipped.
2016-04-18 03:27:23 +00:00
bool willCloseItself = false;
2017-03-28 23:29:22 +00:00
StateError _closed() => new StateError('Cannot modify a closed response.');
2016-04-18 03:27:23 +00:00
/// Sends a download as a response.
2017-09-22 04:48:22 +00:00
Future download(File file, {String filename}) async {
2017-04-25 02:32:16 +00:00
if (!_isOpen) throw _closed();
2017-03-28 23:29:22 +00:00
2016-12-19 01:38:23 +00:00
headers["Content-Disposition"] =
2017-06-19 01:53:51 +00:00
'attachment; filename="${filename ?? file.path}"';
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
headers[HttpHeaders.CONTENT_LENGTH] = file.lengthSync().toString();
2017-08-15 23:01:16 +00:00
if (_useStream) {
await file.openRead().pipe(this);
} else {
buffer.add(await file.readAsBytes());
end();
}
2016-04-18 03:27:23 +00:00
}
2017-04-25 02:32:16 +00:00
/// Prevents more data from being written to the response, and locks it entire from further editing.
2017-08-15 23:01:16 +00:00
Future close() {
var f = new Future.value();
if (_useStream) {
_useStream = false;
_buffer?.clear();
f = io.close();
} else if (_buffer is _LockableBytesBuilder) {
(_buffer as _LockableBytesBuilder)._lock();
}
2017-09-22 04:48:22 +00:00
_isOpen = _useStream = false;
2017-04-25 02:32:16 +00:00
_isClosed = true;
2017-08-15 23:01:16 +00:00
if (_done?.isCompleted == false) _done.complete();
return f;
2017-04-25 02:32:16 +00:00
}
2017-10-28 08:50:16 +00:00
/// Disposes of all resources.
Future dispose() async {
await close();
properties.clear();
encoders.clear();
_buffer.clear();
cookies.clear();
app = null;
_headers.clear();
serializer = null;
}
2017-04-25 02:32:16 +00:00
/// Prevents further request handlers from running on the response, except for response finalizers.
///
/// To disable response finalizers, see [willCloseItself].
void end() {
_isOpen = false;
2017-10-28 08:50:16 +00:00
if (_done?.isCompleted == false) _done.complete();
2016-10-22 20:41:36 +00:00
}
2016-04-18 03:27:23 +00:00
/// Serializes JSON to the response.
2016-12-31 01:46:41 +00:00
void json(value) => serialize(value, contentType: ContentType.JSON);
2016-04-18 03:27:23 +00:00
/// Returns a JSONP response.
2017-03-28 23:29:22 +00:00
void jsonp(value, {String callbackName: "callback", contentType}) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2017-03-28 23:29:22 +00:00
write("$callbackName(${serializer(value)})");
if (contentType != null) {
if (contentType is ContentType)
this.contentType = contentType;
else
headers[HttpHeaders.CONTENT_TYPE] = contentType.toString();
} else
headers[HttpHeaders.CONTENT_TYPE] = 'application/javascript';
2016-04-18 03:27:23 +00:00
end();
}
/// Renders a view to the response stream, and closes the response.
2016-04-22 02:40:37 +00:00
Future render(String view, [Map data]) async {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2016-04-22 02:40:37 +00:00
write(await app.viewGenerator(view, data));
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
2016-04-18 03:27:23 +00:00
end();
}
/// Redirects to user to the given URL.
2016-11-28 00:49:27 +00:00
///
/// [url] can be a `String`, or a `List`.
/// If it is a `List`, a URI will be constructed
/// based on the provided params.
///
/// See [Router]#navigate for more. :)
2016-12-23 01:49:30 +00:00
void redirect(url, {bool absolute: true, int code: 302}) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2017-04-15 17:42:21 +00:00
headers
..[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString()
..[HttpHeaders.LOCATION] =
2017-06-19 01:53:51 +00:00
url is String ? url : app.navigate(url, absolute: absolute);
2016-12-23 01:49:30 +00:00
statusCode = code ?? 302;
2016-04-18 03:27:23 +00:00
write('''
<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; url=$url">
</head>
<body>
<h1>Currently redirecting you...</h1>
<br />
Click <a href="$url">here</a> if you are not automatically redirected...
2016-04-18 03:27:23 +00:00
<script>
window.location = "$url";
</script>
</body>
</html>
''');
end();
}
/// Redirects to the given named [Route].
2016-10-22 20:41:36 +00:00
void redirectTo(String name, [Map params, int code]) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2016-11-28 00:49:27 +00:00
Route _findRoute(Router r) {
for (Route route in r.routes) {
if (route is SymlinkRoute) {
final m = _findRoute(route.router);
2016-11-23 09:10:47 +00:00
2016-11-28 00:49:27 +00:00
if (m != null) return m;
} else if (route.name == name) return route;
2016-11-23 09:10:47 +00:00
}
2016-11-28 00:49:27 +00:00
return null;
2016-11-23 09:10:47 +00:00
}
2016-11-28 00:49:27 +00:00
Route matched = _findRoute(app);
2016-11-23 09:10:47 +00:00
if (matched != null) {
2016-10-22 20:41:36 +00:00
redirect(matched.makeUri(params), code: code);
return;
}
throw new ArgumentError.notNull('Route to redirect to ($name)');
}
2016-06-27 00:20:42 +00:00
/// Redirects to the given [Controller] action.
2016-10-22 20:41:36 +00:00
void redirectToAction(String action, [Map params, int code]) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2016-06-27 00:20:42 +00:00
// UserController@show
List<String> split = action.split("@");
if (split.length < 2)
2016-10-22 20:41:36 +00:00
throw new Exception(
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
2016-06-27 00:20:42 +00:00
2016-11-28 00:49:27 +00:00
Controller controller =
2017-10-04 14:09:12 +00:00
app.controllers[split[0].replaceAll(_straySlashes, '')];
2016-06-27 00:20:42 +00:00
if (controller == null)
throw new Exception("Could not find a controller named '${split[0]}'");
Route matched = controller.routeMappings[split[1]];
2016-06-27 00:20:42 +00:00
if (matched == null)
2016-10-22 20:41:36 +00:00
throw new Exception(
"Controller '${split[0]}' does not contain any action named '${split[1]}'");
2016-06-27 00:20:42 +00:00
2016-11-28 00:49:27 +00:00
final head =
2017-06-19 01:53:51 +00:00
controller.findExpose().path.toString().replaceAll(_straySlashes, '');
2016-11-28 00:49:27 +00:00
final tail = matched.makeUri(params).replaceAll(_straySlashes, '');
redirect('$head/$tail'.replaceAll(_straySlashes, ''), code: code);
2016-06-27 00:20:42 +00:00
}
2016-12-21 18:18:26 +00:00
/// Copies a file's contents into the response buffer.
2017-09-22 04:48:22 +00:00
Future sendFile(File file) async {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2016-04-18 03:27:23 +00:00
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
buffer.add(await file.readAsBytes());
2016-12-21 18:18:26 +00:00
end();
}
2016-12-31 01:46:41 +00:00
/// Serializes data to the response.
///
/// [contentType] can be either a [String], or a [ContentType].
void serialize(value, {contentType}) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2017-03-28 23:29:22 +00:00
var text = serializer(value);
if (text.isEmpty)
return;
2016-12-31 01:46:41 +00:00
if (contentType is String)
headers[HttpHeaders.CONTENT_TYPE] = contentType;
else if (contentType is ContentType) this.contentType = contentType;
2017-07-23 15:41:12 +00:00
write(text);
2016-12-31 01:46:41 +00:00
end();
}
2016-12-21 18:18:26 +00:00
/// Streams a file to this response.
///
/// You can optionally transform the file stream with a [codec].
2017-09-22 04:48:22 +00:00
Future streamFile(File file) {
2017-04-25 02:32:16 +00:00
if (_isClosed) throw _closed();
2016-12-21 18:18:26 +00:00
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
2017-09-22 04:48:22 +00:00
return file.openRead().pipe(this);
2017-08-15 23:01:16 +00:00
}
@override
void add(List<int> data) {
if (_isClosed && !_useStream)
throw _closed();
else if (_useStream)
io.add(data);
else
buffer.add(data);
}
2017-11-28 18:14:50 +00:00
/// Configure the response to write directly to the output stream, instead of buffering.
bool useStream() {
if (!_useStream) {
// If this is the first stream added to this response,
// then add headers, status code, etc.
io
..statusCode = statusCode
..cookies.addAll(cookies);
headers.forEach(io.headers.set);
willCloseItself = _useStream = _isClosed = true;
if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
(_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
}
2017-12-06 14:46:35 +00:00
if (_correspondingRequest?.injections?.containsKey(PoolResource) ==
true) {
(_correspondingRequest.injections[PoolResource] as PoolResource)
.release();
2017-11-28 18:14:50 +00:00
}
return true;
}
return false;
}
2017-08-15 23:01:16 +00:00
/// Adds a stream directly the underlying dart:[io] response.
///
/// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
///
/// If this instance has access to a [correspondingRequest], then it will attempt to transform
/// the content using at most one of the response [encoders].
@override
Future addStream(Stream<List<int>> stream) {
if (_isClosed && !_useStream) throw _closed();
2017-11-28 18:14:50 +00:00
var firstStream = useStream();
2017-10-04 14:09:12 +00:00
2017-08-15 23:01:16 +00:00
Stream<List<int>> output = stream;
if (encoders.isNotEmpty && correspondingRequest != null) {
var allowedEncodings =
(correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? [])
.map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (encoders.containsKey(encodingName))
encoder = encoders[encodingName];
else if (encodingName == '*') {
encoder = encoders[key = encoders.keys.first];
}
if (encoder != null) {
if (firstStream) {
io.headers.set(HttpHeaders.CONTENT_ENCODING, key);
}
output = encoders[key].bind(output);
break;
}
}
}
return io.addStream(output);
}
@override
void addError(Object error, [StackTrace stackTrace]) {
io.addError(error, stackTrace);
if (_done?.isCompleted == false) _done.completeError(error, stackTrace);
2016-04-18 03:27:23 +00:00
}
/// Writes data to the response.
2017-08-15 23:01:16 +00:00
void write(value, {Encoding encoding}) {
encoding ??= UTF8;
if (_isClosed && !_useStream)
2017-03-28 23:29:22 +00:00
throw _closed();
2017-08-15 23:01:16 +00:00
else if (_useStream) {
if (value is List<int>)
io.add(value);
else
io.add(encoding.encode(value.toString()));
} else {
2017-04-25 02:32:16 +00:00
if (value is List<int>)
buffer.add(value);
else
buffer.add(encoding.encode(value.toString()));
}
2016-04-18 03:27:23 +00:00
}
2017-06-19 01:53:51 +00:00
@override
void writeCharCode(int charCode) {
2017-08-15 23:01:16 +00:00
if (_isClosed && !_useStream)
2017-06-19 01:53:51 +00:00
throw _closed();
2017-08-15 23:01:16 +00:00
else if (_useStream)
io.add([charCode]);
2017-06-19 01:53:51 +00:00
else
buffer.addByte(charCode);
}
@override
void writeln([Object obj = ""]) {
write(obj.toString());
write('\r\n');
}
@override
void writeAll(Iterable objects, [String separator = ""]) {
write(objects.join(separator));
}
2016-10-22 20:41:36 +00:00
}
2017-03-28 23:29:22 +00:00
abstract class _LockableBytesBuilder extends BytesBuilder {
2017-10-28 08:50:16 +00:00
factory _LockableBytesBuilder() {
return new _LockableBytesBuilderImpl();
}
2017-05-27 12:39:45 +00:00
2017-03-28 23:29:22 +00:00
void _lock();
}
class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
2017-10-28 08:50:16 +00:00
final BytesBuilder _buf = new BytesBuilder(copy: false);
2017-03-28 23:29:22 +00:00
bool _closed = false;
StateError _deny() =>
new StateError('Cannot modified a closed response\'s buffer.');
@override
void _lock() {
2017-04-25 02:32:16 +00:00
_closed = true;
2017-03-28 23:29:22 +00:00
}
2017-08-15 23:01:16 +00:00
2017-03-28 23:29:22 +00:00
@override
void add(List<int> bytes) {
if (_closed)
throw _deny();
2017-12-06 14:46:35 +00:00
else
_buf.add(bytes);
2017-03-28 23:29:22 +00:00
}
@override
void addByte(int byte) {
if (_closed)
throw _deny();
2017-12-06 14:46:35 +00:00
else
_buf.addByte(byte);
2017-03-28 23:29:22 +00:00
}
@override
void clear() {
2017-10-28 08:50:16 +00:00
_buf.clear();
2017-03-28 23:29:22 +00:00
}
@override
2017-10-28 08:50:16 +00:00
bool get isEmpty => _buf.isEmpty;
2017-03-28 23:29:22 +00:00
@override
2017-10-28 08:50:16 +00:00
bool get isNotEmpty => _buf.isNotEmpty;
2017-03-28 23:29:22 +00:00
@override
2017-10-28 08:50:16 +00:00
int get length => _buf.length;
2017-03-28 23:29:22 +00:00
@override
List<int> takeBytes() {
2017-10-28 08:50:16 +00:00
return _buf.takeBytes();
2017-03-28 23:29:22 +00:00
}
@override
List<int> toBytes() {
2017-10-28 08:50:16 +00:00
return _buf.toBytes();
2017-03-28 23:29:22 +00:00
}
}