blob: d6b437eb732b6e96d151732ce738b42e15b5921c [file] [log] [blame]
// Copyright (c) 2017, 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.
part of sync.http;
/// A simple synchronous HTTP client.
///
/// This is a two-step process. When a [SyncHttpClientRequest] is returned the
/// underlying network connection has been established, but no data has yet been
/// sent. The HTTP headers and body can be set on the request, and close is
/// called to send it to the server and get the [SyncHttpClientResponse].
abstract class SyncHttpClient {
/// Send a GET request to the provided URL.
static SyncHttpClientRequest getUrl(Uri uri) =>
SyncHttpClientRequest._('GET', uri, false);
/// Send a POST request to the provided URL.
static SyncHttpClientRequest postUrl(Uri uri) =>
SyncHttpClientRequest._('POST', uri, true);
/// Send a DELETE request to the provided URL.
static SyncHttpClientRequest deleteUrl(Uri uri) =>
SyncHttpClientRequest._('DELETE', uri, false);
/// Send a PUT request to the provided URL.
static SyncHttpClientRequest putUrl(Uri uri) =>
SyncHttpClientRequest._('PUT', uri, true);
}
/// HTTP request for a synchronous client connection.
class SyncHttpClientRequest {
static const String _protocolVersion = '1.1';
/// The length of the request body. Is set to `-1` when no body exists.
int get contentLength => hasBody ? _body!.length : -1;
HttpHeaders? _headers;
/// The headers associated with the HTTP request.
HttpHeaders get headers => _headers ??= _SyncHttpClientRequestHeaders(this);
/// The type of HTTP request being made.
final String method;
/// The Uri the HTTP request will be sent to.
final Uri uri;
/// The default encoding for the HTTP request (UTF8).
final Encoding encoding = utf8;
/// The body of the HTTP request. This can be empty if there is no body
/// associated with the request.
final BytesBuilder? _body;
/// The synchronous socket used to initiate the HTTP request.
final RawSynchronousSocket _socket;
SyncHttpClientRequest._(this.method, Uri uri, bool body)
: uri = uri,
_body = body ? BytesBuilder() : null,
_socket = RawSynchronousSocket.connectSync(uri.host, uri.port);
/// Write content into the body of the HTTP request.
void write(Object? obj) {
if (hasBody) {
if (obj != null) {
_body!.add(encoding.encoder.convert(obj.toString()));
}
} else {
throw StateError('write not allowed for method $method');
}
}
/// Specifies whether or not the HTTP request has a body.
bool get hasBody => _body != null;
/// Send the HTTP request and get the response.
SyncHttpClientResponse close() {
var queryString = '';
if (uri.hasQuery) {
var query = StringBuffer();
query.write('?');
uri.queryParameters.forEach((k, v) {
query.write(Uri.encodeComponent(k));
query.write('=');
query.write(Uri.encodeComponent(v));
query.write('&');
});
queryString = query.toString().substring(0, query.length - 1);
}
var buffer = StringBuffer();
buffer.write('$method ${uri.path}$queryString HTTP/$_protocolVersion\r\n');
headers.forEach((name, values) {
values.forEach((value) {
buffer.write('$name: $value\r\n');
});
});
buffer.write('\r\n');
if (hasBody) {
buffer.write(String.fromCharCodes(_body!.takeBytes()));
}
_socket.writeFromSync(buffer.toString().codeUnits);
return SyncHttpClientResponse(_socket);
}
}
class _SyncHttpClientRequestHeaders implements HttpHeaders {
final Map<String, List<String>> _headers = <String, List<String>>{};
final SyncHttpClientRequest _request;
@override
ContentType? contentType;
_SyncHttpClientRequestHeaders(this._request);
@override
List<String>? operator [](String name) {
switch (name) {
case HttpHeaders.acceptCharsetHeader:
return ['utf-8'];
case HttpHeaders.acceptEncodingHeader:
return ['identity'];
case HttpHeaders.connectionHeader:
return ['close'];
case HttpHeaders.contentLengthHeader:
if (!_request.hasBody) {
return null;
}
return [contentLength.toString()];
case HttpHeaders.contentTypeHeader:
if (contentType == null) {
return null;
}
return [contentType.toString()];
case HttpHeaders.hostHeader:
return ['$host:$port'];
default:
var values = _headers[name];
if (values == null || values.isEmpty) {
return null;
}
return values.map<String>((e) => e.toString()).toList(growable: false);
}
}
/// Add [value] to the list of values associated with header [name].
@override
void add(String name, Object value, {bool preserveHeaderCase = false}) {
switch (name) {
case HttpHeaders.acceptCharsetHeader:
case HttpHeaders.acceptEncodingHeader:
case HttpHeaders.connectionHeader:
case HttpHeaders.contentLengthHeader:
case HttpHeaders.dateHeader:
case HttpHeaders.expiresHeader:
case HttpHeaders.ifModifiedSinceHeader:
case HttpHeaders.hostHeader:
throw UnsupportedError('Unsupported or immutable property: $name');
case HttpHeaders.contentTypeHeader:
contentType = value as ContentType?;
break;
default:
if (_headers[name] == null) {
_headers[name] = <String>[];
}
_headers[name]!.add(value as String);
}
}
/// Remove [value] from the list associated with header [name].
@override
void remove(String name, Object value) {
switch (name) {
case HttpHeaders.acceptCharsetHeader:
case HttpHeaders.acceptEncodingHeader:
case HttpHeaders.connectionHeader:
case HttpHeaders.contentLengthHeader:
case HttpHeaders.dateHeader:
case HttpHeaders.expiresHeader:
case HttpHeaders.ifModifiedSinceHeader:
case HttpHeaders.hostHeader:
throw UnsupportedError('Unsupported or immutable property: $name');
case HttpHeaders.contentTypeHeader:
if (contentType == value) {
contentType = null;
}
break;
default:
if (_headers[name] != null) {
_headers[name]!.remove(value);
if (_headers[name]!.isEmpty) {
_headers.remove(name);
}
}
}
}
/// Remove all headers associated with key [name].
@override
void removeAll(String name) {
switch (name) {
case HttpHeaders.acceptCharsetHeader:
case HttpHeaders.acceptEncodingHeader:
case HttpHeaders.connectionHeader:
case HttpHeaders.contentLengthHeader:
case HttpHeaders.dateHeader:
case HttpHeaders.expiresHeader:
case HttpHeaders.ifModifiedSinceHeader:
case HttpHeaders.hostHeader:
throw UnsupportedError('Unsupported or immutable property: $name');
case HttpHeaders.contentTypeHeader:
contentType = null;
break;
default:
_headers.remove(name);
}
}
/// Replace values associated with key [name] with [value].
@override
void set(String name, Object value, {bool preserveHeaderCase = false}) {
removeAll(name);
add(name, value, preserveHeaderCase: preserveHeaderCase);
}
/// Returns the values associated with key [name], if it exists, otherwise
/// returns null.
@override
String? value(String name) {
var val = this[name];
if (val == null || val.isEmpty) {
return null;
} else if (val.length == 1) {
return val[0];
} else {
throw HttpException('header $name has more than one value');
}
}
/// Iterates over all header key-value pairs and applies [f].
@override
void forEach(void Function(String name, List<String> values) f) {
var forEachFunc = (String name) {
var values = this[name];
if (values != null && values.isNotEmpty) {
f(name, values);
}
};
[
HttpHeaders.acceptCharsetHeader,
HttpHeaders.acceptEncodingHeader,
HttpHeaders.connectionHeader,
HttpHeaders.contentLengthHeader,
HttpHeaders.contentTypeHeader,
HttpHeaders.hostHeader
].forEach(forEachFunc);
_headers.keys.forEach(forEachFunc);
}
@override
bool get chunkedTransferEncoding =>
value(HttpHeaders.transferEncodingHeader)?.toLowerCase() == 'chunked';
@override
set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
throw UnsupportedError('chunked transfer is unsupported');
}
@override
int get contentLength => _request.contentLength;
@override
set contentLength(int _contentLength) {
throw UnsupportedError('content length is automatically set');
}
@override
set date(DateTime? _date) {
throw UnsupportedError('date is unsupported');
}
@override
DateTime? get date => null;
@override
set expires(DateTime? _expires) {
throw UnsupportedError('expires is unsupported');
}
@override
DateTime? get expires => null;
@override
set host(String? _host) {
throw UnsupportedError('host is automatically set');
}
@override
String get host => _request.uri.host;
@override
DateTime? get ifModifiedSince => null;
@override
set ifModifiedSince(DateTime? _ifModifiedSince) {
throw UnsupportedError('if modified since is unsupported');
}
@override
void noFolding(String name) {
throw UnsupportedError('no folding is unsupported');
}
@override
bool get persistentConnection => false;
@override
set persistentConnection(bool _persistentConnection) {
throw UnsupportedError('persistence connections are unsupported');
}
@override
set port(int? _port) {
throw UnsupportedError('port is automatically set');
}
@override
int get port => _request.uri.port;
/// Clear all header key-value pairs.
@override
void clear() {
contentType = null;
_headers.clear();
}
}
/// HTTP response for a client connection.
class SyncHttpClientResponse {
/// The length of the body associated with the HTTP response.
int get contentLength => headers.contentLength;
/// The headers associated with the HTTP response.
final HttpHeaders headers;
/// A short textual description of the status code associated with the HTTP
/// response.
final String? reasonPhrase;
/// The resulting HTTP status code associated with the HTTP response.
final int? statusCode;
/// The body of the HTTP response.
final String? body;
/// Creates an instance of [SyncHttpClientResponse] that contains the response
/// sent by the HTTP server over [socket].
factory SyncHttpClientResponse(RawSynchronousSocket socket) {
int? statusCode;
String? reasonPhrase;
var body = StringBuffer();
var headers = <String, List<String>>{};
var inHeader = false;
var inBody = false;
var contentLength = 0;
var contentRead = 0;
void processLine(String line, int bytesRead, _LineDecoder decoder) {
if (inBody) {
body.write(line);
contentRead += bytesRead;
} else if (inHeader) {
if (line.trim().isEmpty) {
inBody = true;
if (contentLength > 0) {
decoder.expectedByteCount = contentLength;
}
return;
}
var separator = line.indexOf(':');
var name = line.substring(0, separator).toLowerCase().trim();
var value = line.substring(separator + 1).trim();
if (name == HttpHeaders.transferEncodingHeader &&
value.toLowerCase() != 'identity') {
throw UnsupportedError('only identity transfer encoding is accepted');
}
if (name == HttpHeaders.contentLengthHeader) {
contentLength = int.parse(value);
}
if (!headers.containsKey(name)) {
headers[name] = [];
}
headers[name]!.add(value);
} else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) {
statusCode = int.parse(
line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length));
reasonPhrase = line.substring('HTTP/1.x xxx '.length);
inHeader = true;
} else {
throw UnsupportedError('unsupported http response format');
}
}
var lineDecoder = _LineDecoder.withCallback(processLine);
try {
while (!inHeader ||
!inBody ||
((contentRead + lineDecoder.bufferedBytes) < contentLength)) {
var bytes = socket.readSync(1024);
if (bytes == null || bytes.isEmpty) {
break;
}
lineDecoder.add(bytes);
}
} finally {
try {
lineDecoder.close();
} finally {
socket.closeSync();
}
}
return SyncHttpClientResponse._(headers,
reasonPhrase: reasonPhrase,
statusCode: statusCode,
body: body.toString());
}
SyncHttpClientResponse._(Map<String, List<String>> headers,
{this.reasonPhrase, this.statusCode, this.body})
: headers = _SyncHttpClientResponseHeaders(headers);
}
class _SyncHttpClientResponseHeaders implements HttpHeaders {
final Map<String, List<String>> _headers;
_SyncHttpClientResponseHeaders(this._headers);
@override
List<String>? operator [](String name) => _headers[name];
@override
void add(String name, Object value, {bool preserveHeaderCase = false}) {
throw UnsupportedError('Response headers are immutable');
}
@override
bool get chunkedTransferEncoding =>
value(HttpHeaders.transferEncodingHeader)?.toLowerCase() == 'chunked';
@override
set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
throw UnsupportedError('Response headers are immutable');
}
@override
int get contentLength {
var val = value(HttpHeaders.contentLengthHeader);
if (val != null) {
var parsed = int.tryParse(val);
if (parsed != null) {
return parsed;
}
}
return -1;
}
@override
set contentLength(int _contentLength) {
throw UnsupportedError('Response headers are immutable');
}
@override
ContentType? get contentType {
var val = value(HttpHeaders.contentTypeHeader);
if (val != null) {
return ContentType.parse(val);
}
return null;
}
@override
set contentType(ContentType? _contentType) {
throw UnsupportedError('Response headers are immutable');
}
@override
set date(DateTime? _date) {
throw UnsupportedError('Response headers are immutable');
}
@override
DateTime? get date {
var val = value(HttpHeaders.dateHeader);
if (val != null) {
return DateTime.parse(val);
}
return null;
}
@override
set expires(DateTime? _expires) {
throw UnsupportedError('Response headers are immutable');
}
@override
DateTime? get expires {
var val = value(HttpHeaders.expiresHeader);
if (val != null) {
return DateTime.parse(val);
}
return null;
}
@override
void forEach(void Function(String name, List<String> values) f) =>
_headers.forEach(f);
@override
set host(String? _host) {
throw UnsupportedError('Response headers are immutable');
}
@override
String? get host {
var val = value(HttpHeaders.hostHeader);
if (val != null) {
return Uri.parse(val).host;
}
return null;
}
@override
DateTime? get ifModifiedSince {
var val = value(HttpHeaders.ifModifiedSinceHeader);
if (val != null) {
return DateTime.parse(val);
}
return null;
}
@override
set ifModifiedSince(DateTime? _ifModifiedSince) {
throw UnsupportedError('Response headers are immutable');
}
@override
void noFolding(String name) {
throw UnsupportedError('Response headers are immutable');
}
@override
bool get persistentConnection => false;
@override
set persistentConnection(bool _persistentConnection) {
throw UnsupportedError('Response headers are immutable');
}
@override
set port(int? _port) {
throw UnsupportedError('Response headers are immutable');
}
@override
int? get port {
var val = value(HttpHeaders.hostHeader);
if (val != null) {
return Uri.parse(val).port;
}
return null;
}
@override
void remove(String name, Object value) {
throw UnsupportedError('Response headers are immutable');
}
@override
void removeAll(String name) {
throw UnsupportedError('Response headers are immutable');
}
@override
void set(String name, Object value, {bool preserveHeaderCase = false}) {
throw UnsupportedError('Response headers are immutable');
}
@override
String? value(String name) {
var val = this[name];
if (val == null || val.isEmpty) {
return null;
} else if (val.length == 1) {
return val[0];
} else {
throw HttpException('header $name has more than one value');
}
}
@override
void clear() {
throw UnsupportedError('Response headers are immutable');
}
}