blob: 151bdcabe3d099d5579b049a4009fa2e881b6d68 [file] [log] [blame]
// Copyright (c) 2025, 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.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import "package:expect/async_helper.dart";
import "package:expect/expect.dart";
class Server {
late HttpServer server;
Future<Server> start() async {
server = await HttpServer.bind(InternetAddress.loopbackIPv4.address, 0);
server.listen((request) {
final response = request.response;
// WARNING: this authenticate header is malformed because of missing
// commas between the arguments
if (request.uri.path == "/malformedAuthenticate") {
response.statusCode = HttpStatus.unauthorized;
response.headers.set(
HttpHeaders.wwwAuthenticateHeader,
"Bearer realm=\"realm\" error=\"invalid_token\"",
);
response.close();
return;
}
// NOTE: see RFC6750 section 3 regarding the authenticate response header
// field:
// https://www.rfc-editor.org/rfc/rfc6750.html#section-3
if (request.headers[HttpHeaders.authorizationHeader] != null) {
final token = base64.encode(utf8.encode(request.uri.path.substring(1)));
Expect.equals(
1,
request.headers[HttpHeaders.authorizationHeader]!.length,
);
final authorizationHeaderParts = request
.headers[HttpHeaders.authorizationHeader]![0]
.split(" ");
Expect.equals("Bearer", authorizationHeaderParts[0]);
if (token != authorizationHeaderParts[1]) {
response.statusCode = HttpStatus.unauthorized;
response.headers.set(
HttpHeaders.wwwAuthenticateHeader,
"Bearer realm=\"realm\", error=\"invalid_token\"",
);
}
} else {
response.statusCode = HttpStatus.unauthorized;
response.headers.set(
HttpHeaders.wwwAuthenticateHeader,
"Bearer realm=\"realm\"",
);
}
response.close();
});
return this;
}
Future<void> shutdown() => server.close();
String get host => server.address.address;
int get port => server.port;
}
void testCreateValidBearerTokens() {
HttpClientBearerCredentials("977ce44bc56dc5000c9d2c329e682547");
HttpClientBearerCredentials("dGVzdHRlc3R0ZXN0dGVzdA==");
HttpClientBearerCredentials("mF_9.B5f-4.1JqM");
}
void testCreateInvalidBearerTokens() {
Expect.throws(() => HttpClientBearerCredentials("#(&%)"));
Expect.throws(() => HttpClientBearerCredentials("áéîöü"));
Expect.throws(() => HttpClientBearerCredentials("あいうえお"));
Expect.throws(() => HttpClientBearerCredentials(" "));
}
Future<void> testBearerWithoutCredentials() async {
final server = await Server().start();
final client = HttpClient();
Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.unauthorized, response.statusCode);
return response.drain();
}
await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);
await server.shutdown();
client.close();
}
Future<void> testBearerWithCredentials() async {
final server = await Server().start();
final client = HttpClient();
Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
return response.drain();
}
for (int i = 0; i < 5; i++) {
final token = base64.encode(utf8.encode("test$i"));
client.addCredentials(
Uri.parse("http://${server.host}:${server.port}/test$i"),
"realm",
HttpClientBearerCredentials(token),
);
}
await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);
await server.shutdown();
client.close();
}
Future<void> testBearerWithAuthenticateCallback() async {
final server = await Server().start();
final client = HttpClient();
final callbacks = <String>{};
client.authenticate = (url, scheme, realm) async {
Expect.equals("Bearer", scheme);
Expect.equals("realm", realm);
callbacks.add(url.path.substring(1));
String token = base64.encode(utf8.encode(url.path.substring(1)));
client.addCredentials(url, realm!, HttpClientBearerCredentials(token));
return true;
};
Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
return response.drain();
}
await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);
// assert that all authenticate callbacks have actually been called
Expect.setEquals({for (int i = 0; i < 5; i++) "test$i"}, callbacks);
await server.shutdown();
client.close();
}
Future<void> testMalformedAuthenticateHeaderWithoutCredentials() async {
final server = await Server().start();
final client = HttpClient();
final uri = Uri.parse(
"http://${server.host}:${server.port}/malformedAuthenticate",
);
// the request should resolve normally if no authentication is configured
final request = await client.getUrl(uri);
await request.close();
await server.shutdown();
client.close();
}
Future<void> testMalformedAuthenticateHeaderWithCredentials() async {
final server = await Server().start();
final client = HttpClient();
final uri = Uri.parse(
"http://${server.host}:${server.port}/malformedAuthenticate",
);
final token = base64.encode(utf8.encode("test"));
// the request should throw an exception if credentials have been added
client.addCredentials(uri, "realm", HttpClientBearerCredentials(token));
await asyncExpectThrows<HttpException>(
Future(() async {
final request = await client.getUrl(uri);
await request.close();
}),
);
await server.shutdown();
client.close();
}
Future<void> testMalformedAuthenticateHeaderWithAuthenticateCallback() async {
final server = await Server().start();
final client = HttpClient();
final uri = Uri.parse(
"http://${server.host}:${server.port}/malformedAuthenticate",
);
// the request should throw an exception if the authenticate handler is set
client.authenticate = (url, scheme, realm) async => false;
await asyncExpectThrows<HttpException>(
Future(() async {
final request = await client.getUrl(uri);
await request.close();
}),
);
await server.shutdown();
client.close();
}
Future<void> testLocalServerBearer() async {
final client = HttpClient();
client.authenticate = (url, scheme, realm) async {
final token = base64.encode(utf8.encode("test"));
client.addCredentials(
Uri.parse("http://127.0.0.1/bearer"),
"test",
HttpClientBearerCredentials(token),
);
return true;
};
final request = await client.getUrl(
Uri.parse("http://127.0.0.1/bearer/test"),
);
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
await response.drain();
client.close();
}
Future<void> main() async {
asyncStart();
testCreateValidBearerTokens();
testCreateInvalidBearerTokens();
await testBearerWithoutCredentials();
await testBearerWithCredentials();
await testBearerWithAuthenticateCallback();
await testMalformedAuthenticateHeaderWithoutCredentials();
await testMalformedAuthenticateHeaderWithCredentials();
await testMalformedAuthenticateHeaderWithAuthenticateCallback();
// These tests are not normally run. They can be used for locally
// testing with another web server (e.g. Apache).
// await testLocalServerBearer();
asyncEnd();
}