mirror of
https://github.com/servo/servo
synced 2026-05-09 08:32:31 +02:00
Continuation of https://github.com/servo/servo/pull/42135, switch Error::Type and Error::Range to also use CStrings internally, as they are converted to CString for throwing JS exceptions (other get thrown as DomException object, which uses rust string internally). Changes in script crate are mechanical. Testing: Should be covered by WPT tests. Part of #42126 Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
549 lines
20 KiB
Rust
549 lines
20 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::cell::Cell;
|
||
use std::str::{self, FromStr};
|
||
|
||
use dom_struct::dom_struct;
|
||
use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue};
|
||
use js::rust::HandleObject;
|
||
use net_traits::fetch::headers::{
|
||
extract_mime_type, get_decode_and_split_header_value, get_value_from_header_list,
|
||
is_forbidden_method,
|
||
};
|
||
use net_traits::request::is_cors_safelisted_request_header;
|
||
use net_traits::trim_http_whitespace;
|
||
use script_bindings::cformat;
|
||
|
||
use crate::dom::bindings::cell::DomRefCell;
|
||
use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods};
|
||
use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
|
||
use crate::dom::bindings::iterable::Iterable;
|
||
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
|
||
use crate::dom::bindings::root::DomRoot;
|
||
use crate::dom::bindings::str::{ByteString, is_token};
|
||
use crate::dom::globalscope::GlobalScope;
|
||
use crate::script_runtime::CanGc;
|
||
|
||
#[dom_struct]
|
||
pub(crate) struct Headers {
|
||
reflector_: Reflector,
|
||
guard: Cell<Guard>,
|
||
#[ignore_malloc_size_of = "Defined in hyper"]
|
||
#[no_trace]
|
||
header_list: DomRefCell<HyperHeaders>,
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#concept-headers-guard>
|
||
#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
|
||
pub(crate) enum Guard {
|
||
Immutable,
|
||
Request,
|
||
RequestNoCors,
|
||
Response,
|
||
None,
|
||
}
|
||
|
||
impl Headers {
|
||
pub(crate) fn new_inherited() -> Headers {
|
||
Headers {
|
||
reflector_: Reflector::new(),
|
||
guard: Cell::new(Guard::None),
|
||
header_list: DomRefCell::new(HyperHeaders::new()),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
|
||
Self::new_with_proto(global, None, can_gc)
|
||
}
|
||
|
||
fn new_with_proto(
|
||
global: &GlobalScope,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Headers> {
|
||
reflect_dom_object_with_proto(Box::new(Headers::new_inherited()), global, proto, can_gc)
|
||
}
|
||
}
|
||
|
||
impl HeadersMethods<crate::DomTypeHolder> for Headers {
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers>
|
||
fn Constructor(
|
||
global: &GlobalScope,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
init: Option<HeadersInit>,
|
||
) -> Fallible<DomRoot<Headers>> {
|
||
let dom_headers_new = Headers::new_with_proto(global, proto, can_gc);
|
||
dom_headers_new.fill(init)?;
|
||
Ok(dom_headers_new)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#concept-headers-append>
|
||
fn Append(&self, name: ByteString, value: ByteString) -> ErrorResult {
|
||
// 1. Normalize value.
|
||
let value = trim_http_whitespace(&value);
|
||
|
||
// 2. If validating (name, value) for headers returns false, then return.
|
||
let Some((mut valid_name, valid_value)) =
|
||
self.validate_name_and_value(name, ByteString::new(value.into()))?
|
||
else {
|
||
return Ok(());
|
||
};
|
||
|
||
valid_name = valid_name.to_lowercase();
|
||
|
||
// 3. If headers’s guard is "request-no-cors":
|
||
if self.guard.get() == Guard::RequestNoCors {
|
||
// 3.1. Let temporaryValue be the result of getting name from headers’s header list.
|
||
let tmp_value = if let Some(mut value) =
|
||
get_value_from_header_list(&valid_name, &self.header_list.borrow())
|
||
{
|
||
// 3.3. Otherwise, set temporaryValue to temporaryValue, followed by 0x2C 0x20, followed by value.
|
||
value.extend(b", ");
|
||
value.extend(valid_value.to_vec());
|
||
value
|
||
} else {
|
||
// 3.2. If temporaryValue is null, then set temporaryValue to value.
|
||
valid_value.to_vec()
|
||
};
|
||
// 3.4. If (name, temporaryValue) is not a no-CORS-safelisted request-header, then return.
|
||
if !is_cors_safelisted_request_header(&valid_name, &tmp_value) {
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
// 4. Append (name, value) to headers’s header list.
|
||
match HeaderValue::from_bytes(&valid_value) {
|
||
Ok(value) => {
|
||
self.header_list
|
||
.borrow_mut()
|
||
.append(HeaderName::from_str(&valid_name).unwrap(), value);
|
||
},
|
||
Err(_) => {
|
||
// can't add the header, but we don't need to panic the browser over it
|
||
warn!(
|
||
"Servo thinks \"{:?}\" is a valid HTTP header value but HeaderValue doesn't.",
|
||
valid_value
|
||
);
|
||
},
|
||
};
|
||
|
||
// 5. If headers’s guard is "request-no-cors", then remove privileged no-CORS request-headers from headers.
|
||
if self.guard.get() == Guard::RequestNoCors {
|
||
self.remove_privileged_no_cors_request_headers();
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers-delete>
|
||
fn Delete(&self, name: ByteString) -> ErrorResult {
|
||
// Step 1 If validating (name, ``) for this returns false, then return.
|
||
let name_and_value = self.validate_name_and_value(name, ByteString::new(vec![]))?;
|
||
let Some((mut valid_name, _valid_value)) = name_and_value else {
|
||
return Ok(());
|
||
};
|
||
|
||
valid_name = valid_name.to_lowercase();
|
||
|
||
// Step 2 If this’s guard is "request-no-cors", name is not a no-CORS-safelisted request-header name,
|
||
// and name is not a privileged no-CORS request-header name, then return.
|
||
if self.guard.get() == Guard::RequestNoCors &&
|
||
!is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec())
|
||
{
|
||
return Ok(());
|
||
}
|
||
|
||
// 3. If this’s header list does not contain name, then return.
|
||
// 4. Delete name from this’s header list.
|
||
self.header_list.borrow_mut().remove(valid_name);
|
||
|
||
// 5. If this’s guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
|
||
if self.guard.get() == Guard::RequestNoCors {
|
||
self.remove_privileged_no_cors_request_headers();
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers-get>
|
||
fn Get(&self, name: ByteString) -> Fallible<Option<ByteString>> {
|
||
// 1. If name is not a header name, then throw a TypeError.
|
||
let valid_name = validate_name(name)?;
|
||
|
||
// 2. Return the result of getting name from this’s header list.
|
||
Ok(
|
||
get_value_from_header_list(&valid_name, &self.header_list.borrow())
|
||
.map(ByteString::new),
|
||
)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers-getsetcookie>
|
||
fn GetSetCookie(&self) -> Vec<ByteString> {
|
||
// 1. If this’s header list does not contain `Set-Cookie`, then return « ».
|
||
// 2. Return the values of all headers in this’s header list whose name is a
|
||
// byte-case-insensitive match for `Set-Cookie`, in order.
|
||
self.header_list
|
||
.borrow()
|
||
.get_all("set-cookie")
|
||
.iter()
|
||
.map(|v| ByteString::new(v.as_bytes().to_vec()))
|
||
.collect()
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers-has>
|
||
fn Has(&self, name: ByteString) -> Fallible<bool> {
|
||
// 1. If name is not a header name, then throw a TypeError.
|
||
let valid_name = validate_name(name)?;
|
||
// 2. Return true if this’s header list contains name; otherwise false.
|
||
Ok(self.header_list.borrow_mut().get(&valid_name).is_some())
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-headers-set>
|
||
fn Set(&self, name: ByteString, value: ByteString) -> Fallible<()> {
|
||
// 1. Normalize value
|
||
let value = trim_http_whitespace(&value);
|
||
|
||
// 2. If validating (name, value) for this returns false, then return.
|
||
let Some((mut valid_name, valid_value)) =
|
||
self.validate_name_and_value(name, ByteString::new(value.into()))?
|
||
else {
|
||
return Ok(());
|
||
};
|
||
valid_name = valid_name.to_lowercase();
|
||
|
||
// 3. If this’s guard is "request-no-cors" and (name, value) is not a
|
||
// no-CORS-safelisted request-header, then return.
|
||
if self.guard.get() == Guard::RequestNoCors &&
|
||
!is_cors_safelisted_request_header(&valid_name, &valid_value.to_vec())
|
||
{
|
||
return Ok(());
|
||
}
|
||
|
||
// 4. Set (name, value) in this’s header list.
|
||
// https://fetch.spec.whatwg.org/#concept-header-list-set
|
||
match HeaderValue::from_bytes(&valid_value) {
|
||
Ok(value) => {
|
||
self.header_list
|
||
.borrow_mut()
|
||
.insert(HeaderName::from_str(&valid_name).unwrap(), value);
|
||
},
|
||
Err(_) => {
|
||
// can't add the header, but we don't need to panic the browser over it
|
||
warn!(
|
||
"Servo thinks \"{:?}\" is a valid HTTP header value but HeaderValue doesn't.",
|
||
valid_value
|
||
);
|
||
},
|
||
};
|
||
|
||
// 5. If this’s guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
|
||
if self.guard.get() == Guard::RequestNoCors {
|
||
self.remove_privileged_no_cors_request_headers();
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Headers {
|
||
pub(crate) fn copy_from_headers(&self, headers: DomRoot<Headers>) -> ErrorResult {
|
||
for (name, value) in headers.header_list.borrow().iter() {
|
||
self.Append(
|
||
ByteString::new(Vec::from(name.as_str())),
|
||
ByteString::new(Vec::from(value.as_bytes())),
|
||
)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#concept-headers-fill>
|
||
pub(crate) fn fill(&self, filler: Option<HeadersInit>) -> ErrorResult {
|
||
match filler {
|
||
Some(HeadersInit::ByteStringSequenceSequence(v)) => {
|
||
for mut seq in v {
|
||
if seq.len() == 2 {
|
||
let val = seq.pop().unwrap();
|
||
let name = seq.pop().unwrap();
|
||
self.Append(name, val)?;
|
||
} else {
|
||
return Err(Error::Type(cformat!(
|
||
"Each header object must be a sequence of length 2 - found one with length {}",
|
||
seq.len()
|
||
)));
|
||
}
|
||
}
|
||
Ok(())
|
||
},
|
||
Some(HeadersInit::ByteStringByteStringRecord(m)) => {
|
||
for (key, value) in m.iter() {
|
||
self.Append(key.clone(), value.clone())?;
|
||
}
|
||
Ok(())
|
||
},
|
||
None => Ok(()),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn for_request(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
|
||
let headers_for_request = Headers::new(global, can_gc);
|
||
headers_for_request.guard.set(Guard::Request);
|
||
headers_for_request
|
||
}
|
||
|
||
pub(crate) fn for_response(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
|
||
let headers_for_response = Headers::new(global, can_gc);
|
||
headers_for_response.guard.set(Guard::Response);
|
||
headers_for_response
|
||
}
|
||
|
||
pub(crate) fn set_guard(&self, new_guard: Guard) {
|
||
self.guard.set(new_guard)
|
||
}
|
||
|
||
pub(crate) fn get_guard(&self) -> Guard {
|
||
self.guard.get()
|
||
}
|
||
|
||
pub(crate) fn set_headers(&self, hyper_headers: HyperHeaders) {
|
||
*self.header_list.borrow_mut() = hyper_headers;
|
||
}
|
||
|
||
pub(crate) fn get_headers_list(&self) -> HyperHeaders {
|
||
self.header_list.borrow_mut().clone()
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#concept-header-extract-mime-type>
|
||
pub(crate) fn extract_mime_type(&self) -> Vec<u8> {
|
||
extract_mime_type(&self.header_list.borrow()).unwrap_or_default()
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine>
|
||
pub(crate) fn sort_and_combine(&self) -> Vec<(String, Vec<u8>)> {
|
||
let borrowed_header_list = self.header_list.borrow();
|
||
let mut header_vec = vec![];
|
||
|
||
for name in borrowed_header_list.keys() {
|
||
let name = name.as_str();
|
||
if name == "set-cookie" {
|
||
for value in borrowed_header_list.get_all(name).iter() {
|
||
header_vec.push((name.to_owned(), value.as_bytes().to_vec()));
|
||
}
|
||
} else if let Some(value) = get_value_from_header_list(name, &borrowed_header_list) {
|
||
header_vec.push((name.to_owned(), value));
|
||
}
|
||
}
|
||
|
||
header_vec.sort_by(|a, b| a.0.cmp(&b.0));
|
||
header_vec
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#ref-for-privileged-no-cors-request-header-name>
|
||
pub(crate) fn remove_privileged_no_cors_request_headers(&self) {
|
||
// <https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name>
|
||
self.header_list.borrow_mut().remove("range");
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#headers-validate>
|
||
pub(crate) fn validate_name_and_value(
|
||
&self,
|
||
name: ByteString,
|
||
value: ByteString,
|
||
) -> Fallible<Option<(String, ByteString)>> {
|
||
// 1. If name is not a header name or value is not a header value, then throw a TypeError.
|
||
let valid_name = validate_name(name)?;
|
||
if !is_legal_header_value(&value) {
|
||
return Err(Error::Type(c"Header value is not valid".to_owned()));
|
||
}
|
||
// 2. If headers’s guard is "immutable", then throw a TypeError.
|
||
if self.guard.get() == Guard::Immutable {
|
||
return Err(Error::Type(c"Guard is immutable".to_owned()));
|
||
}
|
||
// 3. If headers’s guard is "request" and (name, value) is a forbidden request-header, then return false.
|
||
if self.guard.get() == Guard::Request && is_forbidden_request_header(&valid_name, &value) {
|
||
return Ok(None);
|
||
}
|
||
// 4. If headers’s guard is "response" and name is a forbidden response-header name, then return false.
|
||
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
|
||
return Ok(None);
|
||
}
|
||
|
||
Ok(Some((valid_name, value)))
|
||
}
|
||
}
|
||
|
||
impl Iterable for Headers {
|
||
type Key = ByteString;
|
||
type Value = ByteString;
|
||
|
||
fn get_iterable_length(&self) -> u32 {
|
||
let sorted_header_vec = self.sort_and_combine();
|
||
sorted_header_vec.len() as u32
|
||
}
|
||
|
||
fn get_value_at_index(&self, n: u32) -> ByteString {
|
||
let sorted_header_vec = self.sort_and_combine();
|
||
let value = sorted_header_vec[n as usize].1.clone();
|
||
ByteString::new(value)
|
||
}
|
||
|
||
fn get_key_at_index(&self, n: u32) -> ByteString {
|
||
let sorted_header_vec = self.sort_and_combine();
|
||
let key = sorted_header_vec[n as usize].0.clone();
|
||
ByteString::new(key.into_bytes().to_vec())
|
||
}
|
||
}
|
||
|
||
/// This function will internally convert `name` to lowercase for matching, so explicitly converting
|
||
/// before calling is not necessary
|
||
///
|
||
/// <https://fetch.spec.whatwg.org/#forbidden-request-header>
|
||
pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
|
||
let forbidden_header_names = [
|
||
"accept-charset",
|
||
"accept-encoding",
|
||
"access-control-request-headers",
|
||
"access-control-request-method",
|
||
"connection",
|
||
"content-length",
|
||
"cookie",
|
||
"cookie2",
|
||
"date",
|
||
"dnt",
|
||
"expect",
|
||
"host",
|
||
"keep-alive",
|
||
"origin",
|
||
"referer",
|
||
"set-cookie",
|
||
"te",
|
||
"trailer",
|
||
"transfer-encoding",
|
||
"upgrade",
|
||
"via",
|
||
// This list is defined in the fetch spec, however the draft spec for private-network-access
|
||
// proposes this additional forbidden name, which is currently included in WPT tests. See:
|
||
// https://wicg.github.io/private-network-access/#forbidden-header-names
|
||
"access-control-request-private-network",
|
||
];
|
||
|
||
// Step 1: If name is a byte-case-insensitive match for one of (forbidden_header_names), return
|
||
// true
|
||
let lowercase_name = name.to_lowercase();
|
||
|
||
if forbidden_header_names.contains(&lowercase_name.as_str()) {
|
||
return true;
|
||
}
|
||
|
||
let forbidden_header_prefixes = ["sec-", "proxy-"];
|
||
|
||
// Step 2: If name when byte-lowercased starts with `proxy-` or `sec-`, then return true.
|
||
if forbidden_header_prefixes
|
||
.iter()
|
||
.any(|prefix| lowercase_name.starts_with(prefix))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
let potentially_forbidden_header_names = [
|
||
"x-http-method",
|
||
"x-http-method-override",
|
||
"x-method-override",
|
||
];
|
||
|
||
// Step 3: If name is a byte-case-insensitive match for one of (potentially_forbidden_header_names)
|
||
if potentially_forbidden_header_names
|
||
.iter()
|
||
.any(|header| *header == lowercase_name)
|
||
{
|
||
// Step 3.1: Let parsedValues be the result of getting, decoding, and splitting value.
|
||
let parsed_values = get_decode_and_split_header_value(value.to_vec());
|
||
|
||
// Step 3.2: For each method of parsedValues: if the isomorphic encoding of method is a
|
||
// forbidden method, then return true.
|
||
return parsed_values
|
||
.iter()
|
||
.any(|s| is_forbidden_method(s.as_bytes()));
|
||
}
|
||
|
||
// Step 4: Return false.
|
||
false
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#forbidden-response-header-name>
|
||
fn is_forbidden_response_header(name: &str) -> bool {
|
||
// A forbidden response-header name is a header name that is a byte-case-insensitive match for one of
|
||
let name = name.to_ascii_lowercase();
|
||
matches!(name.as_str(), "set-cookie" | "set-cookie2")
|
||
}
|
||
|
||
fn validate_name(name: ByteString) -> Fallible<String> {
|
||
if !is_field_name(&name) {
|
||
return Err(Error::Type(c"Name is not valid".to_owned()));
|
||
}
|
||
match String::from_utf8(name.into()) {
|
||
Ok(ns) => Ok(ns),
|
||
_ => Err(Error::Type(c"Non-UTF8 header name found".to_owned())),
|
||
}
|
||
}
|
||
|
||
/// <http://tools.ietf.org/html/rfc7230#section-3.2>
|
||
fn is_field_name(name: &ByteString) -> bool {
|
||
is_token(name)
|
||
}
|
||
|
||
// As of December 2019, WHATWG has no formal grammar production for value;
|
||
// https://fetch.spec.whatg.org/#concept-header-value just says not to have
|
||
// newlines, nulls, or leading/trailing whitespace. It even allows
|
||
// octets that aren't a valid UTF-8 encoding, and WPT tests reflect this.
|
||
// The HeaderValue class does not fully reflect this, so headers
|
||
// containing bytes with values 1..31 or 127 can't be created, failing
|
||
// WPT tests but probably not affecting anything important on the real Internet.
|
||
/// <https://fetch.spec.whatg.org/#concept-header-value>
|
||
fn is_legal_header_value(value: &[u8]) -> bool {
|
||
let value_len = value.len();
|
||
if value_len == 0 {
|
||
return true;
|
||
}
|
||
match value[0] {
|
||
b' ' | b'\t' => return false,
|
||
_ => {},
|
||
};
|
||
match value[value_len - 1] {
|
||
b' ' | b'\t' => return false,
|
||
_ => {},
|
||
};
|
||
for &ch in value {
|
||
match ch {
|
||
b'\0' | b'\n' | b'\r' => return false,
|
||
_ => {},
|
||
}
|
||
}
|
||
true
|
||
// If accepting non-UTF8 header values causes breakage,
|
||
// removing the above "true" and uncommenting the below code
|
||
// would ameliorate it while still accepting most reasonable headers:
|
||
// match str::from_utf8(value) {
|
||
// Ok(_) => true,
|
||
// Err(_) => {
|
||
// warn!(
|
||
// "Rejecting spec-legal but non-UTF8 header value: {:?}",
|
||
// value
|
||
// );
|
||
// false
|
||
// },
|
||
// }
|
||
}
|
||
|
||
/// <https://tools.ietf.org/html/rfc5234#appendix-B.1>
|
||
pub(crate) fn is_vchar(x: u8) -> bool {
|
||
matches!(x, 0x21..=0x7E)
|
||
}
|
||
|
||
/// <http://tools.ietf.org/html/rfc7230#section-3.2.6>
|
||
pub(crate) fn is_obs_text(x: u8) -> bool {
|
||
matches!(x, 0x80..=0xFF)
|
||
}
|