2018-11-08 04:11:10 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
2019-02-03 18:00:42 +00:00
|
|
|
import 'package:angel_container/mirrors.dart';
|
2018-11-08 04:11:10 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart' hide Header;
|
|
|
|
import 'package:angel_framework/http2.dart';
|
2021-03-20 08:11:18 +00:00
|
|
|
import 'package:collection/collection.dart' show IterableExtension;
|
2018-11-08 04:11:10 +00:00
|
|
|
import 'package:http/src/multipart_file.dart' as http;
|
|
|
|
import 'package:http/src/multipart_request.dart' as http;
|
2019-10-12 14:17:46 +00:00
|
|
|
import 'package:http/io_client.dart';
|
2018-11-08 04:11:10 +00:00
|
|
|
import 'package:http2/transport.dart';
|
|
|
|
import 'package:http_parser/http_parser.dart';
|
2019-02-03 18:00:42 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2018-11-08 04:11:10 +00:00
|
|
|
import 'package:test/test.dart';
|
|
|
|
import 'http2_client.dart';
|
|
|
|
|
|
|
|
const String jfk =
|
|
|
|
'Ask not what your country can do for you, but what you can do for your country.';
|
|
|
|
|
|
|
|
Stream<List<int>> jfkStream() {
|
2019-05-02 22:48:31 +00:00
|
|
|
return Stream.fromIterable([utf8.encode(jfk)]);
|
2018-11-08 04:11:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void main() {
|
2019-05-02 22:48:31 +00:00
|
|
|
var client = Http2Client();
|
2021-03-20 08:11:18 +00:00
|
|
|
late IOClient h1c;
|
2018-11-08 04:11:10 +00:00
|
|
|
Angel app;
|
2021-03-20 08:11:18 +00:00
|
|
|
late AngelHttp2 http2;
|
|
|
|
late Uri serverRoot;
|
2018-11-08 04:11:10 +00:00
|
|
|
|
|
|
|
setUp(() async {
|
2019-05-02 22:48:31 +00:00
|
|
|
app = Angel(reflector: MirrorsReflector())..encoders['gzip'] = gzip.encoder;
|
2019-02-03 18:00:42 +00:00
|
|
|
hierarchicalLoggingEnabled = true;
|
|
|
|
app.logger = Logger.detached('angel.http2')
|
|
|
|
..onRecord.listen((rec) {
|
|
|
|
print(rec);
|
|
|
|
if (rec.error == null) return;
|
|
|
|
print(rec.error);
|
|
|
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
|
|
|
});
|
2018-11-08 04:11:10 +00:00
|
|
|
|
2018-11-11 01:07:09 +00:00
|
|
|
app.get('/', (req, res) async {
|
|
|
|
res.write('Hello world');
|
|
|
|
await res.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
app.all('/method', (req, res) => req.method);
|
|
|
|
|
|
|
|
app.get('/json', (_, __) => {'foo': 'bar'});
|
|
|
|
|
|
|
|
app.get('/stream', (req, res) => jfkStream().pipe(res));
|
|
|
|
|
2018-11-11 01:07:09 +00:00
|
|
|
app.get('/headers', (req, res) async {
|
|
|
|
res.headers.addAll({'foo': 'bar', 'x-angel': 'http2'});
|
|
|
|
await res.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
2018-11-11 01:07:09 +00:00
|
|
|
app.get('/status', (req, res) async {
|
|
|
|
res.statusCode = 1337;
|
|
|
|
await res.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
2018-12-09 15:49:59 +00:00
|
|
|
app.post('/body', (req, res) => req.parseBody().then((_) => req.bodyAsMap));
|
2018-11-08 04:11:10 +00:00
|
|
|
|
|
|
|
app.post('/upload', (req, res) async {
|
2018-12-09 15:49:59 +00:00
|
|
|
await req.parseBody();
|
2021-03-20 08:11:18 +00:00
|
|
|
var body = req.bodyAsMap;
|
|
|
|
List<UploadedFile> files = req.uploadedFiles ?? [];
|
|
|
|
|
|
|
|
UploadedFile file = files.firstWhereOrNull((f) => f.name == 'file')!;
|
2018-12-09 15:49:59 +00:00
|
|
|
return [
|
|
|
|
await file.data.map((l) => l.length).reduce((a, b) => a + b),
|
2021-03-20 08:11:18 +00:00
|
|
|
file.contentType!.mimeType,
|
2018-12-09 15:49:59 +00:00
|
|
|
body
|
|
|
|
];
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
app.get('/push', (req, res) async {
|
2018-11-11 01:07:09 +00:00
|
|
|
res.write('ok');
|
2018-11-08 04:11:10 +00:00
|
|
|
|
|
|
|
if (res is Http2ResponseContext && res.canPush) {
|
2019-04-17 19:42:24 +00:00
|
|
|
var a = res.push('a')..write('a');
|
|
|
|
await a.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
|
2019-04-17 19:42:24 +00:00
|
|
|
var b = res.push('b')..write('b');
|
|
|
|
await b.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
}
|
2018-11-11 01:07:09 +00:00
|
|
|
|
|
|
|
await res.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
2019-02-03 18:00:42 +00:00
|
|
|
app.get('/param/:name', (req, res) => req.params);
|
|
|
|
|
|
|
|
app.get('/query', (req, res) {
|
|
|
|
print('incoming URI: ${req.uri}');
|
|
|
|
return req.queryParameters;
|
|
|
|
});
|
|
|
|
|
2019-05-02 22:48:31 +00:00
|
|
|
var ctx = SecurityContext()
|
2018-11-08 04:11:10 +00:00
|
|
|
..useCertificateChain('dev.pem')
|
|
|
|
..usePrivateKey('dev.key', password: 'dartdart')
|
|
|
|
..setAlpnProtocols(['h2'], true);
|
|
|
|
|
2019-10-12 14:17:46 +00:00
|
|
|
// Create an HTTP client that trusts our server.
|
|
|
|
h1c = IOClient(HttpClient()..badCertificateCallback = (_, __, ___) => true);
|
|
|
|
|
2019-07-17 17:21:43 +00:00
|
|
|
http2 = AngelHttp2(app, ctx, allowHttp1: true);
|
2018-11-08 04:11:10 +00:00
|
|
|
|
2021-03-20 08:11:18 +00:00
|
|
|
SecureServerSocket server = await http2.startServer();
|
2018-11-08 04:11:10 +00:00
|
|
|
serverRoot = Uri.parse('https://127.0.0.1:${server.port}');
|
|
|
|
});
|
|
|
|
|
|
|
|
tearDown(() async {
|
|
|
|
await http2.close();
|
2021-03-20 08:11:18 +00:00
|
|
|
h1c.close();
|
2018-11-08 04:11:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
test('buffered response', () async {
|
|
|
|
var response = await client.get(serverRoot);
|
|
|
|
expect(response.body, 'Hello world');
|
|
|
|
});
|
|
|
|
|
2019-07-17 17:21:43 +00:00
|
|
|
test('allowHttp1', () async {
|
|
|
|
var response = await h1c.get(serverRoot);
|
|
|
|
expect(response.body, 'Hello world');
|
|
|
|
});
|
|
|
|
|
2018-11-08 04:11:10 +00:00
|
|
|
test('streamed response', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/stream'));
|
|
|
|
expect(response.body, jfk);
|
|
|
|
});
|
|
|
|
|
|
|
|
group('gzip', () {
|
|
|
|
test('buffered response', () async {
|
|
|
|
var response = await client
|
|
|
|
.get(serverRoot, headers: {'accept-encoding': 'gzip, deflate, br'});
|
|
|
|
expect(response.headers['content-encoding'], 'gzip');
|
|
|
|
var decoded = gzip.decode(response.bodyBytes);
|
|
|
|
expect(utf8.decode(decoded), 'Hello world');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('streamed response', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/stream'),
|
|
|
|
headers: {'accept-encoding': 'gzip'});
|
|
|
|
expect(response.headers['content-encoding'], 'gzip');
|
|
|
|
//print(response.body);
|
|
|
|
var decoded = gzip.decode(response.bodyBytes);
|
|
|
|
expect(utf8.decode(decoded), jfk);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-02-03 18:00:42 +00:00
|
|
|
test('query uri decoded', () async {
|
|
|
|
var uri =
|
|
|
|
serverRoot.replace(path: '/query', queryParameters: {'foo!': 'bar?'});
|
|
|
|
var response = await client.get(uri);
|
|
|
|
print('Sent $uri');
|
|
|
|
expect(response.body, json.encode({'foo!': 'bar?'}));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('params uri decoded', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/param/foo!'));
|
|
|
|
expect(response.body, json.encode({'name': 'foo!'}));
|
|
|
|
});
|
|
|
|
|
2018-11-08 04:11:10 +00:00
|
|
|
test('method parsed', () async {
|
|
|
|
var response = await client.delete(serverRoot.replace(path: '/method'));
|
|
|
|
expect(response.body, json.encode('DELETE'));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('json response', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/json'));
|
|
|
|
expect(response.body, json.encode({'foo': 'bar'}));
|
2021-03-20 08:11:18 +00:00
|
|
|
expect(ContentType.parse(response.headers['content-type']!).mimeType,
|
2018-11-08 04:11:10 +00:00
|
|
|
ContentType.json.mimeType);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('status sent', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/status'));
|
|
|
|
expect(response.statusCode, 1337);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('headers sent', () async {
|
|
|
|
var response = await client.get(serverRoot.replace(path: '/headers'));
|
|
|
|
expect(response.headers['foo'], 'bar');
|
|
|
|
expect(response.headers['x-angel'], 'http2');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('server push', () async {
|
|
|
|
var socket = await SecureSocket.connect(
|
|
|
|
serverRoot.host,
|
|
|
|
serverRoot.port ?? 443,
|
|
|
|
onBadCertificate: (_) => true,
|
|
|
|
supportedProtocols: ['h2'],
|
|
|
|
);
|
|
|
|
|
2019-05-02 22:48:31 +00:00
|
|
|
var connection = ClientTransportConnection.viaSocket(
|
2018-11-08 04:11:10 +00:00
|
|
|
socket,
|
2019-05-02 22:48:31 +00:00
|
|
|
settings: ClientSettings(allowServerPushes: true),
|
2018-11-08 04:11:10 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
var headers = <Header>[
|
2019-05-02 22:48:31 +00:00
|
|
|
Header.ascii(':authority', serverRoot.authority),
|
|
|
|
Header.ascii(':method', 'GET'),
|
|
|
|
Header.ascii(':path', serverRoot.replace(path: '/push').path),
|
|
|
|
Header.ascii(':scheme', serverRoot.scheme),
|
2018-11-08 04:11:10 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
var stream = await connection.makeRequest(headers, endStream: true);
|
|
|
|
|
|
|
|
var bb = await stream.incomingMessages
|
|
|
|
.where((s) => s is DataStreamMessage)
|
|
|
|
.cast<DataStreamMessage>()
|
2019-05-02 22:48:31 +00:00
|
|
|
.fold<BytesBuilder>(BytesBuilder(), (out, msg) => out..add(msg.bytes));
|
2018-11-08 04:11:10 +00:00
|
|
|
|
|
|
|
// Check that main body was sent
|
|
|
|
expect(utf8.decode(bb.takeBytes()), 'ok');
|
|
|
|
|
|
|
|
var pushes = await stream.peerPushes.toList();
|
|
|
|
expect(pushes, hasLength(2));
|
|
|
|
|
|
|
|
var pushA = pushes[0], pushB = pushes[1];
|
|
|
|
|
|
|
|
String getPath(TransportStreamPush p) => ascii.decode(p.requestHeaders
|
|
|
|
.firstWhere((h) => ascii.decode(h.name) == ':path')
|
|
|
|
.value);
|
|
|
|
|
|
|
|
/*
|
|
|
|
Future<String> getBody(ClientTransportStream stream) async {
|
|
|
|
await stream.outgoingMessages.close();
|
|
|
|
var bb = await stream.incomingMessages
|
|
|
|
.map((s) {
|
|
|
|
if (s is HeadersStreamMessage) {
|
|
|
|
for (var h in s.headers) {
|
|
|
|
print('${ASCII.decode(h.name)}: ${ASCII.decode(h.value)}');
|
|
|
|
}
|
|
|
|
} else if (s is DataStreamMessage) {
|
|
|
|
print(UTF8.decode(s.bytes));
|
|
|
|
}
|
|
|
|
|
|
|
|
return s;
|
|
|
|
})
|
|
|
|
.where((s) => s is DataStreamMessage)
|
|
|
|
.cast<DataStreamMessage>()
|
|
|
|
.fold<BytesBuilder>(
|
2019-05-02 22:48:31 +00:00
|
|
|
BytesBuilder(), (out, msg) => out..add(msg.bytes));
|
2018-11-08 04:11:10 +00:00
|
|
|
return UTF8.decode(bb.takeBytes());
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
expect(getPath(pushA), '/a');
|
|
|
|
expect(getPath(pushB), '/b');
|
|
|
|
|
|
|
|
// However, Chrome, Firefox, Edge all can
|
|
|
|
//expect(await getBody(pushA.stream), 'a');
|
|
|
|
//expect(await getBody(pushB.stream), 'b');
|
|
|
|
});
|
|
|
|
|
|
|
|
group('body parsing', () {
|
|
|
|
test('urlencoded body parsed', () async {
|
|
|
|
var response = await client.post(
|
|
|
|
serverRoot.replace(path: '/body'),
|
|
|
|
headers: {
|
|
|
|
'accept': 'application/json',
|
|
|
|
'content-type': 'application/x-www-form-urlencoded'
|
|
|
|
},
|
|
|
|
body: 'foo=bar',
|
|
|
|
);
|
|
|
|
expect(response.body, json.encode({'foo': 'bar'}));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('json body parsed', () async {
|
|
|
|
var response = await client.post(serverRoot.replace(path: '/body'),
|
|
|
|
headers: {
|
|
|
|
'accept': 'application/json',
|
|
|
|
'content-type': 'application/json'
|
|
|
|
},
|
|
|
|
body: json.encode({'foo': 'bar'}));
|
|
|
|
expect(response.body, json.encode({'foo': 'bar'}));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('multipart body parsed', () async {
|
2019-05-02 22:48:31 +00:00
|
|
|
var rq =
|
|
|
|
http.MultipartRequest('POST', serverRoot.replace(path: '/upload'));
|
2018-11-08 04:11:10 +00:00
|
|
|
rq.headers.addAll({'accept': 'application/json'});
|
|
|
|
|
|
|
|
rq.fields['foo'] = 'bar';
|
2019-05-02 22:48:31 +00:00
|
|
|
rq.files.add(http.MultipartFile(
|
|
|
|
'file', Stream.fromIterable([utf8.encode('hello world')]), 11,
|
|
|
|
contentType: MediaType('angel', 'framework')));
|
2018-11-08 04:11:10 +00:00
|
|
|
|
|
|
|
var response = await client.send(rq);
|
|
|
|
var responseBody = await response.stream.transform(utf8.decoder).join();
|
|
|
|
|
|
|
|
expect(
|
|
|
|
responseBody,
|
|
|
|
json.encode([
|
|
|
|
11,
|
|
|
|
'angel/framework',
|
|
|
|
{'foo': 'bar'}
|
|
|
|
]));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|