2019-03-14 17:24:31 +00:00
|
|
|
import 'dart:async';
|
2019-04-29 08:22:36 +00:00
|
|
|
import 'dart:convert';
|
2019-03-14 17:24:31 +00:00
|
|
|
import 'dart:io';
|
2019-04-29 08:22:36 +00:00
|
|
|
import 'dart:typed_data';
|
2019-03-14 17:24:31 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2019-04-29 08:22:36 +00:00
|
|
|
import 'package:charcode/ascii.dart';
|
|
|
|
import 'wings_request.dart';
|
|
|
|
import 'wings_socket.dart';
|
|
|
|
|
|
|
|
class WingsResponseContext extends ResponseContext<int> {
|
|
|
|
@override
|
|
|
|
final WingsRequestContext correspondingRequest;
|
|
|
|
|
|
|
|
LockableBytesBuilder _buffer;
|
2018-07-03 23:16:29 +00:00
|
|
|
|
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
final int rawResponse;
|
|
|
|
|
|
|
|
bool _isDetached = false, _isClosed = false, _streamInitialized = false;
|
|
|
|
|
|
|
|
WingsResponseContext(this.rawResponse, [this.correspondingRequest]);
|
|
|
|
|
|
|
|
Iterable<String> __allowedEncodings;
|
|
|
|
|
|
|
|
Iterable<String> get _allowedEncodings {
|
|
|
|
return __allowedEncodings ??= correspondingRequest.headers
|
|
|
|
.value('accept-encoding')
|
|
|
|
?.split(',')
|
|
|
|
?.map((s) => s.trim())
|
|
|
|
?.where((s) => s.isNotEmpty)
|
|
|
|
?.map((str) {
|
|
|
|
// Ignore quality specifications in accept-encoding
|
|
|
|
// ex. gzip;q=0.8
|
|
|
|
if (!str.contains(';')) return str;
|
|
|
|
return str.split(';')[0];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _openStream() {
|
|
|
|
if (!_streamInitialized) {
|
|
|
|
// If this is the first stream added to this response,
|
|
|
|
// then add headers, status code, etc.
|
|
|
|
var outHeaders = <String, String>{};
|
|
|
|
var statusLine =
|
|
|
|
utf8.encode('HTTP/1.1 $statusCode').followedBy([$cr, $lf]);
|
|
|
|
writeToNativeSocket(rawResponse, Uint8List.fromList(statusLine.toList()));
|
|
|
|
|
|
|
|
headers.forEach((k, v) => outHeaders[k] = v);
|
|
|
|
|
|
|
|
if (headers.containsKey('content-length')) {
|
|
|
|
var l = int.tryParse(headers['content-length']);
|
|
|
|
if (l != null) {
|
|
|
|
outHeaders['content-length'] = l.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (contentType != null)
|
|
|
|
outHeaders['content-type'] = contentType.toString();
|
|
|
|
|
|
|
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
|
|
|
if (_allowedEncodings != null) {
|
|
|
|
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) {
|
|
|
|
outHeaders['content-encoding'] = key;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _wh(String k, String v) {
|
|
|
|
var headerLine =
|
|
|
|
utf8.encode('$k: ${Uri.encodeComponent(v)}').followedBy([$cr, $lf]);
|
|
|
|
writeToNativeSocket(
|
|
|
|
rawResponse, Uint8List.fromList(headerLine.toList()));
|
|
|
|
}
|
|
|
|
|
|
|
|
outHeaders.forEach(_wh);
|
|
|
|
|
|
|
|
for (var c in cookies) {
|
|
|
|
_wh('set-cookie', c.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
writeToNativeSocket(rawResponse, Uint8List.fromList([$cr, $lf]));
|
|
|
|
|
|
|
|
//_isClosed = true;
|
|
|
|
return _streamInitialized = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2018-07-03 23:16:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-03-14 17:24:31 +00:00
|
|
|
Future addStream(Stream<List<int>> stream) {
|
2019-04-29 08:22:36 +00:00
|
|
|
if (_isClosed && isBuffered) throw ResponseContext.closed();
|
|
|
|
_openStream();
|
|
|
|
|
|
|
|
Stream<List<int>> output = stream;
|
|
|
|
|
|
|
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
|
|
|
if (_allowedEncodings != null) {
|
|
|
|
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) {
|
|
|
|
output = encoders[key].bind(output);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return output.forEach((buf) {
|
|
|
|
writeToNativeSocket(
|
|
|
|
rawResponse, buf is Uint8List ? buf : Uint8List.fromList(buf));
|
|
|
|
});
|
2018-07-03 23:16:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
void add(List<int> data) {
|
|
|
|
if (_isClosed && isBuffered)
|
|
|
|
throw ResponseContext.closed();
|
|
|
|
else if (!isBuffered) {
|
|
|
|
if (!_isClosed) {
|
|
|
|
_openStream();
|
2018-07-03 23:16:29 +00:00
|
|
|
|
2019-04-29 08:22:36 +00:00
|
|
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
|
|
|
if (_allowedEncodings != null) {
|
|
|
|
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) {
|
|
|
|
data = encoders[key].convert(data);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writeToNativeSocket(
|
|
|
|
rawResponse, data is Uint8List ? data : Uint8List.fromList(data));
|
|
|
|
}
|
|
|
|
} else
|
|
|
|
buffer.add(data);
|
|
|
|
}
|
2018-07-03 23:16:29 +00:00
|
|
|
|
2019-03-14 17:24:31 +00:00
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
Future close() {
|
|
|
|
if (!_isDetached) {
|
|
|
|
if (!_isClosed) {
|
|
|
|
if (!isBuffered) {
|
|
|
|
try {
|
|
|
|
_openStream();
|
|
|
|
closeNativeSocketDescriptor(rawResponse);
|
|
|
|
} catch (_) {
|
|
|
|
// This only seems to occur on `MockHttpRequest`, but
|
|
|
|
// this try/catch prevents a crash.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_buffer.lock();
|
|
|
|
}
|
|
|
|
|
|
|
|
_isClosed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
super.close();
|
|
|
|
}
|
|
|
|
return new Future.value();
|
2018-07-03 23:16:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
BytesBuilder get buffer => _buffer;
|
2018-07-03 23:16:29 +00:00
|
|
|
|
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
int detach() {
|
|
|
|
_isDetached = true;
|
|
|
|
return rawResponse;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get isBuffered => _buffer != null;
|
2018-07-03 23:16:29 +00:00
|
|
|
|
|
|
|
@override
|
2019-04-29 08:22:36 +00:00
|
|
|
bool get isOpen => !_isClosed && !_isDetached;
|
2018-07-03 23:16:29 +00:00
|
|
|
|
|
|
|
@override
|
2019-03-14 17:24:31 +00:00
|
|
|
void useBuffer() {
|
2019-04-29 08:22:36 +00:00
|
|
|
_buffer = LockableBytesBuilder();
|
2018-07-03 23:16:29 +00:00
|
|
|
}
|
2019-04-29 08:22:36 +00:00
|
|
|
}
|