33dc2dd6e4
Also remove duplicate call to _connectionEstablished. BUG= TEST= Review URL: https://chromiumcodereview.appspot.com//10356069 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7448 260f80e4-7a28-3924-810f-c04153c831b5
1657 lines
47 KiB
Dart
1657 lines
47 KiB
Dart
// Copyright (c) 2012, 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.
|
|
|
|
class _HttpHeaders implements HttpHeaders {
|
|
_HttpHeaders() : _headers = new Map<String, List<String>>();
|
|
|
|
List<String> operator[](String name) {
|
|
name = name.toLowerCase();
|
|
return _headers[name];
|
|
}
|
|
|
|
String value(String name) {
|
|
name = name.toLowerCase();
|
|
List<String> values = _headers[name];
|
|
if (values == null) return null;
|
|
if (values.length > 1) {
|
|
throw new HttpException("More than one value for header $name");
|
|
}
|
|
return values[0];
|
|
}
|
|
|
|
void add(String name, Object value) {
|
|
_checkMutable();
|
|
if (value is List) {
|
|
for (int i = 0; i < value.length; i++) {
|
|
_add(name, value[i]);
|
|
}
|
|
} else {
|
|
_add(name, value);
|
|
}
|
|
}
|
|
|
|
void set(String name, Object value) {
|
|
_checkMutable();
|
|
removeAll(name);
|
|
add(name, value);
|
|
}
|
|
|
|
void remove(String name, Object value) {
|
|
_checkMutable();
|
|
name = name.toLowerCase();
|
|
List<String> values = _headers[name];
|
|
if (values != null) {
|
|
int index = values.indexOf(value);
|
|
if (index != -1) {
|
|
values.removeRange(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeAll(String name) {
|
|
_checkMutable();
|
|
name = name.toLowerCase();
|
|
_headers.remove(name);
|
|
}
|
|
|
|
void forEach(void f(String name, List<String> values)) {
|
|
_headers.forEach(f);
|
|
}
|
|
|
|
String get host() => _host;
|
|
|
|
void set host(String host) {
|
|
_checkMutable();
|
|
_host = host;
|
|
_updateHostHeader();
|
|
}
|
|
|
|
int get port() => _port;
|
|
|
|
void set port(int port) {
|
|
_checkMutable();
|
|
_port = port;
|
|
_updateHostHeader();
|
|
}
|
|
|
|
Date get date() {
|
|
List<String> values = _headers["date"];
|
|
if (values != null) {
|
|
try {
|
|
return _HttpUtils.parseDate(values[0]);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void set date(Date date) {
|
|
_checkMutable();
|
|
// Format "Date" header with date in Greenwich Mean Time (GMT).
|
|
String formatted =
|
|
_HttpUtils.formatDate(expires.changeTimeZone(new TimeZone.utc()));
|
|
_set("date", formatted);
|
|
}
|
|
|
|
Date get expires() {
|
|
List<String> values = _headers["expires"];
|
|
if (values != null) {
|
|
try {
|
|
return _HttpUtils.parseDate(values[0]);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void set expires(Date expires) {
|
|
_checkMutable();
|
|
// Format "Expires" header with date in Greenwich Mean Time (GMT).
|
|
String formatted =
|
|
_HttpUtils.formatDate(expires.changeTimeZone(new TimeZone.utc()));
|
|
_set("expires", formatted);
|
|
}
|
|
|
|
void _add(String name, Object value) {
|
|
// TODO(sgjesse): Add immutable state throw HttpException is immutable.
|
|
if (name.toLowerCase() == "date") {
|
|
if (value is Date) {
|
|
date = value;
|
|
} else if (value is String) {
|
|
_set("date", value);
|
|
} else {
|
|
throw new HttpException("Unexpected type for header named $name");
|
|
}
|
|
} else if (name.toLowerCase() == "expires") {
|
|
if (value is Date) {
|
|
expires = value;
|
|
} else if (value is String) {
|
|
_set("expires", value);
|
|
} else {
|
|
throw new HttpException("Unexpected type for header named $name");
|
|
}
|
|
} else if (name.toLowerCase() == "host") {
|
|
int pos = value.indexOf(":");
|
|
if (pos == -1) {
|
|
_host = value;
|
|
_port = HttpClient.DEFAULT_HTTP_PORT;
|
|
} else {
|
|
if (pos > 0) {
|
|
_host = value.substring(0, pos);
|
|
} else {
|
|
_host = null;
|
|
}
|
|
if (pos + 1 == value.length) {
|
|
_port = HttpClient.DEFAULT_HTTP_PORT;
|
|
} else {
|
|
try {
|
|
_port = Math.parseInt(value.substring(pos + 1));
|
|
} catch (BadNumberFormatException e) {
|
|
_port = null;
|
|
}
|
|
}
|
|
_set("host", value);
|
|
}
|
|
} else {
|
|
name = name.toLowerCase();
|
|
List<String> values = _headers[name];
|
|
if (values == null) {
|
|
values = new List<String>();
|
|
_headers[name] = values;
|
|
}
|
|
values.add(value.toString());
|
|
}
|
|
}
|
|
|
|
void _set(String name, String value) {
|
|
name = name.toLowerCase();
|
|
List<String> values = new List<String>();
|
|
_headers[name] = values;
|
|
values.add(value);
|
|
}
|
|
|
|
_checkMutable() {
|
|
if (!_mutable) throw new HttpException("HTTP headers are not mutable");
|
|
}
|
|
|
|
_updateHostHeader() {
|
|
bool defaultPort = _port == null || _port == HttpClient.DEFAULT_HTTP_PORT;
|
|
String portPart = defaultPort ? "" : ":$_port";
|
|
_set("host", "$host$portPart");
|
|
}
|
|
|
|
_write(_HttpConnectionBase connection) {
|
|
final COLONSP = const [_CharCode.COLON, _CharCode.SP];
|
|
final COMMASP = const [_CharCode.COMMA, _CharCode.SP];
|
|
final CRLF = const [_CharCode.CR, _CharCode.LF];
|
|
|
|
// Format headers.
|
|
_headers.forEach((String name, List<String> values) {
|
|
List<int> data;
|
|
data = name.charCodes();
|
|
connection._write(data);
|
|
connection._write(COLONSP);
|
|
for (int i = 0; i < values.length; i++) {
|
|
if (i > 0) {
|
|
connection._write(COMMASP);
|
|
}
|
|
data = values[i].charCodes();
|
|
connection._write(data);
|
|
}
|
|
connection._write(CRLF);
|
|
});
|
|
}
|
|
|
|
String toString() {
|
|
StringBuffer sb = new StringBuffer();
|
|
_headers.forEach((String name, List<String> values) {
|
|
sb.add(name);
|
|
sb.add(": ");
|
|
for (int i = 0; i < values.length; i++) {
|
|
if (i > 0) {
|
|
sb.add(", ");
|
|
}
|
|
sb.add(values[i]);
|
|
}
|
|
sb.add("\n");
|
|
});
|
|
return sb.toString();
|
|
}
|
|
|
|
bool _mutable = true; // Are the headers currently mutable?
|
|
Map<String, List<String>> _headers;
|
|
|
|
String _host;
|
|
int _port;
|
|
}
|
|
|
|
|
|
class _HttpRequestResponseBase {
|
|
final int START = 0;
|
|
final int HEADER_SENT = 1;
|
|
final int DONE = 2;
|
|
final int UPGRADED = 3;
|
|
|
|
_HttpRequestResponseBase(_HttpConnectionBase this._httpConnection)
|
|
: _headers = new _HttpHeaders() {
|
|
_state = START;
|
|
}
|
|
|
|
int get contentLength() => _contentLength;
|
|
HttpHeaders get headers() => _headers;
|
|
|
|
bool get persistentConnection() {
|
|
List<String> connection = headers[HttpHeaders.CONNECTION];
|
|
if (_protocolVersion == "1.1") {
|
|
if (connection == null) return true;
|
|
return !headers[HttpHeaders.CONNECTION].some(
|
|
(value) => value.toLowerCase() == "close");
|
|
} else {
|
|
if (connection == null) return false;
|
|
return headers[HttpHeaders.CONNECTION].some(
|
|
(value) => value.toLowerCase() == "keep-alive");
|
|
}
|
|
}
|
|
|
|
void set persistentConnection(bool persistentConnection) {
|
|
if (_outputStream != null) throw new HttpException("Header already sent");
|
|
|
|
// Determine the value of the "Connection" header.
|
|
headers.remove(HttpHeaders.CONNECTION, "close");
|
|
headers.remove(HttpHeaders.CONNECTION, "keep-alive");
|
|
if (_protocolVersion == "1.1" && !persistentConnection) {
|
|
headers.add(HttpHeaders.CONNECTION, "close");
|
|
} else if (_protocolVersion == "1.0" && persistentConnection) {
|
|
headers.add(HttpHeaders.CONNECTION, "keep-alive");
|
|
}
|
|
}
|
|
|
|
|
|
bool _write(List<int> data, bool copyBuffer) {
|
|
_ensureHeadersSent();
|
|
bool allWritten = true;
|
|
if (data.length > 0) {
|
|
if (_contentLength < 0) {
|
|
// Write chunk size if transfer encoding is chunked.
|
|
_writeHexString(data.length);
|
|
_writeCRLF();
|
|
_httpConnection._write(data, copyBuffer);
|
|
allWritten = _writeCRLF();
|
|
} else {
|
|
_updateContentLength(data.length);
|
|
allWritten = _httpConnection._write(data, copyBuffer);
|
|
}
|
|
}
|
|
return allWritten;
|
|
}
|
|
|
|
bool _writeList(List<int> data, int offset, int count) {
|
|
_ensureHeadersSent();
|
|
bool allWritten = true;
|
|
if (count > 0) {
|
|
if (_contentLength < 0) {
|
|
// Write chunk size if transfer encoding is chunked.
|
|
_writeHexString(count);
|
|
_writeCRLF();
|
|
_httpConnection._writeFrom(data, offset, count);
|
|
allWritten = _writeCRLF();
|
|
} else {
|
|
_updateContentLength(count);
|
|
allWritten = _httpConnection._writeFrom(data, offset, count);
|
|
}
|
|
}
|
|
return allWritten;
|
|
}
|
|
|
|
bool _writeDone() {
|
|
bool allWritten = true;
|
|
if (_contentLength < 0) {
|
|
// Terminate the content if transfer encoding is chunked.
|
|
allWritten = _httpConnection._write(_Const.END_CHUNKED);
|
|
} else {
|
|
if (_bodyBytesWritten < _contentLength) {
|
|
throw new HttpException("Sending less than specified content length");
|
|
}
|
|
assert(_bodyBytesWritten == _contentLength);
|
|
}
|
|
if (!persistentConnection) _httpConnection._close();
|
|
return allWritten;
|
|
}
|
|
|
|
bool _writeHeaders() {
|
|
_headers._mutable = false;
|
|
_headers._write(_httpConnection);
|
|
// Terminate header.
|
|
return _writeCRLF();
|
|
}
|
|
|
|
bool _writeHexString(int x) {
|
|
final List<int> hexDigits = [0x30, 0x31, 0x32, 0x33, 0x34,
|
|
0x35, 0x36, 0x37, 0x38, 0x39,
|
|
0x41, 0x42, 0x43, 0x44, 0x45, 0x46];
|
|
List<int> hex = new Uint8List(10);
|
|
int index = hex.length;
|
|
while (x > 0) {
|
|
index--;
|
|
hex[index] = hexDigits[x % 16];
|
|
x = x >> 4;
|
|
}
|
|
return _httpConnection._writeFrom(hex, index, hex.length - index);
|
|
}
|
|
|
|
bool _writeCRLF() {
|
|
final CRLF = const [_CharCode.CR, _CharCode.LF];
|
|
return _httpConnection._write(CRLF);
|
|
}
|
|
|
|
bool _writeSP() {
|
|
final SP = const [_CharCode.SP];
|
|
return _httpConnection._write(SP);
|
|
}
|
|
|
|
void _ensureHeadersSent() {
|
|
// Ensure that headers are written.
|
|
if (_state == START) {
|
|
_writeHeader();
|
|
}
|
|
}
|
|
|
|
void _updateContentLength(int bytes) {
|
|
if (_bodyBytesWritten + bytes > _contentLength) {
|
|
throw new HttpException("Writing more than specified content length");
|
|
}
|
|
_bodyBytesWritten += bytes;
|
|
}
|
|
|
|
int _state;
|
|
|
|
_HttpConnectionBase _httpConnection;
|
|
_HttpHeaders _headers;
|
|
String _protocolVersion = "1.1";
|
|
|
|
// Length of the content body. If this is set to -1 (default value)
|
|
// when starting to send data chunked transfer encoding will be
|
|
// used.
|
|
int _contentLength = -1;
|
|
// Number of body bytes written. This is only actual body data not
|
|
// including headers or chunk information of using chinked transfer
|
|
// encoding.
|
|
int _bodyBytesWritten = 0;
|
|
}
|
|
|
|
|
|
// Parsed HTTP request providing information on the HTTP headers.
|
|
class _HttpRequest extends _HttpRequestResponseBase implements HttpRequest {
|
|
_HttpRequest(_HttpConnection connection) : super(connection);
|
|
|
|
String get method() => _method;
|
|
String get uri() => _uri;
|
|
String get path() => _path;
|
|
String get queryString() => _queryString;
|
|
Map get queryParameters() => _queryParameters;
|
|
|
|
InputStream get inputStream() {
|
|
if (_inputStream == null) {
|
|
_inputStream = new _HttpInputStream(this);
|
|
}
|
|
return _inputStream;
|
|
}
|
|
|
|
String get protocolVersion() => _protocolVersion;
|
|
|
|
void _onRequestStart(String method, String uri, String version) {
|
|
_method = method;
|
|
_uri = uri;
|
|
_parseRequestUri(uri);
|
|
}
|
|
|
|
void _onHeaderReceived(String name, String value) {
|
|
_headers.add(name, value);
|
|
}
|
|
|
|
void _onHeadersComplete() {
|
|
_headers._mutable = false;
|
|
// Prepare for receiving data.
|
|
_buffer = new _BufferList();
|
|
}
|
|
|
|
void _onDataReceived(List<int> data) {
|
|
_buffer.add(data);
|
|
if (_inputStream != null) _inputStream._dataReceived();
|
|
}
|
|
|
|
void _onDataEnd() {
|
|
if (_inputStream != null) _inputStream._closeReceived();
|
|
}
|
|
|
|
// Escaped characters in uri are expected to have been parsed.
|
|
void _parseRequestUri(String uri) {
|
|
int position;
|
|
position = uri.indexOf("?", 0);
|
|
if (position == -1) {
|
|
_path = _HttpUtils.decodeUrlEncodedString(_uri);
|
|
_queryString = null;
|
|
_queryParameters = new Map();
|
|
} else {
|
|
_path = _HttpUtils.decodeUrlEncodedString(_uri.substring(0, position));
|
|
_queryString = _uri.substring(position + 1);
|
|
_queryParameters = _HttpUtils.splitQueryString(_queryString);
|
|
}
|
|
}
|
|
|
|
// Delegate functions for the HttpInputStream implementation.
|
|
int _streamAvailable() {
|
|
return _buffer.length;
|
|
}
|
|
|
|
List<int> _streamRead(int bytesToRead) {
|
|
return _buffer.readBytes(bytesToRead);
|
|
}
|
|
|
|
int _streamReadInto(List<int> buffer, int offset, int len) {
|
|
List<int> data = _buffer.readBytes(len);
|
|
buffer.setRange(offset, data.length, data);
|
|
}
|
|
|
|
void _streamSetErrorHandler(callback(e)) {
|
|
_streamErrorHandler = callback;
|
|
}
|
|
|
|
String _method;
|
|
String _uri;
|
|
String _path;
|
|
String _queryString;
|
|
Map<String, String> _queryParameters;
|
|
_HttpInputStream _inputStream;
|
|
_BufferList _buffer;
|
|
Function _streamErrorHandler;
|
|
}
|
|
|
|
|
|
// HTTP response object for sending a HTTP response.
|
|
class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
|
|
_HttpResponse(_HttpConnection httpConnection)
|
|
: super(httpConnection),
|
|
_statusCode = HttpStatus.OK;
|
|
|
|
void set contentLength(int contentLength) {
|
|
if (_state >= HEADER_SENT) throw new HttpException("Header already sent");
|
|
_contentLength = contentLength;
|
|
}
|
|
|
|
int get statusCode() => _statusCode;
|
|
void set statusCode(int statusCode) {
|
|
if (_outputStream != null) throw new HttpException("Header already sent");
|
|
_statusCode = statusCode;
|
|
}
|
|
|
|
String get reasonPhrase() => _findReasonPhrase(_statusCode);
|
|
void set reasonPhrase(String reasonPhrase) {
|
|
if (_outputStream != null) throw new HttpException("Header already sent");
|
|
_reasonPhrase = reasonPhrase;
|
|
}
|
|
|
|
OutputStream get outputStream() {
|
|
if (_state >= DONE) throw new HttpException("Response closed");
|
|
if (_outputStream == null) {
|
|
_outputStream = new _HttpOutputStream(this);
|
|
}
|
|
return _outputStream;
|
|
}
|
|
|
|
DetachedSocket detachSocket() {
|
|
if (_state >= DONE) throw new HttpException("Response closed");
|
|
// Ensure that headers are written.
|
|
if (_state == START) {
|
|
_writeHeader();
|
|
}
|
|
_state = UPGRADED;
|
|
// Ensure that any trailing data is written.
|
|
_writeDone();
|
|
// Indicate to the connection that the response handling is done.
|
|
return _httpConnection._detachSocket();
|
|
}
|
|
|
|
void _responseEnd() {
|
|
_ensureHeadersSent();
|
|
_state = DONE;
|
|
// Stop tracking no pending write events.
|
|
_httpConnection._onNoPendingWrites = null;
|
|
// Ensure that any trailing data is written.
|
|
_writeDone();
|
|
// Indicate to the connection that the response handling is done.
|
|
_httpConnection._responseDone();
|
|
}
|
|
|
|
// Delegate functions for the HttpOutputStream implementation.
|
|
bool _streamWrite(List<int> buffer, bool copyBuffer) {
|
|
if (_state == DONE) throw new HttpException("Response closed");
|
|
return _write(buffer, copyBuffer);
|
|
}
|
|
|
|
bool _streamWriteFrom(List<int> buffer, int offset, int len) {
|
|
if (_state == DONE) throw new HttpException("Response closed");
|
|
return _writeList(buffer, offset, len);
|
|
}
|
|
|
|
void _streamClose() {
|
|
_responseEnd();
|
|
}
|
|
|
|
void _streamSetNoPendingWriteHandler(callback()) {
|
|
if (_state != DONE) {
|
|
_httpConnection._onNoPendingWrites = callback;
|
|
}
|
|
}
|
|
|
|
void _streamSetCloseHandler(callback()) {
|
|
// TODO(sgjesse): Handle this.
|
|
}
|
|
|
|
void _streamSetErrorHandler(callback(e)) {
|
|
_streamErrorHandler = callback;
|
|
}
|
|
|
|
String _findReasonPhrase(int statusCode) {
|
|
if (_reasonPhrase != null) {
|
|
return _reasonPhrase;
|
|
}
|
|
|
|
switch (statusCode) {
|
|
case HttpStatus.CONTINUE: return "Continue";
|
|
case HttpStatus.SWITCHING_PROTOCOLS: return "Switching Protocols";
|
|
case HttpStatus.OK: return "OK";
|
|
case HttpStatus.CREATED: return "Created";
|
|
case HttpStatus.ACCEPTED: return "Accepted";
|
|
case HttpStatus.NON_AUTHORITATIVE_INFORMATION:
|
|
return "Non-Authoritative Information";
|
|
case HttpStatus.NO_CONTENT: return "No Content";
|
|
case HttpStatus.RESET_CONTENT: return "Reset Content";
|
|
case HttpStatus.PARTIAL_CONTENT: return "Partial Content";
|
|
case HttpStatus.MULTIPLE_CHOICES: return "Multiple Choices";
|
|
case HttpStatus.MOVED_PERMANENTLY: return "Moved Permanently";
|
|
case HttpStatus.FOUND: return "Found";
|
|
case HttpStatus.SEE_OTHER: return "See Other";
|
|
case HttpStatus.NOT_MODIFIED: return "Not Modified";
|
|
case HttpStatus.USE_PROXY: return "Use Proxy";
|
|
case HttpStatus.TEMPORARY_REDIRECT: return "Temporary Redirect";
|
|
case HttpStatus.BAD_REQUEST: return "Bad Request";
|
|
case HttpStatus.UNAUTHORIZED: return "Unauthorized";
|
|
case HttpStatus.PAYMENT_REQUIRED: return "Payment Required";
|
|
case HttpStatus.FORBIDDEN: return "Forbidden";
|
|
case HttpStatus.NOT_FOUND: return "Not Found";
|
|
case HttpStatus.METHOD_NOT_ALLOWED: return "Method Not Allowed";
|
|
case HttpStatus.NOT_ACCEPTABLE: return "Not Acceptable";
|
|
case HttpStatus.PROXY_AUTHENTICATION_REQUIRED:
|
|
return "Proxy Authentication Required";
|
|
case HttpStatus.REQUEST_TIMEOUT: return "Request Time-out";
|
|
case HttpStatus.CONFLICT: return "Conflict";
|
|
case HttpStatus.GONE: return "Gone";
|
|
case HttpStatus.LENGTH_REQUIRED: return "Length Required";
|
|
case HttpStatus.PRECONDITION_FAILED: return "Precondition Failed";
|
|
case HttpStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
return "Request Entity Too Large";
|
|
case HttpStatus.REQUEST_URI_TOO_LONG: return "Request-URI Too Large";
|
|
case HttpStatus.UNSUPPORTED_MEDIA_TYPE: return "Unsupported Media Type";
|
|
case HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE:
|
|
return "Requested range not satisfiable";
|
|
case HttpStatus.EXPECTATION_FAILED: return "Expectation Failed";
|
|
case HttpStatus.INTERNAL_SERVER_ERROR: return "Internal Server Error";
|
|
case HttpStatus.NOT_IMPLEMENTED: return "Not Implemented";
|
|
case HttpStatus.BAD_GATEWAY: return "Bad Gateway";
|
|
case HttpStatus.SERVICE_UNAVAILABLE: return "Service Unavailable";
|
|
case HttpStatus.GATEWAY_TIMEOUT: return "Gateway Time-out";
|
|
case HttpStatus.HTTP_VERSION_NOT_SUPPORTED:
|
|
return "Http Version not supported";
|
|
default: return "Status $statusCode";
|
|
}
|
|
}
|
|
|
|
bool _writeHeader() {
|
|
List<int> data;
|
|
|
|
// HTTP/1.0 does not support chunked.
|
|
if (_protocolVersion == "1.0" && _contentLength < 0) {
|
|
throw new HttpException("Content length required for HTTP 1.0");
|
|
}
|
|
|
|
// Write status line.
|
|
if (_protocolVersion == "1.1") {
|
|
_httpConnection._write(_Const.HTTP11);
|
|
} else {
|
|
_httpConnection._write(_Const.HTTP10);
|
|
}
|
|
_writeSP();
|
|
data = _statusCode.toString().charCodes();
|
|
_httpConnection._write(data);
|
|
_writeSP();
|
|
data = reasonPhrase.charCodes();
|
|
_httpConnection._write(data);
|
|
_writeCRLF();
|
|
|
|
// Determine the value of the "Transfer-Encoding" header based on
|
|
// whether the content length is known.
|
|
if (_contentLength > 0) {
|
|
_headers.set(HttpHeaders.CONTENT_LENGTH, _contentLength.toString());
|
|
} else if (_contentLength < 0) {
|
|
_headers.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
|
}
|
|
|
|
// Write headers.
|
|
bool allWritten = _writeHeaders();
|
|
_state = HEADER_SENT;
|
|
return allWritten;
|
|
}
|
|
|
|
// Response status code.
|
|
int _statusCode;
|
|
String _reasonPhrase;
|
|
_HttpOutputStream _outputStream;
|
|
Function _streamErrorHandler;
|
|
}
|
|
|
|
|
|
class _HttpInputStream extends _BaseDataInputStream implements InputStream {
|
|
_HttpInputStream(_HttpRequestResponseBase this._requestOrResponse) {
|
|
_checkScheduleCallbacks();
|
|
}
|
|
|
|
int available() {
|
|
return _requestOrResponse._streamAvailable();
|
|
}
|
|
|
|
void pipe(OutputStream output, [bool close = true]) {
|
|
_pipe(this, output, close: close);
|
|
}
|
|
|
|
List<int> _read(int bytesToRead) {
|
|
List<int> result = _requestOrResponse._streamRead(bytesToRead);
|
|
_checkScheduleCallbacks();
|
|
return result;
|
|
}
|
|
|
|
void set onError(void callback(e)) {
|
|
_requestOrResponse._streamSetErrorHandler(callback);
|
|
}
|
|
|
|
int _readInto(List<int> buffer, int offset, int len) {
|
|
int result = _requestOrResponse._streamReadInto(buffer, offset, len);
|
|
_checkScheduleCallbacks();
|
|
return result;
|
|
}
|
|
|
|
void _close() {
|
|
// TODO(sgjesse): Handle this.
|
|
}
|
|
|
|
void _dataReceived() {
|
|
super._dataReceived();
|
|
}
|
|
|
|
_HttpRequestResponseBase _requestOrResponse;
|
|
}
|
|
|
|
|
|
class _HttpOutputStream extends _BaseOutputStream implements OutputStream {
|
|
_HttpOutputStream(_HttpRequestResponseBase this._requestOrResponse);
|
|
|
|
bool write(List<int> buffer, [bool copyBuffer = true]) {
|
|
return _requestOrResponse._streamWrite(buffer, copyBuffer);
|
|
}
|
|
|
|
bool writeFrom(List<int> buffer, [int offset = 0, int len]) {
|
|
return _requestOrResponse._streamWriteFrom(buffer, offset, len);
|
|
}
|
|
|
|
void close() {
|
|
_requestOrResponse._streamClose();
|
|
}
|
|
|
|
void destroy() {
|
|
throw "Not implemented";
|
|
}
|
|
|
|
void set onNoPendingWrites(void callback()) {
|
|
_requestOrResponse._streamSetNoPendingWriteHandler(callback);
|
|
}
|
|
|
|
void set onClosed(void callback()) {
|
|
_requestOrResponse._streamSetCloseHandler(callback);
|
|
}
|
|
|
|
void set onError(void callback(e)) {
|
|
_requestOrResponse._streamSetErrorHandler(callback);
|
|
}
|
|
|
|
_HttpRequestResponseBase _requestOrResponse;
|
|
}
|
|
|
|
|
|
class _HttpConnectionBase implements Hashable {
|
|
_HttpConnectionBase() : _sendBuffers = new Queue(),
|
|
_httpParser = new _HttpParser() {
|
|
_hashCode = _nextHashCode;
|
|
_nextHashCode = (_nextHashCode + 1) & 0xFFFFFFF;
|
|
}
|
|
|
|
void _connectionEstablished(Socket socket) {
|
|
_socket = socket;
|
|
// Register handler for socket events.
|
|
_socket.onData = _onData;
|
|
_socket.onClosed = _onClosed;
|
|
_socket.onError = _onError;
|
|
// Ignore errors in the socket output stream as this is getting
|
|
// the same errors as the socket itself.
|
|
_socket.outputStream.onError = (e) => null;
|
|
}
|
|
|
|
bool _write(List<int> data, [bool copyBuffer = false]) {
|
|
if (!_error && !_closing) {
|
|
return _socket.outputStream.write(data, copyBuffer);
|
|
}
|
|
}
|
|
|
|
bool _writeFrom(List<int> buffer, [int offset, int len]) {
|
|
if (!_error && !_closing) {
|
|
return _socket.outputStream.writeFrom(buffer, offset, len);
|
|
}
|
|
}
|
|
|
|
bool _close() {
|
|
_closing = true;
|
|
_socket.outputStream.close();
|
|
}
|
|
|
|
bool _destroy() {
|
|
_closing = true;
|
|
_socket.close();
|
|
}
|
|
|
|
void _onData() {
|
|
int available = _socket.available();
|
|
if (available == 0) {
|
|
return;
|
|
}
|
|
|
|
List<int> buffer = new Uint8List(available);
|
|
int bytesRead = _socket.readList(buffer, 0, available);
|
|
if (bytesRead > 0) {
|
|
int parsed = _httpParser.writeList(buffer, 0, bytesRead);
|
|
if (!_httpParser.upgrade) {
|
|
if (parsed != bytesRead) {
|
|
if (_socket != null) {
|
|
// TODO(sgjesse): Error handling.
|
|
_destroy();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onClosed() {
|
|
_closing = true;
|
|
_onConnectionClosed(null);
|
|
}
|
|
|
|
void _onError(e) {
|
|
// If an error occurs, make sure to close the socket if one is associated.
|
|
_error = true;
|
|
if (_socket != null) {
|
|
_socket.close();
|
|
}
|
|
_onConnectionClosed(e);
|
|
}
|
|
|
|
DetachedSocket _detachSocket() {
|
|
_socket.onData = null;
|
|
_socket.onClosed = null;
|
|
_socket.onError = null;
|
|
_socket.outputStream.onNoPendingWrites = null;
|
|
Socket socket = _socket;
|
|
_socket = null;
|
|
if (onDetach != null) onDetach();
|
|
return new _DetachedSocket(socket, _httpParser.unparsedData);
|
|
}
|
|
|
|
abstract void _onConnectionClosed(e);
|
|
abstract void _responseDone();
|
|
|
|
void set _onNoPendingWrites(void callback()) {
|
|
if (!_error) {
|
|
_socket.outputStream.onNoPendingWrites = callback;
|
|
}
|
|
}
|
|
|
|
int hashCode() => _hashCode;
|
|
|
|
Socket _socket;
|
|
bool _closing = false; // Is the socket closed by the client?
|
|
bool _error = false; // Is the socket closed due to an error?
|
|
_HttpParser _httpParser;
|
|
|
|
Queue _sendBuffers;
|
|
|
|
Function onDetach;
|
|
|
|
// Hash code for HTTP connection. Currently this is just a counter.
|
|
int _hashCode;
|
|
static int _nextHashCode = 0;
|
|
}
|
|
|
|
|
|
// HTTP server connection over a socket.
|
|
class _HttpConnection extends _HttpConnectionBase {
|
|
_HttpConnection(HttpServer this._server) {
|
|
// Register HTTP parser callbacks.
|
|
_httpParser.requestStart =
|
|
(method, uri, version) => _onRequestStart(method, uri, version);
|
|
_httpParser.responseStart =
|
|
(statusCode, reasonPhrase, version) =>
|
|
_onResponseStart(statusCode, reasonPhrase, version);
|
|
_httpParser.headerReceived =
|
|
(name, value) => _onHeaderReceived(name, value);
|
|
_httpParser.headersComplete = () => _onHeadersComplete();
|
|
_httpParser.dataReceived = (data) => _onDataReceived(data);
|
|
_httpParser.dataEnd = (close) => _onDataEnd(close);
|
|
_httpParser.error = (e) => _onError(e);
|
|
}
|
|
|
|
void _onConnectionClosed(e) {
|
|
// Don't report errors when HTTP parser is in idle state. Clients
|
|
// can close the connection and cause a connection reset by peer
|
|
// error which is OK.
|
|
if (e != null && onError != null && !_httpParser.isIdle) {
|
|
onError(e);
|
|
// Propagate the error to the streams.
|
|
if (_request != null && _request._streamErrorHandler != null) {
|
|
_request._streamErrorHandler(e);
|
|
}
|
|
if (_response != null && _response._streamErrorHandler != null) {
|
|
_response._streamErrorHandler(e);
|
|
}
|
|
}
|
|
|
|
// If currently not processing any request just close the socket.
|
|
if (_httpParser.isIdle) {
|
|
_destroy();
|
|
if (onClosed != null && e == null) {
|
|
// Don't call onClosed if onError has been called.
|
|
onClosed();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Processing a request.
|
|
if (e == null) {
|
|
// Indicate connection close to the HTTP parser.
|
|
_httpParser.connectionClosed();
|
|
}
|
|
}
|
|
|
|
void _onRequestStart(String method, String uri, String version) {
|
|
// Create new request and response objects for this request.
|
|
_request = new _HttpRequest(this);
|
|
_response = new _HttpResponse(this);
|
|
_request._onRequestStart(method, uri, version);
|
|
_request._protocolVersion = version;
|
|
_response._protocolVersion = version;
|
|
}
|
|
|
|
void _onResponseStart(int statusCode, String reasonPhrase, String version) {
|
|
// TODO(sgjesse): Error handling.
|
|
}
|
|
|
|
void _onHeaderReceived(String name, String value) {
|
|
_request._onHeaderReceived(name, value);
|
|
}
|
|
|
|
void _onHeadersComplete() {
|
|
_request._onHeadersComplete();
|
|
_response.persistentConnection = _httpParser.persistentConnection;
|
|
if (onRequestReceived != null) {
|
|
onRequestReceived(_request, _response);
|
|
}
|
|
}
|
|
|
|
void _onDataReceived(List<int> data) {
|
|
_request._onDataReceived(data);
|
|
}
|
|
|
|
void _onDataEnd(bool close) {
|
|
if (_request != null) {
|
|
_request._onDataEnd();
|
|
}
|
|
_request = null;
|
|
}
|
|
|
|
void _responseDone() {
|
|
// If the connection is closing then close the output stream to
|
|
// fully close the socket.
|
|
if (_closing) {
|
|
_socket.close();
|
|
}
|
|
_response = null;
|
|
}
|
|
|
|
HttpServer _server;
|
|
HttpRequest _request;
|
|
HttpResponse _response;
|
|
|
|
// Callbacks.
|
|
Function onRequestReceived;
|
|
Function onClosed;
|
|
Function onError;
|
|
}
|
|
|
|
|
|
class _RequestHandlerRegistration {
|
|
_RequestHandlerRegistration(Function this._matcher, Function this._handler);
|
|
Function _matcher;
|
|
Function _handler;
|
|
}
|
|
|
|
|
|
// HTTP server waiting for socket connections. The connections are
|
|
// managed by the server and as requests are received the request.
|
|
class _HttpServer implements HttpServer {
|
|
_HttpServer() : _connections = new Set<_HttpConnection>(),
|
|
_handlers = new List<_RequestHandlerRegistration>();
|
|
|
|
void listen(String host, int port, [int backlog = 5]) {
|
|
listenOn(new ServerSocket(host, port, backlog));
|
|
_closeServer = true;
|
|
}
|
|
|
|
void listenOn(ServerSocket serverSocket) {
|
|
void onConnection(Socket socket) {
|
|
// Accept the client connection.
|
|
_HttpConnection connection = new _HttpConnection(this);
|
|
connection._connectionEstablished(socket);
|
|
_connections.add(connection);
|
|
connection.onRequestReceived = _handleRequest;
|
|
connection.onClosed = () => _connections.remove(connection);
|
|
connection.onDetach = () => _connections.remove(connection);
|
|
connection.onError = (e) {
|
|
_connections.remove(connection);
|
|
if (_onError != null) {
|
|
_onError(e);
|
|
} else {
|
|
throw(e);
|
|
}
|
|
};
|
|
}
|
|
serverSocket.onConnection = onConnection;
|
|
_server = serverSocket;
|
|
_closeServer = false;
|
|
}
|
|
|
|
addRequestHandler(bool matcher(HttpRequest request),
|
|
void handler(HttpRequest request, HttpResponse response)) {
|
|
_handlers.add(new _RequestHandlerRegistration(matcher, handler));
|
|
}
|
|
|
|
void set defaultRequestHandler(
|
|
void handler(HttpRequest request, HttpResponse response)) {
|
|
_defaultHandler = handler;
|
|
}
|
|
|
|
void close() {
|
|
if (_server !== null && _closeServer) {
|
|
_server.close();
|
|
}
|
|
_server = null;
|
|
for (_HttpConnection connection in _connections) {
|
|
connection._destroy();
|
|
}
|
|
_connections.clear();
|
|
}
|
|
|
|
int get port() {
|
|
if (_server === null) {
|
|
throw new HttpException("The HttpServer is not listening on a port.");
|
|
}
|
|
return _server.port;
|
|
}
|
|
|
|
void set onError(void callback(e)) {
|
|
_onError = callback;
|
|
}
|
|
|
|
void _handleRequest(HttpRequest request, HttpResponse response) {
|
|
for (int i = 0; i < _handlers.length; i++) {
|
|
if (_handlers[i]._matcher(request)) {
|
|
Function handler = _handlers[i]._handler;
|
|
try {
|
|
handler(request, response);
|
|
} catch (var e) {
|
|
if (_onError != null) {
|
|
_onError(e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_defaultHandler != null) {
|
|
_defaultHandler(request, response);
|
|
} else {
|
|
response.statusCode = HttpStatus.NOT_FOUND;
|
|
if (request.protocolVersion == "1.0") {
|
|
response.contentLength = 0;
|
|
}
|
|
response.outputStream.close();
|
|
}
|
|
}
|
|
|
|
|
|
ServerSocket _server; // The server listen socket.
|
|
bool _closeServer = false;
|
|
Set<_HttpConnection> _connections; // Set of currently connected clients.
|
|
List<_RequestHandlerRegistration> _handlers;
|
|
Object _defaultHandler;
|
|
Function _onError;
|
|
}
|
|
|
|
|
|
class _HttpClientRequest
|
|
extends _HttpRequestResponseBase implements HttpClientRequest {
|
|
_HttpClientRequest(String this._method,
|
|
String this._uri,
|
|
_HttpClientConnection connection)
|
|
: super(connection) {
|
|
_connection = connection;
|
|
// Default GET requests to have no content.
|
|
if (_method == "GET") {
|
|
_contentLength = 0;
|
|
}
|
|
}
|
|
|
|
void set contentLength(int contentLength) {
|
|
if (_state >= HEADER_SENT) throw new HttpException("Header already sent");
|
|
_contentLength = contentLength;
|
|
}
|
|
|
|
OutputStream get outputStream() {
|
|
if (_state == DONE) throw new HttpException("Request closed");
|
|
if (_outputStream == null) {
|
|
_outputStream = new _HttpOutputStream(this);
|
|
}
|
|
return _outputStream;
|
|
}
|
|
|
|
// Delegate functions for the HttpOutputStream implementation.
|
|
bool _streamWrite(List<int> buffer, bool copyBuffer) {
|
|
if (_state == DONE) throw new HttpException("Request closed");
|
|
return _write(buffer, copyBuffer);
|
|
}
|
|
|
|
bool _streamWriteFrom(List<int> buffer, int offset, int len) {
|
|
if (_state == DONE) throw new HttpException("Request closed");
|
|
return _writeList(buffer, offset, len);
|
|
}
|
|
|
|
void _streamClose() {
|
|
_ensureHeadersSent();
|
|
_state = DONE;
|
|
// Stop tracking no pending write events.
|
|
_httpConnection._onNoPendingWrites = null;
|
|
// Ensure that any trailing data is written.
|
|
_writeDone();
|
|
}
|
|
|
|
void _streamSetNoPendingWriteHandler(callback()) {
|
|
if (_state != DONE) {
|
|
_httpConnection._onNoPendingWrites = callback;
|
|
}
|
|
}
|
|
|
|
void _streamSetCloseHandler(callback()) {
|
|
// TODO(sgjesse): Handle this.
|
|
}
|
|
|
|
void _streamSetErrorHandler(callback(e)) {
|
|
_streamErrorHandler = callback;
|
|
}
|
|
|
|
void _writeHeader() {
|
|
List<int> data;
|
|
|
|
// Write request line.
|
|
data = _method.toString().charCodes();
|
|
_httpConnection._write(data);
|
|
_writeSP();
|
|
data = _uri.toString().charCodes();
|
|
_httpConnection._write(data);
|
|
_writeSP();
|
|
_httpConnection._write(_Const.HTTP11);
|
|
_writeCRLF();
|
|
|
|
// Determine the value of the "Transfer-Encoding" header based on
|
|
// whether the content length is known. If there is no content
|
|
// neither "Content-Length" nor "Transfer-Encoding" is set
|
|
if (_contentLength > 0) {
|
|
_headers.set(HttpHeaders.CONTENT_LENGTH, _contentLength.toString());
|
|
} else if (_contentLength < 0) {
|
|
_headers.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
|
}
|
|
|
|
// Write headers.
|
|
_writeHeaders();
|
|
_state = HEADER_SENT;
|
|
}
|
|
|
|
String _method;
|
|
String _uri;
|
|
_HttpClientConnection _connection;
|
|
_HttpOutputStream _outputStream;
|
|
Function _streamErrorHandler;
|
|
}
|
|
|
|
|
|
class _HttpClientResponse
|
|
extends _HttpRequestResponseBase implements HttpClientResponse {
|
|
_HttpClientResponse(_HttpClientConnection connection)
|
|
: super(connection) {
|
|
_connection = connection;
|
|
}
|
|
|
|
int get statusCode() => _statusCode;
|
|
String get reasonPhrase() => _reasonPhrase;
|
|
|
|
bool get isRedirect() {
|
|
return statusCode == HttpStatus.MOVED_PERMANENTLY ||
|
|
statusCode == HttpStatus.FOUND ||
|
|
statusCode == HttpStatus.SEE_OTHER ||
|
|
statusCode == HttpStatus.TEMPORARY_REDIRECT;
|
|
}
|
|
|
|
InputStream get inputStream() {
|
|
if (_inputStream == null) {
|
|
_inputStream = new _HttpInputStream(this);
|
|
}
|
|
return _inputStream;
|
|
}
|
|
|
|
void _onRequestStart(String method, String uri, String version) {
|
|
// TODO(sgjesse): Error handling
|
|
}
|
|
|
|
void _onResponseStart(int statusCode, String reasonPhrase, String version) {
|
|
_statusCode = statusCode;
|
|
_reasonPhrase = reasonPhrase;
|
|
}
|
|
|
|
void _onHeaderReceived(String name, String value) {
|
|
_headers.add(name, value);
|
|
}
|
|
|
|
void _onHeadersComplete() {
|
|
_headers._mutable = false;
|
|
_buffer = new _BufferList();
|
|
if (isRedirect && _connection.followRedirects) {
|
|
if (_connection._redirects == null ||
|
|
_connection._redirects.length < _connection.maxRedirects) {
|
|
// Check the location header.
|
|
List<String> location = headers[HttpHeaders.LOCATION];
|
|
if (location == null || location.length > 1) {
|
|
throw new RedirectException("Invalid redirect",
|
|
_connection._redirects);
|
|
}
|
|
// Check for redirect loop
|
|
if (_connection._redirects != null) {
|
|
Uri redirectUrl = new Uri.fromString(location[0]);
|
|
for (int i = 0; i < _connection._redirects.length; i++) {
|
|
if (_connection._redirects[i].location.toString() ==
|
|
redirectUrl.toString()) {
|
|
throw new RedirectLoopException(_connection._redirects);
|
|
}
|
|
}
|
|
}
|
|
// Drain body and redirect.
|
|
inputStream.onData = inputStream.read;
|
|
inputStream.onClosed = _connection.redirect;
|
|
} else {
|
|
throw new RedirectLimitExceededException(_connection._redirects);
|
|
}
|
|
} else if (_connection._onResponse != null) {
|
|
_connection._onResponse(this);
|
|
}
|
|
}
|
|
|
|
void _onDataReceived(List<int> data) {
|
|
_buffer.add(data);
|
|
if (_inputStream != null) _inputStream._dataReceived();
|
|
}
|
|
|
|
void _onDataEnd() {
|
|
_connection._responseDone();
|
|
if (_inputStream != null) _inputStream._closeReceived();
|
|
}
|
|
|
|
// Delegate functions for the HttpInputStream implementation.
|
|
int _streamAvailable() {
|
|
return _buffer.length;
|
|
}
|
|
|
|
List<int> _streamRead(int bytesToRead) {
|
|
return _buffer.readBytes(bytesToRead);
|
|
}
|
|
|
|
int _streamReadInto(List<int> buffer, int offset, int len) {
|
|
List<int> data = _buffer.readBytes(len);
|
|
buffer.setRange(offset, data.length, data);
|
|
return data.length;
|
|
}
|
|
|
|
void _streamSetErrorHandler(callback(e)) {
|
|
_streamErrorHandler = callback;
|
|
}
|
|
|
|
int _statusCode;
|
|
String _reasonPhrase;
|
|
|
|
_HttpClientConnection _connection;
|
|
_HttpInputStream _inputStream;
|
|
_BufferList _buffer;
|
|
Function _streamErrorHandler;
|
|
}
|
|
|
|
|
|
class _HttpClientConnection
|
|
extends _HttpConnectionBase implements HttpClientConnection {
|
|
_HttpClientConnection(_HttpClient this._client);
|
|
|
|
void _connectionEstablished(_SocketConnection socketConn) {
|
|
super._connectionEstablished(socketConn._socket);
|
|
_socketConn = socketConn;
|
|
// Register HTTP parser callbacks.
|
|
_httpParser.requestStart =
|
|
(method, uri, version) => _onRequestStart(method, uri, version);
|
|
_httpParser.responseStart =
|
|
(statusCode, reasonPhrase, version) =>
|
|
_onResponseStart(statusCode, reasonPhrase, version);
|
|
_httpParser.headerReceived =
|
|
(name, value) => _onHeaderReceived(name, value);
|
|
_httpParser.headersComplete = () => _onHeadersComplete();
|
|
_httpParser.dataReceived = (data) => _onDataReceived(data);
|
|
_httpParser.dataEnd = (closed) => _onDataEnd(closed);
|
|
_httpParser.error = (e) => _onError(e);
|
|
// Tell the HTTP parser the method it is expecting a response to.
|
|
_httpParser.responseToMethod = _method;
|
|
}
|
|
|
|
void _responseDone() {
|
|
if (_closing) {
|
|
if (_socket != null) {
|
|
_socket.close();
|
|
}
|
|
} else {
|
|
_client._returnSocketConnection(_socketConn);
|
|
}
|
|
_socket = null;
|
|
_socketConn = null;
|
|
}
|
|
|
|
HttpClientRequest open(String method, String uri) {
|
|
_method = method;
|
|
_request = new _HttpClientRequest(method, uri, this);
|
|
_response = new _HttpClientResponse(this);
|
|
return _request;
|
|
}
|
|
|
|
DetachedSocket detachSocket() {
|
|
return _detachSocket();
|
|
}
|
|
|
|
void _onConnectionClosed(e) {
|
|
// Socket is closed either due to an error or due to normal socket close.
|
|
if (e != null) {
|
|
if (_onErrorCallback != null) {
|
|
_onErrorCallback(e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
_closing = true;
|
|
if (e != null) {
|
|
// Propagate the error to the streams.
|
|
if (_response != null && _response._streamErrorHandler != null) {
|
|
_response._streamErrorHandler(e);
|
|
}
|
|
_responseDone();
|
|
} else {
|
|
// If there was no socket error the socket was closed
|
|
// normally. Indicate closing to the HTTP Parser as there might
|
|
// still be an HTTP error.
|
|
_httpParser.connectionClosed();
|
|
}
|
|
}
|
|
|
|
void _onRequestStart(String method, String uri, String version) {
|
|
// TODO(sgjesse): Error handling.
|
|
}
|
|
|
|
void _onResponseStart(int statusCode, String reasonPhrase, String version) {
|
|
_response._onResponseStart(statusCode, reasonPhrase, version);
|
|
}
|
|
|
|
void _onHeaderReceived(String name, String value) {
|
|
_response._onHeaderReceived(name, value);
|
|
}
|
|
|
|
void _onHeadersComplete() {
|
|
_response._onHeadersComplete();
|
|
}
|
|
|
|
void _onDataReceived(List<int> data) {
|
|
_response._onDataReceived(data);
|
|
}
|
|
|
|
void _onDataEnd(bool close) {
|
|
if (close) _closing = true;
|
|
_response._onDataEnd();
|
|
}
|
|
|
|
void set onRequest(void handler(HttpClientRequest request)) {
|
|
_onRequest = handler;
|
|
}
|
|
|
|
void set onResponse(void handler(HttpClientResponse response)) {
|
|
_onResponse = handler;
|
|
}
|
|
|
|
void set onError(void callback(e)) {
|
|
_onErrorCallback = callback;
|
|
}
|
|
|
|
void redirect([String method, Uri url]) {
|
|
if (_socketConn != null) {
|
|
throw new HttpException("Cannot redirect with body data pending");
|
|
}
|
|
if (method == null) method = _method;
|
|
if (url == null) {
|
|
url = new Uri.fromString(_response.headers.value(HttpHeaders.LOCATION));
|
|
}
|
|
if (_redirects == null) {
|
|
_redirects = new List<_RedirectInfo>();
|
|
}
|
|
_redirects.add(new _RedirectInfo(_response.statusCode, method, url));
|
|
_request = null;
|
|
_response = null;
|
|
// Open redirect URL using the same connection instance.
|
|
_client._openUrl(method, url, this);
|
|
}
|
|
|
|
List<RedirectInfo> get redirects() => _redirects;
|
|
|
|
Function _onRequest;
|
|
Function _onResponse;
|
|
Function _onErrorCallback;
|
|
|
|
_HttpClient _client;
|
|
_SocketConnection _socketConn;
|
|
HttpClientRequest _request;
|
|
HttpClientResponse _response;
|
|
String _method;
|
|
|
|
// Redirect handling
|
|
bool followRedirects = true;
|
|
int maxRedirects = 5;
|
|
List<_RedirectInfo> _redirects;
|
|
|
|
// Callbacks.
|
|
var requestReceived;
|
|
}
|
|
|
|
|
|
// Class for holding keep-alive sockets in the cache for the HTTP
|
|
// client together with the connection information.
|
|
class _SocketConnection {
|
|
_SocketConnection(String this._host,
|
|
int this._port,
|
|
Socket this._socket);
|
|
|
|
void _markReturned() {
|
|
_socket.onData = null;
|
|
_socket.onClosed = null;
|
|
_socket.onError = null;
|
|
_returnTime = new Date.now();
|
|
}
|
|
|
|
Duration _idleTime(Date now) => now.difference(_returnTime);
|
|
|
|
int hashCode() => _socket.hashCode();
|
|
|
|
String _host;
|
|
int _port;
|
|
Socket _socket;
|
|
Date _returnTime;
|
|
}
|
|
|
|
|
|
class _HttpClient implements HttpClient {
|
|
static final int DEFAULT_EVICTION_TIMEOUT = 60000;
|
|
|
|
_HttpClient() : _openSockets = new Map(),
|
|
_activeSockets = new Set(),
|
|
_shutdown = false;
|
|
|
|
HttpClientConnection open(
|
|
String method, String host, int port, String path) {
|
|
return _open(method, host, port, path);
|
|
}
|
|
|
|
HttpClientConnection _open(String method,
|
|
String host,
|
|
int port,
|
|
String path,
|
|
[_HttpClientConnection connection]) {
|
|
if (_shutdown) throw new HttpException("HttpClient shutdown");
|
|
if (method == null || host == null || port == null || path == null) {
|
|
throw new IllegalArgumentException(null);
|
|
}
|
|
return _prepareHttpClientConnection(host, port, method, path, connection);
|
|
}
|
|
|
|
HttpClientConnection openUrl(String method, Uri url) {
|
|
_openUrl(method, url);
|
|
}
|
|
|
|
HttpClientConnection _openUrl(String method,
|
|
Uri url,
|
|
[_HttpClientConnection connection]) {
|
|
if (url.scheme != "http") {
|
|
throw new HttpException("Unsupported URL scheme ${url.scheme}");
|
|
}
|
|
if (url.userInfo != "") {
|
|
throw new HttpException("Unsupported user info ${url.userInfo}");
|
|
}
|
|
int port = url.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : url.port;
|
|
String path;
|
|
if (url.query != "") {
|
|
if (url.fragment != "") {
|
|
path = "${url.path}?${url.query}#${url.fragment}";
|
|
} else {
|
|
path = "${url.path}?${url.query}";
|
|
}
|
|
} else {
|
|
path = url.path;
|
|
}
|
|
return _open(method, url.domain, port, path, connection);
|
|
}
|
|
|
|
HttpClientConnection get(String host, int port, String path) {
|
|
return _open("GET", host, port, path);
|
|
}
|
|
|
|
HttpClientConnection getUrl(Uri url) => _openUrl("GET", url);
|
|
|
|
HttpClientConnection post(String host, int port, String path) {
|
|
return _open("POST", host, port, path);
|
|
}
|
|
|
|
HttpClientConnection postUrl(Uri url) => _openUrl("POST", url);
|
|
|
|
void shutdown() {
|
|
_openSockets.forEach((String key, Queue<_SocketConnection> connections) {
|
|
while (!connections.isEmpty()) {
|
|
_SocketConnection socketConn = connections.removeFirst();
|
|
socketConn._socket.close();
|
|
}
|
|
});
|
|
_activeSockets.forEach((_SocketConnection socketConn) {
|
|
socketConn._socket.close();
|
|
});
|
|
if (_evictionTimer != null) {
|
|
_evictionTimer.cancel();
|
|
}
|
|
_shutdown = true;
|
|
}
|
|
|
|
String _connectionKey(String host, int port) {
|
|
return "$host:$port";
|
|
}
|
|
|
|
HttpClientConnection _prepareHttpClientConnection(
|
|
String host,
|
|
int port,
|
|
String method,
|
|
String path,
|
|
[_HttpClientConnection connection]) {
|
|
|
|
void _connectionOpened(_SocketConnection socketConn,
|
|
_HttpClientConnection connection) {
|
|
connection._connectionEstablished(socketConn);
|
|
HttpClientRequest request = connection.open(method, path);
|
|
request.headers.host = host;
|
|
request.headers.port = port;
|
|
if (connection._onRequest != null) {
|
|
connection._onRequest(request);
|
|
} else {
|
|
request.outputStream.close();
|
|
}
|
|
}
|
|
|
|
// Create a new connection if we are not re-using an existing one.
|
|
if (connection == null) {
|
|
connection = new _HttpClientConnection(this);
|
|
}
|
|
connection.onDetach = () => _activeSockets.remove(connection._socketConn);
|
|
|
|
// If there are active connections for this key get the first one
|
|
// otherwise create a new one.
|
|
Queue socketConnections = _openSockets[_connectionKey(host, port)];
|
|
if (socketConnections == null || socketConnections.isEmpty()) {
|
|
Socket socket = new Socket(host, port);
|
|
// Until the connection is established handle connection errors
|
|
// here as the HttpClientConnection object is not yet associated
|
|
// with the socket.
|
|
socket.onError = (e) {
|
|
// Report the error through the HttpClientConnection object to
|
|
// the client.
|
|
connection._onError(e);
|
|
};
|
|
socket.onConnect = () {
|
|
// When the connection is established, clear the error
|
|
// callback as it will now be handled by the
|
|
// HttpClientConnection object which will be associated with
|
|
// the connected socket.
|
|
socket.onError = null;
|
|
_SocketConnection socketConn =
|
|
new _SocketConnection(host, port, socket);
|
|
_activeSockets.add(socketConn);
|
|
_connectionOpened(socketConn, connection);
|
|
};
|
|
} else {
|
|
_SocketConnection socketConn = socketConnections.removeFirst();
|
|
_activeSockets.add(socketConn);
|
|
new Timer(0, (ignored) => _connectionOpened(socketConn, connection));
|
|
|
|
// Get rid of eviction timer if there are no more active connections.
|
|
if (socketConnections.isEmpty()) {
|
|
_evictionTimer.cancel();
|
|
_evictionTimer = null;
|
|
}
|
|
}
|
|
|
|
return connection;
|
|
}
|
|
|
|
void _returnSocketConnection(_SocketConnection socketConn) {
|
|
// Mark socket as returned to unregister from the old connection.
|
|
socketConn._markReturned();
|
|
|
|
// If the HTTP client is beeing shutdown don't return the connection.
|
|
if (_shutdown) {
|
|
socketConn._socket.close();
|
|
return;
|
|
};
|
|
|
|
String key = _connectionKey(socketConn._host, socketConn._port);
|
|
|
|
// Get or create the connection list for this key.
|
|
Queue sockets = _openSockets[key];
|
|
if (sockets == null) {
|
|
sockets = new Queue();
|
|
_openSockets[key] = sockets;
|
|
}
|
|
|
|
// If there is currently no eviction timer start one.
|
|
if (_evictionTimer == null) {
|
|
void _handleEviction(Timer timer) {
|
|
Date now = new Date.now();
|
|
_openSockets.forEach(
|
|
void _(String key, Queue<_SocketConnection> connections) {
|
|
// As returned connections are added at the head of the
|
|
// list remove from the tail.
|
|
while (!connections.isEmpty()) {
|
|
_SocketConnection socketConn = connections.last();
|
|
if (socketConn._idleTime(now).inMilliseconds >
|
|
DEFAULT_EVICTION_TIMEOUT) {
|
|
connections.removeLast();
|
|
socketConn._socket.close();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
_evictionTimer = new Timer.repeating(10000, _handleEviction);
|
|
}
|
|
|
|
// Return connection.
|
|
_activeSockets.remove(socketConn);
|
|
sockets.addFirst(socketConn);
|
|
}
|
|
|
|
Function _onOpen;
|
|
Map<String, Queue<_SocketConnection>> _openSockets;
|
|
Set<_SocketConnection> _activeSockets;
|
|
Timer _evictionTimer;
|
|
bool _shutdown; // Has this HTTP client been shutdown?
|
|
}
|
|
|
|
|
|
class _DetachedSocket implements DetachedSocket {
|
|
_DetachedSocket(this._socket, this._unparsedData);
|
|
Socket get socket() => _socket;
|
|
List<int> get unparsedData() => _unparsedData;
|
|
Socket _socket;
|
|
List<int> _unparsedData;
|
|
}
|
|
|
|
|
|
class _RedirectInfo implements RedirectInfo {
|
|
const _RedirectInfo(int this.statusCode,
|
|
String this.method,
|
|
Uri this.location);
|
|
final int statusCode;
|
|
final String method;
|
|
final Uri location;
|
|
}
|