Chromium Code Reviews| Index: runtime/bin/http_impl.dart |
| diff --git a/runtime/bin/http_impl.dart b/runtime/bin/http_impl.dart |
| index a0d73baee293f486f2eca039d54a2e727da750d3..dec07933f98f5fd404e3fb4d26a8b3438cf00297 100644 |
| --- a/runtime/bin/http_impl.dart |
| +++ b/runtime/bin/http_impl.dart |
| @@ -308,7 +308,7 @@ class _HttpHeaders implements HttpHeaders { |
| class _HeaderValue implements HeaderValue { |
| _HeaderValue([String this.value = ""]); |
| - _HeaderValue.fromString(String value) { |
| + _HeaderValue.fromString(String value, [this.parameterSeparator = ";"]) { |
| // Parse the string. |
| _parse(value); |
| } |
| @@ -347,20 +347,26 @@ class _HeaderValue implements HeaderValue { |
| String parseValue() { |
| int start = index; |
| while (!done()) { |
| - if (s[index] == " " || s[index] == "\t" || s[index] == ";") break; |
| + if (s[index] == " " || |
| + s[index] == "\t" || |
| + s[index] == parameterSeparator) break; |
| index++; |
| } |
| return s.substring(start, index).toLowerCase(); |
| } |
| void expect(String expected) { |
| - if (done()) throw new HttpException("Failed to parse header value [$s]"); |
| + if (done()) throw new HttpException("Failed to parse header value $expected [$s]"); |
|
Mads Ager (google)
2012/10/17 11:26:43
Long line.
Søren Gjesse
2012/10/26 09:43:00
Done.
|
| if (s[index] != expected) { |
| - throw new HttpException("Failed to parse header value [$s]"); |
| + throw new HttpException("Failed to parse header value x $expected [$s]"); |
|
Mads Ager (google)
2012/10/17 11:26:43
Ditto.
Søren Gjesse
2012/10/26 09:43:00
Done.
|
| } |
| index++; |
| } |
| + void maybeExpect(String expected) { |
| + if (s[index] == expected) index++; |
| + } |
| + |
| void parseParameters() { |
| _parameters = new Map<String, String>(); |
| @@ -381,7 +387,7 @@ class _HeaderValue implements HeaderValue { |
| while (!done()) { |
| if (s[index] == "\\") { |
| if (index + 1 == s.length) { |
| - throw new HttpException("Failed to parse header value [$s]"); |
| + throw new HttpException("Failed to parse header value y [$s]"); |
|
Mads Ager (google)
2012/10/17 11:26:43
Accidental edit?
Søren Gjesse
2012/10/26 09:43:00
Done.
|
| } |
| index++; |
| } else if (s[index] == "\"") { |
| @@ -409,7 +415,7 @@ class _HeaderValue implements HeaderValue { |
| _parameters[name] = value; |
| skipWS(); |
| if (done()) return; |
| - expect(";"); |
| + expect(parameterSeparator); |
| } |
| } |
| @@ -417,11 +423,12 @@ class _HeaderValue implements HeaderValue { |
| value = parseValue(); |
| skipWS(); |
| if (done()) return; |
| - expect(";"); |
| + maybeExpect(parameterSeparator); |
| parseParameters(); |
| } |
| String value; |
| + String parameterSeparator; |
| Map<String, String> _parameters; |
| } |
| @@ -1725,6 +1732,54 @@ class _HttpClientResponse |
| } |
| } |
| + void _handleUnauthorized() { |
| + // Only try to authenticate if there is a challenge in the response. |
| + List<String> challenge = _headers[HttpHeaders.WWW_AUTHENTICATE]; |
| + if (challenge != null && challenge.length == 1) { |
| + HeaderValue header = |
| + new HeaderValue.fromString(challenge[0], parameterSeparator: ","); |
| + _AuthenticationScheme scheme = |
| + new _AuthenticationScheme.fromString(header.value); |
| + String realm = header.parameters["realm"]; |
| + |
| + // See if any credentials are available. |
| + _Credentials cr = |
|
Mads Ager (google)
2012/10/17 11:26:43
What if we authorize once and the credentials are
Søren Gjesse
2012/10/26 09:43:00
Added a used flag on credentials in the cache so t
|
| + _connection._client._findCredentials( |
| + _connection._request._uri, scheme); |
| + |
| + // Ask for more credentials if none found. |
| + if (cr == null) { |
| + if (_connection._client._authenticate( |
| + _connection._request._uri, scheme, realm)) { |
| + cr = _connection._client._findCredentials(_connection._request._uri, |
| + scheme); |
| + } |
| + } |
| + |
| + // If credentials found prepare for retrying the request. |
| + if (cr != null) { |
| + if (cr.scheme == _AuthenticationScheme.DIGEST) { |
| + cr.nonce = header.parameters["nonce"]; |
| + cr.algorithm = header.parameters["algorithm"]; |
| + cr.qop = header.parameters["qop"]; |
| + } |
| + // Drain body and retry. |
| + // TODO(sgesse): Support digest. |
| + if (cr.scheme == _AuthenticationScheme.DIGEST) { |
| + inputStream.onData = inputStream.read; |
| + inputStream.onClosed = _connection.retry; |
| + return; |
| + } |
| + } |
| + } |
| + |
| + // Fall through to here to perform normal response handling if |
| + // there is no sensible authorization handling. |
| + if (_connection._onResponse != null) { |
| + _connection._onResponse(this); |
| + } |
| + } |
| + |
| void _onHeadersComplete() { |
| _headers._mutable = false; |
| _buffer = new _BufferList(); |
| @@ -1753,6 +1808,8 @@ class _HttpClientResponse |
| } else { |
| throw new RedirectLimitExceededException(_connection._redirects); |
| } |
| + } else if (statusCode == HttpStatus.UNAUTHORIZED) { |
| + _handleUnauthorized(); |
| } else if (_connection._onResponse != null) { |
| _connection._onResponse(this); |
| } |
| @@ -1907,6 +1964,14 @@ class _HttpClientConnection |
| _onErrorCallback = callback; |
| } |
| + void retry() { |
| + if (_socketConn != null) { |
| + throw new HttpException("Cannot retry with body data pending"); |
| + } |
| + // Retry the URL using the same connection instance. |
| + _client._openUrl(_method, _request._uri, this); |
| + } |
| + |
| void redirect([String method, Uri url]) { |
| if (_socketConn != null) { |
| throw new HttpException("Cannot redirect with body data pending"); |
| @@ -2031,6 +2096,7 @@ class _HttpClient implements HttpClient { |
| _HttpClient() : _openSockets = new Map(), |
| _activeSockets = new Set(), |
| + credentials = new List<_HttpClientCredentials>(), |
| _shutdown = false; |
| HttpClientConnection open( |
| @@ -2061,9 +2127,6 @@ class _HttpClient implements HttpClient { |
| if (url.scheme != "http") { |
| throw new HttpException("Unsupported URL scheme ${url.scheme}"); |
| } |
| - if (url.userInfo != "") { |
| - throw new HttpException("Unsupported user info ${url.userInfo}"); |
| - } |
| return _open(method, url, connection); |
| } |
| @@ -2079,6 +2142,13 @@ class _HttpClient implements HttpClient { |
| HttpClientConnection postUrl(Uri url) => _openUrl("POST", url); |
| + set authenticate(f(Uri uri, String realm)) => _authenticate = f; |
| + |
| + void addCredentials( |
| + Uri url, String realm, HttpClientCredentials cr) { |
| + credentials.add(new _Credentials(url, realm, cr)); |
| + } |
| + |
| set findProxy(String f(Uri uri)) => _findProxy = f; |
| void shutdown() { |
| @@ -2122,6 +2192,20 @@ class _HttpClient implements HttpClient { |
| HttpClientRequest request = connection.open(method, url); |
| request.headers.host = host; |
| request.headers.port = port; |
| + if (url.userInfo != null && !url.userInfo.isEmpty()) { |
| + // If the URL contains user information use that for basic |
| + // authorization |
| + _UTF8Encoder encoder = new _UTF8Encoder(); |
| + String auth = |
| + CryptoUtils.bytesToBase64(encoder.encodeString(url.userInfo)); |
| + request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth"); |
| + } else { |
| + // Look for credentials. |
| + _Credentials cr = _findCredentials(url); |
| + if (cr != null) { |
| + cr.authorize(request); |
| + } |
| + } |
| if (connection._onRequest != null) { |
| connection._onRequest(request); |
| } else { |
| @@ -2265,11 +2349,23 @@ class _HttpClient implements HttpClient { |
| sockets.addFirst(socketConn); |
| } |
| + _Credentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) { |
| + // Look for credentials. |
| + _Credentials cr = |
| + credentials.reduce(null, (_Credentials prev, _Credentials value) { |
| + if (prev != null) return prev; |
|
Mads Ager (google)
2012/10/17 11:26:43
I would use a two-space indent for these three lin
Søren Gjesse
2012/10/26 09:43:00
Done.
|
| + if (value.applies(url, scheme)) return value; |
| + }); |
| + return cr; |
| + } |
| + |
| Function _onOpen; |
| Map<String, Queue<_SocketConnection>> _openSockets; |
| Set<_SocketConnection> _activeSockets; |
| + List<_HttpClientCredentials> credentials; |
| Timer _evictionTimer; |
| Function _findProxy; |
| + Function _authenticate; |
| bool _shutdown; // Has this HTTP client been shutdown? |
| } |
| @@ -2290,6 +2386,107 @@ class _DetachedSocket implements DetachedSocket { |
| } |
| +class _AuthenticationScheme { |
| + static const UNKNOWN = const _AuthenticationScheme(-1); |
| + static const BASIC = const _AuthenticationScheme(0); |
| + static const DIGEST = const _AuthenticationScheme(1); |
| + |
| + const _AuthenticationScheme(this._scheme); |
| + |
| + factory _AuthenticationScheme.fromString(String scheme) { |
| + if (scheme.toLowerCase() == "basic") return BASIC; |
| + if (scheme.toLowerCase() == "digest") return DIGEST; |
| + return UNKNOWN; |
| + } |
| + |
| + String toString() { |
| + if (this == BASIC) return "Basic"; |
| + if (this == DIGEST) return "Digest"; |
| + return "Unknown"; |
| + } |
| + |
| + final int _scheme; |
| +} |
| + |
| + |
| +class _Credentials { |
| + _Credentials(this.uri, this.realm, this.credentials); |
| + |
| + _AuthenticationScheme get scheme => credentials.scheme; |
| + |
| + bool applies(Uri uri, _AuthenticationScheme scheme) { |
| + if (scheme != null && credentials.scheme != scheme) return false; |
| + if (uri.domain != this.uri.domain) return false; |
| + int thisPort = |
| + this.uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : this.uri.port; |
| + int otherPort = uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : uri.port; |
| + if (otherPort != thisPort) return false; |
| + return uri.path.startsWith(this.uri.path); |
| + } |
| + |
| + |
| + void authorize(HttpClientRequest request) { |
| + credentials.authorize(this, request); |
| + } |
| + |
| + Uri uri; |
| + String realm; |
| + _HttpClientCredentials credentials; |
| + |
| + // Digest specific fields. |
| + String nonce; |
| + String algorithm; |
| + String qop; |
| +} |
| + |
| + |
| +class _HttpClientCredentials implements HttpClientCredentials { |
| + abstract _AuthenticationScheme get scheme; |
| + abstract void authorize(HttpClientRequest request); |
| +} |
| + |
| + |
| +class _HttpClientBasicCredentials implements HttpClientBasicCredentials { |
| + _HttpClientBasicCredentials(this.username, |
| + this.password); |
| + |
| + _AuthenticationScheme get scheme => _AuthenticationScheme.BASIC; |
| + |
| + void authorize(_Credentials _, HttpClientRequest request) { |
| + // There is no mentioning of username/password encoding in RFC |
| + // 2617. However there is an open draft for adding an additional |
| + // accept-charset parameter to the WWW-Authenticate and |
| + // Proxy-Authenticate headers, see |
| + // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For |
| + // now always use UTF-8 encoding. |
| + _UTF8Encoder encoder = new _UTF8Encoder(); |
| + String auth = |
| + CryptoUtils.bytesToBase64(encoder.encodeString( |
| + "$username:$password")); |
| + request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth"); |
| + } |
| + |
| + String username; |
| + String password; |
| +} |
| + |
| + |
| +class _HttpClientDigestCredentials implements HttpClientDigestCredentials { |
| + _HttpClientDigestCredentials(this.username, |
| + this.password); |
| + |
| + _AuthenticationScheme get scheme => _AuthenticationScheme.DIGEST; |
| + |
| + void authorize(_Credentials credentials, HttpClientRequest request) { |
| + // TODO(sgjesse): Implement!!! |
| + } |
| + |
| + String username; |
| + String password; |
| +} |
| + |
| + |
| + |
| class _RedirectInfo implements RedirectInfo { |
| const _RedirectInfo(int this.statusCode, |
| String this.method, |