| // Copyright (c) 2013, 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 dart._http; |
| |
| class _HttpHeaders implements HttpHeaders { |
| final Map<String, List<String>> _headers; |
| // The original header names keyed by the lowercase header names. |
| Map<String, String>? _originalHeaderNames; |
| final String protocolVersion; |
| |
| bool _mutable = true; // Are the headers currently mutable? |
| List<String>? _noFoldingHeaders; |
| |
| int _contentLength = -1; |
| bool _persistentConnection = true; |
| bool _chunkedTransferEncoding = false; |
| String? _host; |
| int? _port; |
| |
| final int _defaultPortForScheme; |
| |
| _HttpHeaders(this.protocolVersion, |
| {int defaultPortForScheme = HttpClient.defaultHttpPort, |
| _HttpHeaders? initialHeaders}) |
| : _headers = HashMap<String, List<String>>(), |
| _defaultPortForScheme = defaultPortForScheme { |
| if (initialHeaders != null) { |
| initialHeaders._headers.forEach((name, value) => _headers[name] = value); |
| _contentLength = initialHeaders._contentLength; |
| _persistentConnection = initialHeaders._persistentConnection; |
| _chunkedTransferEncoding = initialHeaders._chunkedTransferEncoding; |
| _host = initialHeaders._host; |
| _port = initialHeaders._port; |
| } |
| if (protocolVersion == "1.0") { |
| _persistentConnection = false; |
| _chunkedTransferEncoding = false; |
| } |
| } |
| |
| List<String>? operator [](String name) => _headers[_validateField(name)]; |
| |
| String? value(String name) { |
| name = _validateField(name); |
| List<String>? values = _headers[name]; |
| if (values == null) return null; |
| assert(values.isNotEmpty); |
| if (values.length > 1) { |
| throw HttpException("More than one value for header $name"); |
| } |
| return values[0]; |
| } |
| |
| void add(String name, value, {bool preserveHeaderCase = false}) { |
| _checkMutable(); |
| String lowercaseName = _validateField(name); |
| |
| if (preserveHeaderCase && name != lowercaseName) { |
| (_originalHeaderNames ??= {})[lowercaseName] = name; |
| } else { |
| _originalHeaderNames?.remove(lowercaseName); |
| } |
| _addAll(lowercaseName, value); |
| } |
| |
| void _addAll(String name, value) { |
| if (value is Iterable) { |
| for (var v in value) { |
| _add(name, _validateValue(v)); |
| } |
| } else { |
| _add(name, _validateValue(value)); |
| } |
| } |
| |
| void set(String name, Object value, {bool preserveHeaderCase = false}) { |
| _checkMutable(); |
| String lowercaseName = _validateField(name); |
| _headers.remove(lowercaseName); |
| _originalHeaderNames?.remove(lowercaseName); |
| if (lowercaseName == HttpHeaders.contentLengthHeader) { |
| _contentLength = -1; |
| } |
| if (lowercaseName == HttpHeaders.transferEncodingHeader) { |
| _chunkedTransferEncoding = false; |
| } |
| if (preserveHeaderCase && name != lowercaseName) { |
| (_originalHeaderNames ??= {})[lowercaseName] = name; |
| } |
| _addAll(lowercaseName, value); |
| } |
| |
| void remove(String name, Object value) { |
| _checkMutable(); |
| name = _validateField(name); |
| value = _validateValue(value); |
| List<String>? values = _headers[name]; |
| if (values != null) { |
| values.remove(_valueToString(value)); |
| if (values.isEmpty) { |
| _headers.remove(name); |
| _originalHeaderNames?.remove(name); |
| } |
| } |
| if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { |
| _chunkedTransferEncoding = false; |
| } |
| } |
| |
| void removeAll(String name) { |
| _checkMutable(); |
| name = _validateField(name); |
| _headers.remove(name); |
| _originalHeaderNames?.remove(name); |
| } |
| |
| void forEach(void Function(String name, List<String> values) action) { |
| _headers.forEach((String name, List<String> values) { |
| String originalName = _originalHeaderName(name); |
| action(originalName, values); |
| }); |
| } |
| |
| void noFolding(String name) { |
| name = _validateField(name); |
| (_noFoldingHeaders ??= <String>[]).add(name); |
| } |
| |
| bool get persistentConnection => _persistentConnection; |
| |
| void set persistentConnection(bool persistentConnection) { |
| _checkMutable(); |
| if (persistentConnection == _persistentConnection) return; |
| final originalName = _originalHeaderName(HttpHeaders.connectionHeader); |
| if (persistentConnection) { |
| if (protocolVersion == "1.1") { |
| remove(HttpHeaders.connectionHeader, "close"); |
| } else { |
| if (_contentLength < 0) { |
| throw HttpException( |
| "Trying to set 'Connection: Keep-Alive' on HTTP 1.0 headers with " |
| "no ContentLength"); |
| } |
| add(originalName, "keep-alive", preserveHeaderCase: true); |
| } |
| } else { |
| if (protocolVersion == "1.1") { |
| add(originalName, "close", preserveHeaderCase: true); |
| } else { |
| remove(HttpHeaders.connectionHeader, "keep-alive"); |
| } |
| } |
| _persistentConnection = persistentConnection; |
| } |
| |
| int get contentLength => _contentLength; |
| |
| void set contentLength(int contentLength) { |
| _checkMutable(); |
| if (protocolVersion == "1.0" && |
| persistentConnection && |
| contentLength == -1) { |
| throw HttpException( |
| "Trying to clear ContentLength on HTTP 1.0 headers with " |
| "'Connection: Keep-Alive' set"); |
| } |
| if (_contentLength == contentLength) return; |
| _contentLength = contentLength; |
| if (_contentLength >= 0) { |
| if (chunkedTransferEncoding) chunkedTransferEncoding = false; |
| _set(HttpHeaders.contentLengthHeader, contentLength.toString()); |
| } else { |
| _headers.remove(HttpHeaders.contentLengthHeader); |
| if (protocolVersion == "1.1") { |
| chunkedTransferEncoding = true; |
| } |
| } |
| } |
| |
| bool get chunkedTransferEncoding => _chunkedTransferEncoding; |
| |
| void set chunkedTransferEncoding(bool chunkedTransferEncoding) { |
| _checkMutable(); |
| if (chunkedTransferEncoding && protocolVersion == "1.0") { |
| throw HttpException( |
| "Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers"); |
| } |
| if (chunkedTransferEncoding == _chunkedTransferEncoding) return; |
| if (chunkedTransferEncoding) { |
| List<String>? values = _headers[HttpHeaders.transferEncodingHeader]; |
| if (values == null || !values.contains("chunked")) { |
| // Headers does not specify chunked encoding - add it if set. |
| _addValue(HttpHeaders.transferEncodingHeader, "chunked"); |
| } |
| contentLength = -1; |
| } else { |
| // Headers does specify chunked encoding - remove it if not set. |
| remove(HttpHeaders.transferEncodingHeader, "chunked"); |
| } |
| _chunkedTransferEncoding = chunkedTransferEncoding; |
| } |
| |
| 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(); |
| } |
| |
| DateTime? get ifModifiedSince { |
| List<String>? values = _headers[HttpHeaders.ifModifiedSinceHeader]; |
| if (values != null) { |
| assert(values.isNotEmpty); |
| try { |
| return HttpDate.parse(values[0]); |
| } on Exception { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| void set ifModifiedSince(DateTime? ifModifiedSince) { |
| _checkMutable(); |
| if (ifModifiedSince == null) { |
| _headers.remove(HttpHeaders.ifModifiedSinceHeader); |
| } else { |
| // Format "ifModifiedSince" header with date in Greenwich Mean Time (GMT). |
| String formatted = HttpDate.format(ifModifiedSince.toUtc()); |
| _set(HttpHeaders.ifModifiedSinceHeader, formatted); |
| } |
| } |
| |
| DateTime? get date { |
| List<String>? values = _headers[HttpHeaders.dateHeader]; |
| if (values != null) { |
| assert(values.isNotEmpty); |
| try { |
| return HttpDate.parse(values[0]); |
| } on Exception { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| void set date(DateTime? date) { |
| _checkMutable(); |
| if (date == null) { |
| _headers.remove(HttpHeaders.dateHeader); |
| } else { |
| // Format "DateTime" header with date in Greenwich Mean Time (GMT). |
| String formatted = HttpDate.format(date.toUtc()); |
| _set(HttpHeaders.dateHeader, formatted); |
| } |
| } |
| |
| DateTime? get expires { |
| List<String>? values = _headers[HttpHeaders.expiresHeader]; |
| if (values != null) { |
| assert(values.isNotEmpty); |
| try { |
| return HttpDate.parse(values[0]); |
| } on Exception { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| void set expires(DateTime? expires) { |
| _checkMutable(); |
| if (expires == null) { |
| _headers.remove(HttpHeaders.expiresHeader); |
| } else { |
| // Format "Expires" header with date in Greenwich Mean Time (GMT). |
| String formatted = HttpDate.format(expires.toUtc()); |
| _set(HttpHeaders.expiresHeader, formatted); |
| } |
| } |
| |
| ContentType? get contentType { |
| var values = _headers[HttpHeaders.contentTypeHeader]; |
| if (values != null) { |
| return ContentType.parse(values[0]); |
| } else { |
| return null; |
| } |
| } |
| |
| void set contentType(ContentType? contentType) { |
| _checkMutable(); |
| if (contentType == null) { |
| _headers.remove(HttpHeaders.contentTypeHeader); |
| } else { |
| _set(HttpHeaders.contentTypeHeader, contentType.toString()); |
| } |
| } |
| |
| void clear() { |
| _checkMutable(); |
| _headers.clear(); |
| _contentLength = -1; |
| _persistentConnection = true; |
| _chunkedTransferEncoding = false; |
| _host = null; |
| _port = null; |
| } |
| |
| // [name] must be a lower-case version of the name. |
| void _add(String name, value) { |
| assert(name == _validateField(name)); |
| // Use the length as index on what method to call. This is notable |
| // faster than computing hash and looking up in a hash-map. |
| switch (name.length) { |
| case 4: |
| if (HttpHeaders.dateHeader == name) { |
| _addDate(name, value); |
| return; |
| } |
| if (HttpHeaders.hostHeader == name) { |
| _addHost(name, value); |
| return; |
| } |
| break; |
| case 7: |
| if (HttpHeaders.expiresHeader == name) { |
| _addExpires(name, value); |
| return; |
| } |
| break; |
| case 10: |
| if (HttpHeaders.connectionHeader == name) { |
| _addConnection(name, value); |
| return; |
| } |
| break; |
| case 12: |
| if (HttpHeaders.contentTypeHeader == name) { |
| _addContentType(name, value); |
| return; |
| } |
| break; |
| case 14: |
| if (HttpHeaders.contentLengthHeader == name) { |
| _addContentLength(name, value); |
| return; |
| } |
| break; |
| case 17: |
| if (HttpHeaders.transferEncodingHeader == name) { |
| _addTransferEncoding(name, value); |
| return; |
| } |
| if (HttpHeaders.ifModifiedSinceHeader == name) { |
| _addIfModifiedSince(name, value); |
| return; |
| } |
| } |
| _addValue(name, value); |
| } |
| |
| void _addContentLength(String name, value) { |
| if (value is int) { |
| contentLength = value; |
| } else if (value is String) { |
| contentLength = int.parse(value); |
| } else { |
| throw HttpException("Unexpected type for header named $name"); |
| } |
| } |
| |
| void _addTransferEncoding(String name, value) { |
| if (value == "chunked") { |
| chunkedTransferEncoding = true; |
| } else { |
| _addValue(HttpHeaders.transferEncodingHeader, value); |
| } |
| } |
| |
| void _addDate(String name, value) { |
| if (value is DateTime) { |
| date = value; |
| } else if (value is String) { |
| _set(HttpHeaders.dateHeader, value); |
| } else { |
| throw HttpException("Unexpected type for header named $name"); |
| } |
| } |
| |
| void _addExpires(String name, value) { |
| if (value is DateTime) { |
| expires = value; |
| } else if (value is String) { |
| _set(HttpHeaders.expiresHeader, value); |
| } else { |
| throw HttpException("Unexpected type for header named $name"); |
| } |
| } |
| |
| void _addIfModifiedSince(String name, value) { |
| if (value is DateTime) { |
| ifModifiedSince = value; |
| } else if (value is String) { |
| _set(HttpHeaders.ifModifiedSinceHeader, value); |
| } else { |
| throw HttpException("Unexpected type for header named $name"); |
| } |
| } |
| |
| void _addHost(String name, value) { |
| if (value is String) { |
| // value.indexOf will only work for ipv4, ipv6 which has multiple : in its |
| // host part needs lastIndexOf |
| int pos = value.lastIndexOf(":"); |
| // According to RFC 3986, section 3.2.2, host part of ipv6 address must be |
| // enclosed by square brackets. |
| // https://serverfault.com/questions/205793/how-can-one-distinguish-the-host-and-the-port-in-an-ipv6-url |
| if (pos == -1 || value.startsWith("[") && value.endsWith("]")) { |
| _host = value; |
| _port = HttpClient.defaultHttpPort; |
| } else { |
| if (pos > 0) { |
| _host = value.substring(0, pos); |
| } else { |
| _host = null; |
| } |
| if (pos + 1 == value.length) { |
| _port = HttpClient.defaultHttpPort; |
| } else { |
| try { |
| _port = int.parse(value.substring(pos + 1)); |
| } on FormatException { |
| _port = null; |
| } |
| } |
| } |
| _set(HttpHeaders.hostHeader, value); |
| } else { |
| throw HttpException("Unexpected type for header named $name"); |
| } |
| } |
| |
| void _addConnection(String name, String value) { |
| var lowerCaseValue = value.toLowerCase(); |
| if (lowerCaseValue == 'close') { |
| _persistentConnection = false; |
| } else if (lowerCaseValue == 'keep-alive') { |
| _persistentConnection = true; |
| } |
| _addValue(name, value); |
| } |
| |
| void _addContentType(String name, value) { |
| _set(HttpHeaders.contentTypeHeader, value); |
| } |
| |
| void _addValue(String name, Object value) { |
| List<String> values = (_headers[name] ??= <String>[]); |
| values.add(_valueToString(value)); |
| } |
| |
| String _valueToString(Object value) { |
| if (value is DateTime) { |
| return HttpDate.format(value); |
| } else if (value is String) { |
| return value; // TODO(39784): no _validateValue? |
| } else { |
| return _validateValue(value.toString()) as String; |
| } |
| } |
| |
| void _set(String name, String value) { |
| assert(name == _validateField(name)); |
| _headers[name] = <String>[value]; |
| } |
| |
| void _checkMutable() { |
| if (!_mutable) throw HttpException("HTTP headers are not mutable"); |
| } |
| |
| void _updateHostHeader() { |
| var host = _host; |
| if (host != null) { |
| bool defaultPort = _port == null || _port == _defaultPortForScheme; |
| _set("host", defaultPort ? host : "$host:$_port"); |
| } |
| } |
| |
| bool _foldHeader(String name) { |
| if (name == HttpHeaders.setCookieHeader) return false; |
| var noFoldingHeaders = _noFoldingHeaders; |
| return noFoldingHeaders == null || !noFoldingHeaders.contains(name); |
| } |
| |
| void _finalize() { |
| _mutable = false; |
| } |
| |
| void _build(BytesBuilder builder, {bool skipZeroContentLength = false}) { |
| // per https://tools.ietf.org/html/rfc7230#section-3.3.2 |
| // A user agent SHOULD NOT send a |
| // Content-Length header field when the request message does not |
| // contain a payload body and the method semantics do not anticipate |
| // such a body. |
| String? ignoreHeader = _contentLength == 0 && skipZeroContentLength |
| ? HttpHeaders.contentLengthHeader |
| : null; |
| _headers.forEach((String name, List<String> values) { |
| if (ignoreHeader == name) { |
| return; |
| } |
| String originalName = _originalHeaderName(name); |
| bool fold = _foldHeader(name); |
| var nameData = originalName.codeUnits; |
| builder.add(nameData); |
| builder.addByte(_CharCode.COLON); |
| builder.addByte(_CharCode.SP); |
| for (int i = 0; i < values.length; i++) { |
| if (i > 0) { |
| if (fold) { |
| builder.addByte(_CharCode.COMMA); |
| builder.addByte(_CharCode.SP); |
| } else { |
| builder.addByte(_CharCode.CR); |
| builder.addByte(_CharCode.LF); |
| builder.add(nameData); |
| builder.addByte(_CharCode.COLON); |
| builder.addByte(_CharCode.SP); |
| } |
| } |
| builder.add(values[i].codeUnits); |
| } |
| builder.addByte(_CharCode.CR); |
| builder.addByte(_CharCode.LF); |
| }); |
| } |
| |
| String toString() { |
| StringBuffer sb = StringBuffer(); |
| _headers.forEach((String name, List<String> values) { |
| String originalName = _originalHeaderName(name); |
| sb |
| ..write(originalName) |
| ..write(": "); |
| bool fold = _foldHeader(name); |
| for (int i = 0; i < values.length; i++) { |
| if (i > 0) { |
| if (fold) { |
| sb.write(", "); |
| } else { |
| sb |
| ..write("\n") |
| ..write(originalName) |
| ..write(": "); |
| } |
| } |
| sb.write(values[i]); |
| } |
| sb.write("\n"); |
| }); |
| return sb.toString(); |
| } |
| |
| List<Cookie> _parseCookies() { |
| // Parse a Cookie header value according to the rules in RFC 6265. |
| var cookies = <Cookie>[]; |
| void parseCookieString(String s) { |
| int index = 0; |
| |
| bool done() => index == -1 || index == s.length; |
| |
| void skipWS() { |
| while (!done()) { |
| if (s[index] != " " && s[index] != "\t") return; |
| index++; |
| } |
| } |
| |
| String parseName() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == " " || s[index] == "\t" || s[index] == "=") break; |
| index++; |
| } |
| return s.substring(start, index); |
| } |
| |
| String parseValue() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == " " || s[index] == "\t" || s[index] == ";") break; |
| index++; |
| } |
| return s.substring(start, index); |
| } |
| |
| bool expect(String expected) { |
| if (done()) return false; |
| if (s[index] != expected) return false; |
| index++; |
| return true; |
| } |
| |
| while (!done()) { |
| skipWS(); |
| if (done()) return; |
| String name = parseName(); |
| skipWS(); |
| if (!expect("=")) { |
| index = s.indexOf(';', index); |
| continue; |
| } |
| skipWS(); |
| String value = parseValue(); |
| try { |
| cookies.add(_Cookie(name, value)); |
| } catch (_) { |
| // Skip it, invalid cookie data. |
| } |
| skipWS(); |
| if (done()) return; |
| if (!expect(";")) { |
| index = s.indexOf(';', index); |
| continue; |
| } |
| } |
| } |
| |
| List<String>? values = _headers[HttpHeaders.cookieHeader]; |
| if (values != null) { |
| for (var headerValue in values) { |
| parseCookieString(headerValue); |
| } |
| } |
| return cookies; |
| } |
| |
| static String _validateField(String field) { |
| for (var i = 0; i < field.length; i++) { |
| if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) { |
| throw FormatException( |
| "Invalid HTTP header field name: ${json.encode(field)}", field, i); |
| } |
| } |
| return field.toLowerCase(); |
| } |
| |
| static Object _validateValue(Object value) { |
| if (value is! String) return value; |
| for (var i = 0; i < (value).length; i++) { |
| if (!_HttpParser._isValueChar((value).codeUnitAt(i))) { |
| throw FormatException( |
| "Invalid HTTP header field value: ${json.encode(value)}", value, i); |
| } |
| } |
| return value; |
| } |
| |
| String _originalHeaderName(String name) { |
| return _originalHeaderNames?[name] ?? name; |
| } |
| } |
| |
| class _HeaderValue implements HeaderValue { |
| String _value; |
| Map<String, String?>? _parameters; |
| Map<String, String?>? _unmodifiableParameters; |
| |
| _HeaderValue([this._value = "", Map<String, String?> parameters = const {}]) { |
| // TODO(40614): Remove once non-nullability is sound. |
| Map<String, String?>? nullableParameters = parameters; |
| if (nullableParameters != null && nullableParameters.isNotEmpty) { |
| _parameters = HashMap<String, String?>.from(nullableParameters); |
| } |
| } |
| |
| static _HeaderValue parse(String value, |
| {String parameterSeparator = ";", |
| String? valueSeparator, |
| bool preserveBackslash = false}) { |
| // Parse the string. |
| var result = _HeaderValue(); |
| result._parse(value, parameterSeparator, valueSeparator, preserveBackslash); |
| return result; |
| } |
| |
| String get value => _value; |
| |
| Map<String, String?> _ensureParameters() => |
| _parameters ??= <String, String?>{}; |
| |
| Map<String, String?> get parameters => |
| _unmodifiableParameters ??= UnmodifiableMapView(_ensureParameters()); |
| |
| static bool _isToken(String token) { |
| if (token.isEmpty) { |
| return false; |
| } |
| final delimiters = "\"(),/:;<=>?@[]{}"; |
| for (int i = 0; i < token.length; i++) { |
| int codeUnit = token.codeUnitAt(i); |
| if (codeUnit <= 32 || codeUnit >= 127 || delimiters.contains(token[i])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| String toString() { |
| StringBuffer sb = StringBuffer(); |
| sb.write(_value); |
| var parameters = _parameters; |
| if (parameters != null && parameters.isNotEmpty) { |
| parameters.forEach((String name, String? value) { |
| sb |
| ..write("; ") |
| ..write(name); |
| if (value != null) { |
| sb.write("="); |
| if (_isToken(value)) { |
| sb.write(value); |
| } else { |
| sb.write('"'); |
| int start = 0; |
| for (int i = 0; i < value.length; i++) { |
| // Can use codeUnitAt here instead. |
| int codeUnit = value.codeUnitAt(i); |
| if (codeUnit == 92 /* backslash */ || |
| codeUnit == 34 /* double quote */) { |
| sb.write(value.substring(start, i)); |
| sb.write(r'\'); |
| start = i; |
| } |
| } |
| sb |
| ..write(value.substring(start)) |
| ..write('"'); |
| } |
| } |
| }); |
| } |
| return sb.toString(); |
| } |
| |
| void _parse(String s, String parameterSeparator, String? valueSeparator, |
| bool preserveBackslash) { |
| int index = 0; |
| |
| bool done() => index == s.length; |
| |
| void skipWS() { |
| while (!done()) { |
| if (s[index] != " " && s[index] != "\t") return; |
| index++; |
| } |
| } |
| |
| String parseValue() { |
| int start = index; |
| while (!done()) { |
| var char = s[index]; |
| if (char == " " || |
| char == "\t" || |
| char == valueSeparator || |
| char == parameterSeparator) break; |
| index++; |
| } |
| return s.substring(start, index); |
| } |
| |
| void expect(String expected) { |
| if (done() || s[index] != expected) { |
| throw HttpException("Failed to parse header value"); |
| } |
| index++; |
| } |
| |
| bool maybeExpect(String expected) { |
| if (done() || !s.startsWith(expected, index)) { |
| return false; |
| } |
| index++; |
| return true; |
| } |
| |
| void parseParameters() { |
| var parameters = _ensureParameters(); |
| |
| String parseParameterName() { |
| int start = index; |
| while (!done()) { |
| var char = s[index]; |
| if (char == " " || |
| char == "\t" || |
| char == "=" || |
| char == parameterSeparator || |
| char == valueSeparator) break; |
| index++; |
| } |
| return s.substring(start, index).toLowerCase(); |
| } |
| |
| String parseParameterValue() { |
| if (!done() && s[index] == "\"") { |
| // Parse quoted value. |
| StringBuffer sb = StringBuffer(); |
| index++; |
| while (!done()) { |
| var char = s[index]; |
| if (char == "\\") { |
| if (index + 1 == s.length) { |
| throw HttpException("Failed to parse header value"); |
| } |
| if (preserveBackslash && s[index + 1] != "\"") { |
| sb.write(char); |
| } |
| index++; |
| } else if (char == "\"") { |
| index++; |
| return sb.toString(); |
| } |
| char = s[index]; |
| sb.write(char); |
| index++; |
| } |
| throw HttpException("Failed to parse header value"); |
| } else { |
| // Parse non-quoted value. |
| return parseValue(); |
| } |
| } |
| |
| while (!done()) { |
| skipWS(); |
| if (done()) return; |
| String name = parseParameterName(); |
| skipWS(); |
| if (maybeExpect("=")) { |
| skipWS(); |
| String value = parseParameterValue(); |
| if (name == 'charset' && this is _ContentType) { |
| // Charset parameter of ContentTypes are always lower-case. |
| value = value.toLowerCase(); |
| } |
| parameters[name] = value; |
| skipWS(); |
| } else if (name.isNotEmpty) { |
| parameters[name] = null; |
| } |
| if (done()) return; |
| // TODO: Implement support for multi-valued parameters. |
| if (s[index] == valueSeparator) return; |
| expect(parameterSeparator); |
| } |
| } |
| |
| skipWS(); |
| _value = parseValue(); |
| skipWS(); |
| if (done()) return; |
| if (s[index] == valueSeparator) return; |
| maybeExpect(parameterSeparator); |
| parseParameters(); |
| } |
| } |
| |
| class _ContentType extends _HeaderValue implements ContentType { |
| String _primaryType = ""; |
| String _subType = ""; |
| |
| _ContentType(String primaryType, String subType, String? charset, |
| Map<String, String?> parameters) |
| : _primaryType = primaryType, |
| _subType = subType, |
| super("") { |
| // TODO(40614): Remove once non-nullability is sound. |
| String emptyIfNull(String? string) => string ?? ""; |
| _primaryType = emptyIfNull(_primaryType); |
| _subType = emptyIfNull(_subType); |
| _value = "$_primaryType/$_subType"; |
| // TODO(40614): Remove once non-nullability is sound. |
| Map<String, String?>? nullableParameters = parameters; |
| if (nullableParameters != null) { |
| var parameterMap = _ensureParameters(); |
| nullableParameters.forEach((String key, String? value) { |
| String lowerCaseKey = key.toLowerCase(); |
| if (lowerCaseKey == "charset") { |
| value = value?.toLowerCase(); |
| } |
| parameterMap[lowerCaseKey] = value; |
| }); |
| } |
| if (charset != null) { |
| _ensureParameters()["charset"] = charset.toLowerCase(); |
| } |
| } |
| |
| _ContentType._(); |
| |
| static _ContentType parse(String value) { |
| var result = _ContentType._(); |
| result._parse(value, ";", null, false); |
| int index = result._value.indexOf("/"); |
| if (index == -1 || index == (result._value.length - 1)) { |
| result._primaryType = result._value.trim().toLowerCase(); |
| } else { |
| result._primaryType = |
| result._value.substring(0, index).trim().toLowerCase(); |
| result._subType = result._value.substring(index + 1).trim().toLowerCase(); |
| } |
| return result; |
| } |
| |
| String get mimeType => '$primaryType/$subType'; |
| |
| String get primaryType => _primaryType; |
| |
| String get subType => _subType; |
| |
| String? get charset => parameters["charset"]; |
| } |
| |
| class _Cookie implements Cookie { |
| String _name; |
| String _value; |
| DateTime? expires; |
| int? maxAge; |
| String? domain; |
| String? _path; |
| bool httpOnly = false; |
| bool secure = false; |
| |
| _Cookie(String name, String value) |
| : _name = _validateName(name), |
| _value = _validateValue(value), |
| httpOnly = true; |
| |
| String get name => _name; |
| String get value => _value; |
| |
| String? get path => _path; |
| |
| set path(String? newPath) { |
| _validatePath(newPath); |
| _path = newPath; |
| } |
| |
| set name(String newName) { |
| _validateName(newName); |
| _name = newName; |
| } |
| |
| set value(String newValue) { |
| _validateValue(newValue); |
| _value = newValue; |
| } |
| |
| _Cookie.fromSetCookieValue(String value) |
| : _name = "", |
| _value = "" { |
| // Parse the 'set-cookie' header value. |
| _parseSetCookieValue(value); |
| } |
| |
| // Parse a 'set-cookie' header value according to the rules in RFC 6265. |
| void _parseSetCookieValue(String s) { |
| int index = 0; |
| |
| bool done() => index == s.length; |
| |
| String parseName() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == "=") break; |
| index++; |
| } |
| return s.substring(start, index).trim(); |
| } |
| |
| String parseValue() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == ";") break; |
| index++; |
| } |
| return s.substring(start, index).trim(); |
| } |
| |
| void parseAttributes() { |
| String parseAttributeName() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == "=" || s[index] == ";") break; |
| index++; |
| } |
| return s.substring(start, index).trim().toLowerCase(); |
| } |
| |
| String parseAttributeValue() { |
| int start = index; |
| while (!done()) { |
| if (s[index] == ";") break; |
| index++; |
| } |
| return s.substring(start, index).trim().toLowerCase(); |
| } |
| |
| while (!done()) { |
| String name = parseAttributeName(); |
| String value = ""; |
| if (!done() && s[index] == "=") { |
| index++; // Skip the = character. |
| value = parseAttributeValue(); |
| } |
| if (name == "expires") { |
| expires = HttpDate._parseCookieDate(value); |
| } else if (name == "max-age") { |
| maxAge = int.parse(value); |
| } else if (name == "domain") { |
| domain = value; |
| } else if (name == "path") { |
| path = value; |
| } else if (name == "httponly") { |
| httpOnly = true; |
| } else if (name == "secure") { |
| secure = true; |
| } |
| if (!done()) index++; // Skip the ; character |
| } |
| } |
| |
| _name = _validateName(parseName()); |
| if (done() || _name.isEmpty) { |
| throw HttpException("Failed to parse header value [$s]"); |
| } |
| index++; // Skip the = character. |
| _value = _validateValue(parseValue()); |
| if (done()) return; |
| index++; // Skip the ; character. |
| parseAttributes(); |
| } |
| |
| String toString() { |
| StringBuffer sb = StringBuffer(); |
| sb |
| ..write(_name) |
| ..write("=") |
| ..write(_value); |
| var expires = this.expires; |
| if (expires != null) { |
| sb |
| ..write("; Expires=") |
| ..write(HttpDate.format(expires)); |
| } |
| if (maxAge != null) { |
| sb |
| ..write("; Max-Age=") |
| ..write(maxAge); |
| } |
| if (domain != null) { |
| sb |
| ..write("; Domain=") |
| ..write(domain); |
| } |
| if (path != null) { |
| sb |
| ..write("; Path=") |
| ..write(path); |
| } |
| if (secure) sb.write("; Secure"); |
| if (httpOnly) sb.write("; HttpOnly"); |
| return sb.toString(); |
| } |
| |
| static String _validateName(String newName) { |
| const separators = [ |
| "(", |
| ")", |
| "<", |
| ">", |
| "@", |
| ",", |
| ";", |
| ":", |
| "\\", |
| '"', |
| "/", |
| "[", |
| "]", |
| "?", |
| "=", |
| "{", |
| "}" |
| ]; |
| if (newName == null) throw ArgumentError.notNull("name"); |
| for (int i = 0; i < newName.length; i++) { |
| int codeUnit = newName.codeUnitAt(i); |
| if (codeUnit <= 32 || |
| codeUnit >= 127 || |
| separators.contains(newName[i])) { |
| throw FormatException( |
| "Invalid character in cookie name, code unit: '$codeUnit'", |
| newName, |
| i); |
| } |
| } |
| return newName; |
| } |
| |
| static String _validateValue(String newValue) { |
| if (newValue == null) throw ArgumentError.notNull("value"); |
| // Per RFC 6265, consider surrounding "" as part of the value, but otherwise |
| // double quotes are not allowed. |
| int start = 0; |
| int end = newValue.length; |
| if (2 <= newValue.length && |
| newValue.codeUnits[start] == 0x22 && |
| newValue.codeUnits[end - 1] == 0x22) { |
| start++; |
| end--; |
| } |
| |
| for (int i = start; i < end; i++) { |
| int codeUnit = newValue.codeUnits[i]; |
| if (!(codeUnit == 0x21 || |
| (codeUnit >= 0x23 && codeUnit <= 0x2B) || |
| (codeUnit >= 0x2D && codeUnit <= 0x3A) || |
| (codeUnit >= 0x3C && codeUnit <= 0x5B) || |
| (codeUnit >= 0x5D && codeUnit <= 0x7E))) { |
| throw FormatException( |
| "Invalid character in cookie value, code unit: '$codeUnit'", |
| newValue, |
| i); |
| } |
| } |
| return newValue; |
| } |
| |
| static void _validatePath(String? path) { |
| if (path == null) return; |
| for (int i = 0; i < path.length; i++) { |
| int codeUnit = path.codeUnitAt(i); |
| // According to RFC 6265, semicolon and controls should not occur in the |
| // path. |
| // path-value = <any CHAR except CTLs or ";"> |
| // CTLs = %x00-1F / %x7F |
| if (codeUnit < 0x20 || codeUnit >= 0x7f || codeUnit == 0x3b /*;*/) { |
| throw FormatException( |
| "Invalid character in cookie path, code unit: '$codeUnit'"); |
| } |
| } |
| } |
| } |