blob: 2ccf23ac546c5bc929f4b1d51e1d20a8d505f7da [file] [log] [blame]
// 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'");
}
}
}
}