зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1805739 - vendor bhttp, provide bindings for binary http (RFC 9292) r=necko-reviewers,supply-chain-reviewers,valentin
Differential Revision: https://phabricator.services.mozilla.com/D164720
This commit is contained in:
Родитель
b3a467b55a
Коммит
de4fbd4afe
|
@ -422,6 +422,26 @@ dependencies = [
|
|||
"fxhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bhttp"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a561f43fe82923605345b977ebd5951126f0a1b4575e3c3d53e5954e5822de4"
|
||||
dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binary_http"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bhttp",
|
||||
"nserror",
|
||||
"nsstring",
|
||||
"thin-vec",
|
||||
"xpcom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
|
@ -2154,6 +2174,7 @@ dependencies = [
|
|||
"audioipc2-client",
|
||||
"audioipc2-server",
|
||||
"authenticator",
|
||||
"binary_http",
|
||||
"bitsdownload",
|
||||
"bookmark_sync",
|
||||
"cascade_bloom_filter",
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "binary_http"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nserror = { path = "../../../../xpcom/rust/nserror" }
|
||||
nsstring = { path = "../../../../xpcom/rust/nsstring" }
|
||||
bhttp = "0.2.3"
|
||||
thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
|
||||
xpcom = { path = "../../../../xpcom/rust/xpcom" }
|
|
@ -0,0 +1,24 @@
|
|||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
||||
*
|
||||
* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef _binary_http_h_
|
||||
#define _binary_http_h_
|
||||
|
||||
#include "nsISupportsUtils.h" // for nsresult, etc.
|
||||
|
||||
// {b43b3f73-8160-4ab2-9f5d-4129a9708081}
|
||||
#define NS_BINARY_HTTP_CID \
|
||||
{ \
|
||||
0xb43b3f73, 0x8160, 0x4ab2, { \
|
||||
0x9f, 0x5d, 0x41, 0x29, 0xa9, 0x70, 0x80, 0x81 \
|
||||
} \
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
nsresult binary_http_constructor(REFNSIID iid, void** result);
|
||||
};
|
||||
|
||||
#endif // _binary_http_h_
|
|
@ -0,0 +1,263 @@
|
|||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
extern crate bhttp;
|
||||
extern crate nserror;
|
||||
extern crate nsstring;
|
||||
extern crate thin_vec;
|
||||
#[macro_use]
|
||||
extern crate xpcom;
|
||||
|
||||
use bhttp::{Message, Mode};
|
||||
use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_UNEXPECTED, NS_OK};
|
||||
use nsstring::{nsACString, nsCString};
|
||||
use thin_vec::ThinVec;
|
||||
use xpcom::interfaces::{nsIBinaryHttpRequest, nsIBinaryHttpResponse};
|
||||
use xpcom::RefPtr;
|
||||
|
||||
enum HeaderComponent {
|
||||
Name,
|
||||
Value,
|
||||
}
|
||||
|
||||
// Extracts either the names or the values of a slice of header (name, value) pairs.
|
||||
fn extract_header_components(
|
||||
headers: &[(Vec<u8>, Vec<u8>)],
|
||||
component: HeaderComponent,
|
||||
) -> ThinVec<nsCString> {
|
||||
let mut header_components = ThinVec::with_capacity(headers.len());
|
||||
for (name, value) in headers {
|
||||
match component {
|
||||
HeaderComponent::Name => header_components.push(nsCString::from(name.clone())),
|
||||
HeaderComponent::Value => header_components.push(nsCString::from(value.clone())),
|
||||
}
|
||||
}
|
||||
header_components
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIBinaryHttpRequest), atomic)]
|
||||
struct BinaryHttpRequest {
|
||||
method: Vec<u8>,
|
||||
scheme: Vec<u8>,
|
||||
authority: Vec<u8>,
|
||||
path: Vec<u8>,
|
||||
headers: Vec<(Vec<u8>, Vec<u8>)>,
|
||||
content: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BinaryHttpRequest {
|
||||
xpcom_method!(get_method => GetMethod() -> nsACString);
|
||||
fn get_method(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(nsCString::from(self.method.clone()))
|
||||
}
|
||||
|
||||
xpcom_method!(get_scheme => GetScheme() -> nsACString);
|
||||
fn get_scheme(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(nsCString::from(self.scheme.clone()))
|
||||
}
|
||||
|
||||
xpcom_method!(get_authority => GetAuthority() -> nsACString);
|
||||
fn get_authority(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(nsCString::from(self.authority.clone()))
|
||||
}
|
||||
|
||||
xpcom_method!(get_path => GetPath() -> nsACString);
|
||||
fn get_path(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(nsCString::from(self.path.clone()))
|
||||
}
|
||||
|
||||
xpcom_method!(get_content => GetContent() -> ThinVec<u8>);
|
||||
fn get_content(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
Ok(self.content.clone().into_iter().collect())
|
||||
}
|
||||
|
||||
xpcom_method!(get_header_names => GetHeaderNames() -> ThinVec<nsCString>);
|
||||
fn get_header_names(&self) -> Result<ThinVec<nsCString>, nsresult> {
|
||||
Ok(extract_header_components(
|
||||
&self.headers,
|
||||
HeaderComponent::Name,
|
||||
))
|
||||
}
|
||||
|
||||
xpcom_method!(get_header_values => GetHeaderValues() -> ThinVec<nsCString>);
|
||||
fn get_header_values(&self) -> Result<ThinVec<nsCString>, nsresult> {
|
||||
Ok(extract_header_components(
|
||||
&self.headers,
|
||||
HeaderComponent::Value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIBinaryHttpResponse), atomic)]
|
||||
struct BinaryHttpResponse {
|
||||
status: u16,
|
||||
headers: Vec<(Vec<u8>, Vec<u8>)>,
|
||||
content: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BinaryHttpResponse {
|
||||
xpcom_method!(get_status => GetStatus() -> u16);
|
||||
fn get_status(&self) -> Result<u16, nsresult> {
|
||||
Ok(self.status)
|
||||
}
|
||||
|
||||
xpcom_method!(get_content => GetContent() -> ThinVec<u8>);
|
||||
fn get_content(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
Ok(self.content.clone().into_iter().collect())
|
||||
}
|
||||
|
||||
xpcom_method!(get_header_names => GetHeaderNames() -> ThinVec<nsCString>);
|
||||
fn get_header_names(&self) -> Result<ThinVec<nsCString>, nsresult> {
|
||||
Ok(extract_header_components(
|
||||
&self.headers,
|
||||
HeaderComponent::Name,
|
||||
))
|
||||
}
|
||||
|
||||
xpcom_method!(get_header_values => GetHeaderValues() -> ThinVec<nsCString>);
|
||||
fn get_header_values(&self) -> Result<ThinVec<nsCString>, nsresult> {
|
||||
Ok(extract_header_components(
|
||||
&self.headers,
|
||||
HeaderComponent::Value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIBinaryHttp), atomic)]
|
||||
struct BinaryHttp {}
|
||||
|
||||
impl BinaryHttp {
|
||||
xpcom_method!(encode_request => EncodeRequest(request: *const nsIBinaryHttpRequest) -> ThinVec<u8>);
|
||||
fn encode_request(&self, request: &nsIBinaryHttpRequest) -> Result<ThinVec<u8>, nsresult> {
|
||||
let mut method = nsCString::new();
|
||||
unsafe { request.GetMethod(&mut *method) }.to_result()?;
|
||||
let mut scheme = nsCString::new();
|
||||
unsafe { request.GetScheme(&mut *scheme) }.to_result()?;
|
||||
let mut authority = nsCString::new();
|
||||
unsafe { request.GetAuthority(&mut *authority) }.to_result()?;
|
||||
let mut path = nsCString::new();
|
||||
unsafe { request.GetPath(&mut *path) }.to_result()?;
|
||||
let mut message = Message::request(
|
||||
method.to_vec(),
|
||||
scheme.to_vec(),
|
||||
authority.to_vec(),
|
||||
path.to_vec(),
|
||||
);
|
||||
let mut header_names = ThinVec::new();
|
||||
unsafe { request.GetHeaderNames(&mut header_names) }.to_result()?;
|
||||
let mut header_values = ThinVec::with_capacity(header_names.len());
|
||||
unsafe { request.GetHeaderValues(&mut header_values) }.to_result()?;
|
||||
if header_names.len() != header_values.len() {
|
||||
return Err(NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
for (name, value) in header_names.iter().zip(header_values.iter()) {
|
||||
message.put_header(name.to_vec(), value.to_vec());
|
||||
}
|
||||
let mut content = ThinVec::new();
|
||||
unsafe { request.GetContent(&mut content) }.to_result()?;
|
||||
message.write_content(content);
|
||||
let mut encoded = ThinVec::new();
|
||||
message
|
||||
.write_bhttp(Mode::KnownLength, &mut encoded)
|
||||
.map_err(|_| NS_ERROR_FAILURE)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
xpcom_method!(decode_response => DecodeResponse(response: *const ThinVec<u8>) -> *const nsIBinaryHttpResponse);
|
||||
fn decode_response(
|
||||
&self,
|
||||
response: &ThinVec<u8>,
|
||||
) -> Result<RefPtr<nsIBinaryHttpResponse>, nsresult> {
|
||||
let decoded =
|
||||
Message::read_bhttp(&mut response.as_slice()).map_err(|_| NS_ERROR_UNEXPECTED)?;
|
||||
let status = decoded.control().status().ok_or(NS_ERROR_UNEXPECTED)?;
|
||||
let headers = decoded
|
||||
.header()
|
||||
.iter()
|
||||
.map(|field| (field.name().to_vec(), field.value().to_vec()))
|
||||
.collect();
|
||||
let content = decoded.content().to_vec();
|
||||
let binary_http_response = BinaryHttpResponse::allocate(InitBinaryHttpResponse {
|
||||
status,
|
||||
headers,
|
||||
content,
|
||||
});
|
||||
binary_http_response
|
||||
.query_interface::<nsIBinaryHttpResponse>()
|
||||
.ok_or(NS_ERROR_FAILURE)
|
||||
}
|
||||
|
||||
xpcom_method!(decode_request => DecodeRequest(request: *const ThinVec<u8>) -> *const nsIBinaryHttpRequest);
|
||||
fn decode_request(
|
||||
&self,
|
||||
request: &ThinVec<u8>,
|
||||
) -> Result<RefPtr<nsIBinaryHttpRequest>, nsresult> {
|
||||
let decoded =
|
||||
Message::read_bhttp(&mut request.as_slice()).map_err(|_| NS_ERROR_UNEXPECTED)?;
|
||||
let method = decoded
|
||||
.control()
|
||||
.method()
|
||||
.ok_or(NS_ERROR_UNEXPECTED)?
|
||||
.to_vec();
|
||||
let scheme = decoded
|
||||
.control()
|
||||
.scheme()
|
||||
.ok_or(NS_ERROR_UNEXPECTED)?
|
||||
.to_vec();
|
||||
// authority and path can be empty, in which case we return empty arrays
|
||||
let authority = decoded.control().authority().unwrap_or(&[]).to_vec();
|
||||
let path = decoded.control().path().unwrap_or(&[]).to_vec();
|
||||
let headers = decoded
|
||||
.header()
|
||||
.iter()
|
||||
.map(|field| (field.name().to_vec(), field.value().to_vec()))
|
||||
.collect();
|
||||
let content = decoded.content().to_vec();
|
||||
let binary_http_request = BinaryHttpRequest::allocate(InitBinaryHttpRequest {
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
headers,
|
||||
content,
|
||||
});
|
||||
binary_http_request
|
||||
.query_interface::<nsIBinaryHttpRequest>()
|
||||
.ok_or(NS_ERROR_FAILURE)
|
||||
}
|
||||
|
||||
xpcom_method!(encode_response => EncodeResponse(response: *const nsIBinaryHttpResponse) -> ThinVec<u8>);
|
||||
fn encode_response(&self, response: &nsIBinaryHttpResponse) -> Result<ThinVec<u8>, nsresult> {
|
||||
let mut status = 0;
|
||||
unsafe { response.GetStatus(&mut status) }.to_result()?;
|
||||
let mut message = Message::response(status);
|
||||
let mut header_names = ThinVec::new();
|
||||
unsafe { response.GetHeaderNames(&mut header_names) }.to_result()?;
|
||||
let mut header_values = ThinVec::with_capacity(header_names.len());
|
||||
unsafe { response.GetHeaderValues(&mut header_values) }.to_result()?;
|
||||
if header_names.len() != header_values.len() {
|
||||
return Err(NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
for (name, value) in header_values.iter().zip(header_names.iter()) {
|
||||
message.put_header(name.to_vec(), value.to_vec());
|
||||
}
|
||||
let mut content = ThinVec::new();
|
||||
unsafe { response.GetContent(&mut content) }.to_result()?;
|
||||
message.write_content(content);
|
||||
let mut encoded = ThinVec::new();
|
||||
message
|
||||
.write_bhttp(Mode::KnownLength, &mut encoded)
|
||||
.map_err(|_| NS_ERROR_FAILURE)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn binary_http_constructor(
|
||||
iid: *const xpcom::nsIID,
|
||||
result: *mut *mut xpcom::reexports::libc::c_void,
|
||||
) -> nsresult {
|
||||
let binary_http = BinaryHttp::allocate(InitBinaryHttp {});
|
||||
unsafe { binary_http.QueryInterface(iid, result) }
|
||||
}
|
|
@ -11,6 +11,12 @@ Classes = [
|
|||
'jsm': 'resource://gre/modules/WellKnownOpportunisticUtils.jsm',
|
||||
'constructor': 'WellKnownOpportunisticUtils',
|
||||
},
|
||||
{
|
||||
'cid': '{b43b3f73-8160-4ab2-9f5d-4129a9708081}',
|
||||
'contract_ids': ['@mozilla.org/network/binary-http;1'],
|
||||
'headers': ['/netwerk/protocol/http/binary_http/src/binary_http.h'],
|
||||
'legacy_constructor': 'binary_http_constructor',
|
||||
},
|
||||
{
|
||||
'cid': '{d581149e-3319-4563-b95e-46c64af5c4e8}',
|
||||
'contract_ids': ['@mozilla.org/network/oblivious-http;1'],
|
||||
|
|
|
@ -9,6 +9,7 @@ with Files("**"):
|
|||
|
||||
XPIDL_SOURCES += [
|
||||
"nsIBackgroundChannelRegistrar.idl",
|
||||
"nsIBinaryHttp.idl",
|
||||
"nsIEarlyHintObserver.idl",
|
||||
"nsIHttpActivityObserver.idl",
|
||||
"nsIHttpAuthenticableChannel.idl",
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
[scriptable, uuid(f6f899cc-683a-43da-9206-0eb0c09cc758)]
|
||||
interface nsIBinaryHttpRequest : nsISupports {
|
||||
readonly attribute ACString method;
|
||||
readonly attribute ACString scheme;
|
||||
readonly attribute ACString authority;
|
||||
readonly attribute ACString path;
|
||||
readonly attribute Array<ACString> headerNames;
|
||||
readonly attribute Array<ACString> headerValues;
|
||||
readonly attribute Array<octet> content;
|
||||
};
|
||||
|
||||
[scriptable, uuid(6ca85d9c-cdc5-45d4-9adc-005abedce9c9)]
|
||||
interface nsIBinaryHttpResponse : nsISupports {
|
||||
readonly attribute uint16_t status;
|
||||
readonly attribute Array<ACString> headerNames;
|
||||
readonly attribute Array<ACString> headerValues;
|
||||
readonly attribute Array<octet> content;
|
||||
};
|
||||
|
||||
// Implements Binary Representation of HTTP Messages (RFC 9292).
|
||||
// In normal operation, encodeRequest and decodeResponse are expected to be
|
||||
// used. For testing, decodeRequest and encodeResponse are available as well.
|
||||
// Thread safety: this interface may be used on any thread, but objects
|
||||
// returned by it are not inherently thread-safe and should only be used on the
|
||||
// threads they were created on.
|
||||
[scriptable, builtinclass, uuid(b43b3f73-8160-4ab2-9f5d-4129a9708081)]
|
||||
interface nsIBinaryHttp : nsISupports {
|
||||
Array<octet> encodeRequest(in nsIBinaryHttpRequest request);
|
||||
nsIBinaryHttpRequest decodeRequest(in Array<octet> request);
|
||||
|
||||
nsIBinaryHttpResponse decodeResponse(in Array<octet> response);
|
||||
Array<octet> encodeResponse(in nsIBinaryHttpResponse response);
|
||||
};
|
|
@ -447,3 +447,13 @@ function promiseAsyncOpen(chan) {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
function hexStringToBytes(hex) {
|
||||
let bytes = [];
|
||||
for (let hexByteStr of hex.split(/(..)/)) {
|
||||
if (hexByteStr.length) {
|
||||
bytes.push(parseInt(hexByteStr, 16));
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Unit tests for the binary http bindings.
|
||||
// Tests basic encoding and decoding of requests and responses.
|
||||
|
||||
function stringToBytes(str) {
|
||||
return Array.from(str, chr => chr.charCodeAt(0));
|
||||
}
|
||||
|
||||
function BinaryHttpRequest(
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
headerNames,
|
||||
headerValues,
|
||||
content
|
||||
) {
|
||||
this.method = method;
|
||||
this.scheme = scheme;
|
||||
this.authority = authority;
|
||||
this.path = path;
|
||||
this.headerNames = headerNames;
|
||||
this.headerValues = headerValues;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
BinaryHttpRequest.prototype = {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpRequest"]),
|
||||
};
|
||||
|
||||
function BinaryHttpResponse(status, headerNames, headerValues, content) {
|
||||
this.status = status;
|
||||
this.headerNames = headerNames;
|
||||
this.headerValues = headerValues;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
BinaryHttpResponse.prototype = {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
|
||||
};
|
||||
|
||||
function test_encode_request() {
|
||||
let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
|
||||
Ci.nsIBinaryHttp
|
||||
);
|
||||
let request = new BinaryHttpRequest(
|
||||
"GET",
|
||||
"https",
|
||||
"",
|
||||
"/hello.txt",
|
||||
["user-agent", "host", "accept-language"],
|
||||
[
|
||||
"curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3",
|
||||
"www.example.com",
|
||||
"en, mi",
|
||||
],
|
||||
[]
|
||||
);
|
||||
let encoded = bhttp.encodeRequest(request);
|
||||
// This example is from RFC 9292.
|
||||
let expected = hexStringToBytes(
|
||||
"0003474554056874747073000a2f6865" +
|
||||
"6c6c6f2e747874406c0a757365722d61" +
|
||||
"67656e74346375726c2f372e31362e33" +
|
||||
"206c69626375726c2f372e31362e3320" +
|
||||
"4f70656e53534c2f302e392e376c207a" +
|
||||
"6c69622f312e322e3304686f73740f77" +
|
||||
"77772e6578616d706c652e636f6d0f61" +
|
||||
"63636570742d6c616e67756167650665" +
|
||||
"6e2c206d690000"
|
||||
);
|
||||
deepEqual(encoded, expected);
|
||||
|
||||
let mismatchedHeaders = new BinaryHttpRequest(
|
||||
"GET",
|
||||
"https",
|
||||
"",
|
||||
"",
|
||||
["whoops-only-one-header-name"],
|
||||
["some-header-value", "some-other-header-value"],
|
||||
[]
|
||||
);
|
||||
// The implementation uses "NS_ERROR_INVALID_ARG", because that's an
|
||||
// appropriate description for the error. However, that is an alias to
|
||||
// "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so
|
||||
// that's what is tested for here.
|
||||
Assert.throws(
|
||||
() => bhttp.encodeRequest(mismatchedHeaders),
|
||||
/NS_ERROR_ILLEGAL_VALUE/
|
||||
);
|
||||
}
|
||||
|
||||
function test_decode_request() {
|
||||
let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
|
||||
Ci.nsIBinaryHttp
|
||||
);
|
||||
|
||||
// From RFC 9292.
|
||||
let encoded = hexStringToBytes(
|
||||
"0003474554056874747073000a2f6865" +
|
||||
"6c6c6f2e747874406c0a757365722d61" +
|
||||
"67656e74346375726c2f372e31362e33" +
|
||||
"206c69626375726c2f372e31362e3320" +
|
||||
"4f70656e53534c2f302e392e376c207a" +
|
||||
"6c69622f312e322e3304686f73740f77" +
|
||||
"77772e6578616d706c652e636f6d0f61" +
|
||||
"63636570742d6c616e67756167650665" +
|
||||
"6e2c206d690000"
|
||||
);
|
||||
let request = bhttp.decodeRequest(encoded);
|
||||
equal(request.method, "GET");
|
||||
equal(request.scheme, "https");
|
||||
equal(request.authority, "");
|
||||
equal(request.path, "/hello.txt");
|
||||
let expectedHeaderNames = ["user-agent", "host", "accept-language"];
|
||||
deepEqual(request.headerNames, expectedHeaderNames);
|
||||
let expectedHeaderValues = [
|
||||
"curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3",
|
||||
"www.example.com",
|
||||
"en, mi",
|
||||
];
|
||||
deepEqual(request.headerValues, expectedHeaderValues);
|
||||
deepEqual(request.content, []);
|
||||
|
||||
let garbage = hexStringToBytes("115f00ab64c0fa783fe4cb723eaa87fa78900a0b00");
|
||||
Assert.throws(() => bhttp.decodeRequest(garbage), /NS_ERROR_UNEXPECTED/);
|
||||
}
|
||||
|
||||
function test_decode_response() {
|
||||
let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
|
||||
Ci.nsIBinaryHttp
|
||||
);
|
||||
// From RFC 9292.
|
||||
let encoded = hexStringToBytes(
|
||||
"0340660772756e6e696e670a22736c65" +
|
||||
"657020313522004067046c696e6b233c" +
|
||||
"2f7374796c652e6373733e3b2072656c" +
|
||||
"3d7072656c6f61643b2061733d737479" +
|
||||
"6c65046c696e6b243c2f736372697074" +
|
||||
"2e6a733e3b2072656c3d7072656c6f61" +
|
||||
"643b2061733d7363726970740040c804" +
|
||||
"646174651d4d6f6e2c203237204a756c" +
|
||||
"20323030392031323a32383a35332047" +
|
||||
"4d540673657276657206417061636865" +
|
||||
"0d6c6173742d6d6f6469666965641d57" +
|
||||
"65642c203232204a756c203230303920" +
|
||||
"31393a31353a353620474d5404657461" +
|
||||
"671422333461613338372d642d313536" +
|
||||
"3865623030220d6163636570742d7261" +
|
||||
"6e6765730562797465730e636f6e7465" +
|
||||
"6e742d6c656e67746802353104766172" +
|
||||
"790f4163636570742d456e636f64696e" +
|
||||
"670c636f6e74656e742d747970650a74" +
|
||||
"6578742f706c61696e003348656c6c6f" +
|
||||
"20576f726c6421204d7920636f6e7465" +
|
||||
"6e7420696e636c756465732061207472" +
|
||||
"61696c696e672043524c462e0d0a0000"
|
||||
);
|
||||
let response = bhttp.decodeResponse(encoded);
|
||||
equal(response.status, 200);
|
||||
deepEqual(
|
||||
response.content,
|
||||
stringToBytes("Hello World! My content includes a trailing CRLF.\r\n")
|
||||
);
|
||||
let expectedHeaderNames = [
|
||||
"date",
|
||||
"server",
|
||||
"last-modified",
|
||||
"etag",
|
||||
"accept-ranges",
|
||||
"content-length",
|
||||
"vary",
|
||||
"content-type",
|
||||
];
|
||||
deepEqual(response.headerNames, expectedHeaderNames);
|
||||
let expectedHeaderValues = [
|
||||
"Mon, 27 Jul 2009 12:28:53 GMT",
|
||||
"Apache",
|
||||
"Wed, 22 Jul 2009 19:15:56 GMT",
|
||||
'"34aa387-d-1568eb00"',
|
||||
"bytes",
|
||||
"51",
|
||||
"Accept-Encoding",
|
||||
"text/plain",
|
||||
];
|
||||
deepEqual(response.headerValues, expectedHeaderValues);
|
||||
|
||||
let garbage = hexStringToBytes(
|
||||
"0367890084cb0ab03115fa0b4c2ea0fa783f7a87fa00"
|
||||
);
|
||||
Assert.throws(() => bhttp.decodeResponse(garbage), /NS_ERROR_UNEXPECTED/);
|
||||
}
|
||||
|
||||
function test_encode_response() {
|
||||
let response = new BinaryHttpResponse(
|
||||
418,
|
||||
["content-type"],
|
||||
["text/plain"],
|
||||
stringToBytes("I'm a teapot")
|
||||
);
|
||||
let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
|
||||
Ci.nsIBinaryHttp
|
||||
);
|
||||
let encoded = bhttp.encodeResponse(response);
|
||||
let expected = hexStringToBytes(
|
||||
"0141a2180a746578742f706c61696e0c" +
|
||||
"636f6e74656e742d747970650c49276d" +
|
||||
"206120746561706f7400"
|
||||
);
|
||||
deepEqual(encoded, expected);
|
||||
|
||||
let mismatchedHeaders = new BinaryHttpResponse(
|
||||
500,
|
||||
["some-header", "some-other-header"],
|
||||
["whoops-only-one-header-value"],
|
||||
[]
|
||||
);
|
||||
// The implementation uses "NS_ERROR_INVALID_ARG", because that's an
|
||||
// appropriate description for the error. However, that is an alias to
|
||||
// "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so
|
||||
// that's what is tested for here.
|
||||
Assert.throws(
|
||||
() => bhttp.encodeResponse(mismatchedHeaders),
|
||||
/NS_ERROR_ILLEGAL_VALUE/
|
||||
);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
test_encode_request();
|
||||
test_decode_request();
|
||||
test_encode_response();
|
||||
test_decode_response();
|
||||
}
|
|
@ -3,16 +3,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
function hexStringToBytes(hex) {
|
||||
let bytes = [];
|
||||
for (let hexByteStr of hex.split(/(..)/)) {
|
||||
if (hexByteStr.length) {
|
||||
bytes.push(parseInt(hexByteStr, 16));
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function test_known_config() {
|
||||
let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
|
||||
Ci.nsIObliviousHttp
|
||||
|
|
|
@ -682,6 +682,7 @@ head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js
|
|||
[test_coaleasing_h2_and_h3_connection.js]
|
||||
skip-if = os == 'android'
|
||||
run-sequentially = http3server
|
||||
[test_bhttp.js]
|
||||
[test_ohttp.js]
|
||||
[test_websocket_500k.js]
|
||||
skip-if = verify
|
||||
|
|
|
@ -113,6 +113,12 @@ criteria = "safe-to-deploy"
|
|||
version = "1.1.0"
|
||||
notes = "All code written or reviewed by Josh Stone."
|
||||
|
||||
[[audits.bhttp]]
|
||||
who = "Dana Keeler <dkeeler@mozilla.com>"
|
||||
criteria = "safe-to-deploy"
|
||||
version = "0.2.3"
|
||||
notes = "Mozilla-developed package, no unsafe code or powerful imports."
|
||||
|
||||
[[audits.bindgen]]
|
||||
who = "Emilio Cobos Álvarez <emilio@crisal.io>"
|
||||
criteria = "safe-to-deploy"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"files":{"Cargo.toml":"c0467a1857c040076f2a4be2da9f34b52bbceda73ace86ccef35acad98d0ae7b","README.md":"329e4ce4fbfabd037f6202418fbb381b6f6c77728dbe39545dae7c4f4265bae7","src/err.rs":"f0861ce8656de7b714c99fbfaea8e60e516bc8e85d4a4969c413525a1b1394b7","src/lib.rs":"112aae36a450530635dc36fcc920ebfeb52527ec1349d5a57461c36c6b5acc1e","src/parse.rs":"b4691c1e39e42ffa4a4d1a56f8c6e7b1b7a38d8cf5a1786fdcee4d7d48be1ff6","src/rw.rs":"3de0c74d4bc669918ebd5e0cfe8c134278224627db7e69c68d2c0675795adb86","tests/test.rs":"f0ed8cdfef30253ae34a9bcd3c3990ce4fda8084f3c4253e458b75ef5c1640cc"},"package":"9a561f43fe82923605345b977ebd5951126f0a1b4575e3c3d53e5954e5822de4"}
|
|
@ -0,0 +1,41 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "bhttp"
|
||||
version = "0.2.3"
|
||||
authors = ["Martin Thomson <mt@lowentropy.net>"]
|
||||
description = "Binary HTTP messages (draft-ietf-httpbis-binary-message)"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/martinthomson/ohttp"
|
||||
|
||||
[dependencies.url]
|
||||
version = "2"
|
||||
|
||||
[dev-dependencies.hex]
|
||||
version = "0.4"
|
||||
|
||||
[features]
|
||||
bhttp = [
|
||||
"read-bhttp",
|
||||
"write-bhttp",
|
||||
]
|
||||
default = ["bhttp"]
|
||||
http = [
|
||||
"read-http",
|
||||
"write-http",
|
||||
]
|
||||
read-bhttp = []
|
||||
read-http = []
|
||||
write-bhttp = []
|
||||
write-http = []
|
|
@ -0,0 +1,28 @@
|
|||
# Binary HTTP Messages
|
||||
|
||||
This is a rust implementation of [Binary HTTP
|
||||
Messages](https://httpwg.org/http-extensions/draft-ietf-httpbis-binary-message.html).
|
||||
|
||||
This work is undergoing active revision in the IETF and so are these
|
||||
implementations. Use at your own risk.
|
||||
|
||||
## Using
|
||||
|
||||
The API documentation is currently sparse, but the API is fairly small and
|
||||
descriptive.
|
||||
|
||||
The `bhttp` crate has the following features:
|
||||
|
||||
- `read-bhttp` enables parsing of binary HTTP messages. This is enabled by
|
||||
default.
|
||||
|
||||
- `write-bhttp` enables writing of binary HTTP messages. This is enabled by
|
||||
default.
|
||||
|
||||
- `read-http` enables a simple HTTP/1.1 message parser. This parser is fairly
|
||||
basic and is not recommended for production use. Getting an HTTP/1.1 parser
|
||||
right is a massive enterprise; this one only does the basics. This is
|
||||
disabled by default.
|
||||
|
||||
- `write-http` enables writing of HTTP/1.1 messages. This is disabled by
|
||||
default.
|
|
@ -0,0 +1,70 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// A request used the CONNECT method.
|
||||
ConnectUnsupported,
|
||||
/// A field contained invalid Unicode.
|
||||
CharacterEncoding(std::string::FromUtf8Error),
|
||||
/// A field contained an integer value that was out of range.
|
||||
IntRange(std::num::TryFromIntError),
|
||||
/// The mode of the message was invalid.
|
||||
InvalidMode,
|
||||
/// An IO error.
|
||||
Io(std::io::Error),
|
||||
/// A field or line was missing a necessary character.
|
||||
Missing(u8),
|
||||
/// A URL was missing a key component.
|
||||
MissingUrlComponent,
|
||||
/// An obs-fold line was the first line of a field section.
|
||||
ObsFold,
|
||||
/// A field contained a non-integer value.
|
||||
ParseInt(std::num::ParseIntError),
|
||||
/// A field was truncated.
|
||||
Truncated,
|
||||
/// A message included the Upgrade field.
|
||||
UpgradeUnsupported,
|
||||
/// A URL could not be parsed into components.
|
||||
UrlParse(url::ParseError),
|
||||
}
|
||||
|
||||
macro_rules! forward_errors {
|
||||
{$($(#[$a:meta])* $t:path => $v:ident),* $(,)?} => {
|
||||
$(
|
||||
impl From<$t> for Error {
|
||||
fn from(e: $t) -> Self {
|
||||
Self::$v(e)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
$( $(#[$a])* Self::$v(e) => Some(e), )*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
forward_errors! {
|
||||
std::io::Error => Io,
|
||||
std::string::FromUtf8Error => CharacterEncoding,
|
||||
std::num::ParseIntError => ParseInt,
|
||||
std::num::TryFromIntError => IntRange,
|
||||
url::ParseError => UrlParse,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "read-http",
|
||||
feature = "write-http",
|
||||
feature = "read-bhttp",
|
||||
feature = "write-bhttp"
|
||||
))]
|
||||
pub type Res<T> = Result<T, Error>;
|
|
@ -0,0 +1,762 @@
|
|||
#![deny(warnings, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)] // Too lazy to document these.
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
use std::convert::TryFrom;
|
||||
#[cfg(any(
|
||||
feature = "read-http",
|
||||
feature = "write-http",
|
||||
feature = "read-bhttp",
|
||||
feature = "write-bhttp"
|
||||
))]
|
||||
use std::io;
|
||||
#[cfg(feature = "read-http")]
|
||||
use url::Url;
|
||||
|
||||
mod err;
|
||||
mod parse;
|
||||
#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))]
|
||||
mod rw;
|
||||
|
||||
pub use err::Error;
|
||||
#[cfg(any(
|
||||
feature = "read-http",
|
||||
feature = "write-http",
|
||||
feature = "read-bhttp",
|
||||
feature = "write-bhttp"
|
||||
))]
|
||||
use err::Res;
|
||||
#[cfg(feature = "read-http")]
|
||||
use parse::{downcase, is_ows, read_line, split_at, COLON, SEMICOLON, SLASH, SP};
|
||||
use parse::{index_of, trim_ows, COMMA};
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
use rw::{read_varint, read_vec};
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
use rw::{write_len, write_varint, write_vec};
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
const CONTENT_LENGTH: &[u8] = b"content-length";
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
const COOKIE: &[u8] = b"cookie";
|
||||
const TRANSFER_ENCODING: &[u8] = b"transfer-encoding";
|
||||
const CHUNKED: &[u8] = b"chunked";
|
||||
|
||||
pub type StatusCode = u16;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))]
|
||||
pub enum Mode {
|
||||
KnownLength,
|
||||
IndefiniteLength,
|
||||
}
|
||||
|
||||
pub struct Field {
|
||||
name: Vec<u8>,
|
||||
value: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
#[must_use]
|
||||
pub fn new(name: Vec<u8>, value: Vec<u8>) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &[u8] {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &[u8] {
|
||||
&self.value
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-http")]
|
||||
pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
w.write_all(&self.name)?;
|
||||
w.write_all(b": ")?;
|
||||
w.write_all(&self.value)?;
|
||||
w.write_all(b"\r\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
write_vec(&self.name, w)?;
|
||||
write_vec(&self.value, w)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn obs_fold(&mut self, extra: &[u8]) {
|
||||
self.value.push(SP);
|
||||
self.value.extend(trim_ows(extra));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FieldSection(Vec<Field>);
|
||||
impl FieldSection {
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Gets the value from the first instance of the field.
|
||||
#[must_use]
|
||||
pub fn get(&self, n: &[u8]) -> Option<&[u8]> {
|
||||
for f in &self.0 {
|
||||
if &f.name[..] == n {
|
||||
return Some(&f.value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn put(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
|
||||
self.0.push(Field::new(name.into(), value.into()));
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Field> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fields(&self) -> &[Field] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_chunked(&self) -> bool {
|
||||
// Look at the last symbol in Transfer-Encoding.
|
||||
// This is very primitive decoding; structured field this is not.
|
||||
if let Some(te) = self.get(TRANSFER_ENCODING) {
|
||||
let mut slc = te;
|
||||
while let Some(i) = index_of(COMMA, slc) {
|
||||
slc = trim_ows(&slc[i + 1..]);
|
||||
}
|
||||
slc == CHUNKED
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// As required by the HTTP specification, remove the Connection header
|
||||
/// field, everything it refers to, and a few extra fields.
|
||||
#[cfg(feature = "read-http")]
|
||||
fn strip_connection_headers(&mut self) {
|
||||
const CONNECTION: &[u8] = b"connection";
|
||||
const PROXY_CONNECTION: &[u8] = b"proxy-connection";
|
||||
const SHOULD_REMOVE: &[&[u8]] = &[
|
||||
CONNECTION,
|
||||
PROXY_CONNECTION,
|
||||
b"keep-alive",
|
||||
b"te",
|
||||
b"trailer",
|
||||
b"transfer-encoding",
|
||||
b"upgrade",
|
||||
];
|
||||
let mut listed = Vec::new();
|
||||
let mut track = |n| {
|
||||
let mut name = Vec::from(trim_ows(n));
|
||||
downcase(&mut name);
|
||||
if !listed.contains(&name) {
|
||||
listed.push(name);
|
||||
}
|
||||
};
|
||||
|
||||
for f in self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|f| f.name() == CONNECTION || f.name == PROXY_CONNECTION)
|
||||
{
|
||||
let mut v = f.value();
|
||||
while let Some(i) = index_of(COMMA, v) {
|
||||
track(&v[..i]);
|
||||
v = &v[i + 1..];
|
||||
}
|
||||
track(v);
|
||||
}
|
||||
|
||||
self.0.retain(|f| {
|
||||
!SHOULD_REMOVE.contains(&f.name()) && listed.iter().all(|x| &x[..] != f.name())
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
fn parse_line(fields: &mut Vec<Field>, line: Vec<u8>) -> Res<()> {
|
||||
// obs-fold is helpful in specs, so support it here too
|
||||
let f = if is_ows(line[0]) {
|
||||
let mut e = fields.pop().ok_or(Error::ObsFold)?;
|
||||
e.obs_fold(&line);
|
||||
e
|
||||
} else if let Some((n, v)) = split_at(COLON, line) {
|
||||
let mut name = Vec::from(trim_ows(&n));
|
||||
downcase(&mut name);
|
||||
let value = Vec::from(trim_ows(&v));
|
||||
Field::new(name, value)
|
||||
} else {
|
||||
return Err(Error::Missing(COLON));
|
||||
};
|
||||
fields.push(f);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn read_http(r: &mut impl io::BufRead) -> Res<Self> {
|
||||
let mut fields = Vec::new();
|
||||
loop {
|
||||
let line = read_line(r)?;
|
||||
if trim_ows(&line).is_empty() {
|
||||
return Ok(Self(fields));
|
||||
}
|
||||
Self::parse_line(&mut fields, line)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
fn read_bhttp_fields(terminator: bool, r: &mut impl io::BufRead) -> Res<Vec<Field>> {
|
||||
let mut fields = Vec::new();
|
||||
let mut cookie_index: Option<usize> = None;
|
||||
loop {
|
||||
if let Some(n) = read_vec(r)? {
|
||||
if n.is_empty() {
|
||||
if terminator {
|
||||
return Ok(fields);
|
||||
}
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let mut v = read_vec(r)?.ok_or(Error::Truncated)?;
|
||||
if n == COOKIE {
|
||||
if let Some(i) = &cookie_index {
|
||||
fields[*i].value.extend_from_slice(b"; ");
|
||||
fields[*i].value.append(&mut v);
|
||||
continue;
|
||||
}
|
||||
cookie_index = Some(fields.len());
|
||||
}
|
||||
fields.push(Field::new(n, v));
|
||||
} else if terminator {
|
||||
return Err(Error::Truncated);
|
||||
} else {
|
||||
return Ok(fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
pub fn read_bhttp(mode: Mode, r: &mut impl io::BufRead) -> Res<Self> {
|
||||
let fields = if mode == Mode::KnownLength {
|
||||
if let Some(buf) = read_vec(r)? {
|
||||
Self::read_bhttp_fields(false, &mut io::BufReader::new(&buf[..]))?
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
Self::read_bhttp_fields(true, r)?
|
||||
};
|
||||
Ok(Self(fields))
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
fn write_bhttp_headers(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
for f in &self.0 {
|
||||
f.write_bhttp(w)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
|
||||
if mode == Mode::KnownLength {
|
||||
let mut buf = Vec::new();
|
||||
self.write_bhttp_headers(&mut buf)?;
|
||||
write_vec(&buf, w)?;
|
||||
} else {
|
||||
self.write_bhttp_headers(w)?;
|
||||
write_len(0, w)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-http")]
|
||||
pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
for f in &self.0 {
|
||||
f.write_http(w)?;
|
||||
}
|
||||
w.write_all(b"\r\n")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ControlData {
|
||||
Request {
|
||||
method: Vec<u8>,
|
||||
scheme: Vec<u8>,
|
||||
authority: Vec<u8>,
|
||||
path: Vec<u8>,
|
||||
},
|
||||
Response(StatusCode),
|
||||
}
|
||||
|
||||
impl ControlData {
|
||||
#[must_use]
|
||||
pub fn is_request(&self) -> bool {
|
||||
matches!(self, Self::Request { .. })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn method(&self) -> Option<&[u8]> {
|
||||
if let Self::Request { method, .. } = self {
|
||||
Some(method)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn scheme(&self) -> Option<&[u8]> {
|
||||
if let Self::Request { scheme, .. } = self {
|
||||
Some(scheme)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn authority(&self) -> Option<&[u8]> {
|
||||
if let Self::Request { authority, .. } = self {
|
||||
if authority.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(authority)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn path(&self) -> Option<&[u8]> {
|
||||
if let Self::Request { path, .. } = self {
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(path)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn status(&self) -> Option<StatusCode> {
|
||||
if let Self::Response(code) = self {
|
||||
Some(*code)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn read_http(line: Vec<u8>) -> Res<Self> {
|
||||
// request-line = method SP request-target SP HTTP-version
|
||||
// status-line = HTTP-version SP status-code SP [reason-phrase]
|
||||
let (a, r) = split_at(SP, line).ok_or(Error::Missing(SP))?;
|
||||
let (b, _) = split_at(SP, r).ok_or(Error::Missing(SP))?;
|
||||
if index_of(SLASH, &a).is_some() {
|
||||
// Probably a response, so treat it as such.
|
||||
let status_str = String::from_utf8(b)?;
|
||||
let code = status_str.parse::<u16>()?;
|
||||
Ok(Self::Response(code))
|
||||
} else if index_of(COLON, &b).is_some() {
|
||||
// Now try to parse the URL.
|
||||
let url_str = String::from_utf8(b)?;
|
||||
let parsed = Url::parse(&url_str)?;
|
||||
let authority = parsed.host_str().map_or_else(String::new, |host| {
|
||||
let mut authority = String::from(host);
|
||||
if let Some(port) = parsed.port() {
|
||||
authority.push(':');
|
||||
authority.push_str(&port.to_string());
|
||||
}
|
||||
authority
|
||||
});
|
||||
let mut path = String::from(parsed.path());
|
||||
if let Some(q) = parsed.query() {
|
||||
path.push('?');
|
||||
path.push_str(q);
|
||||
}
|
||||
Ok(Self::Request {
|
||||
method: a,
|
||||
scheme: Vec::from(parsed.scheme().as_bytes()),
|
||||
authority: Vec::from(authority.as_bytes()),
|
||||
path: Vec::from(path.as_bytes()),
|
||||
})
|
||||
} else {
|
||||
if a == b"CONNECT" {
|
||||
return Err(Error::ConnectUnsupported);
|
||||
}
|
||||
Ok(Self::Request {
|
||||
method: a,
|
||||
scheme: Vec::from(&b"https"[..]),
|
||||
authority: Vec::new(),
|
||||
path: b,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
pub fn read_bhttp(request: bool, r: &mut impl io::BufRead) -> Res<Self> {
|
||||
let v = if request {
|
||||
let method = read_vec(r)?.ok_or(Error::Truncated)?;
|
||||
let scheme = read_vec(r)?.ok_or(Error::Truncated)?;
|
||||
let authority = read_vec(r)?.ok_or(Error::Truncated)?;
|
||||
let path = read_vec(r)?.ok_or(Error::Truncated)?;
|
||||
Self::Request {
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
}
|
||||
} else {
|
||||
Self::Response(u16::try_from(read_varint(r)?.ok_or(Error::Truncated)?)?)
|
||||
};
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// If this is an informational response.
|
||||
#[cfg(any(feature = "read-bhttp", feature = "read-http"))]
|
||||
#[must_use]
|
||||
fn informational(&self) -> Option<StatusCode> {
|
||||
match self {
|
||||
Self::Response(v) if *v >= 100 && *v < 200 => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
#[must_use]
|
||||
fn code(&self, mode: Mode) -> u64 {
|
||||
match (self, mode) {
|
||||
(Self::Request { .. }, Mode::KnownLength) => 0,
|
||||
(Self::Response(_), Mode::KnownLength) => 1,
|
||||
(Self::Request { .. }, Mode::IndefiniteLength) => 2,
|
||||
(Self::Response(_), Mode::IndefiniteLength) => 3,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
match self {
|
||||
Self::Request {
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
} => {
|
||||
write_vec(method, w)?;
|
||||
write_vec(scheme, w)?;
|
||||
write_vec(authority, w)?;
|
||||
write_vec(path, w)?;
|
||||
}
|
||||
Self::Response(status) => write_varint(*status, w)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-http")]
|
||||
pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
match self {
|
||||
Self::Request {
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
} => {
|
||||
w.write_all(method)?;
|
||||
w.write_all(b" ")?;
|
||||
if !authority.is_empty() {
|
||||
w.write_all(scheme)?;
|
||||
w.write_all(b"://")?;
|
||||
w.write_all(authority)?;
|
||||
}
|
||||
w.write_all(path)?;
|
||||
w.write_all(b" HTTP/1.1\r\n")?;
|
||||
}
|
||||
Self::Response(status) => {
|
||||
let buf = format!("HTTP/1.1 {} Reason\r\n", *status);
|
||||
w.write_all(buf.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InformationalResponse {
|
||||
status: StatusCode,
|
||||
fields: FieldSection,
|
||||
}
|
||||
|
||||
impl InformationalResponse {
|
||||
#[must_use]
|
||||
pub fn new(status: StatusCode, fields: FieldSection) -> Self {
|
||||
Self { status, fields }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fields(&self) -> &FieldSection {
|
||||
&self.fields
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
|
||||
write_varint(self.status, w)?;
|
||||
self.fields.write_bhttp(mode, w)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Message {
|
||||
informational: Vec<InformationalResponse>,
|
||||
control: ControlData,
|
||||
header: FieldSection,
|
||||
content: Vec<u8>,
|
||||
trailer: FieldSection,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
#[must_use]
|
||||
pub fn request(method: Vec<u8>, scheme: Vec<u8>, authority: Vec<u8>, path: Vec<u8>) -> Self {
|
||||
Self {
|
||||
informational: Vec::new(),
|
||||
control: ControlData::Request {
|
||||
method,
|
||||
scheme,
|
||||
authority,
|
||||
path,
|
||||
},
|
||||
header: FieldSection::default(),
|
||||
content: Vec::new(),
|
||||
trailer: FieldSection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn response(status: StatusCode) -> Self {
|
||||
Self {
|
||||
informational: Vec::new(),
|
||||
control: ControlData::Response(status),
|
||||
header: FieldSection::default(),
|
||||
content: Vec::new(),
|
||||
trailer: FieldSection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_header(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
|
||||
self.header.put(name, value);
|
||||
}
|
||||
|
||||
pub fn put_trailer(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) {
|
||||
self.trailer.put(name, value);
|
||||
}
|
||||
|
||||
pub fn write_content(&mut self, d: impl AsRef<[u8]>) {
|
||||
self.content.extend_from_slice(d.as_ref());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn informational(&self) -> &[InformationalResponse] {
|
||||
&self.informational
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn control(&self) -> &ControlData {
|
||||
&self.control
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn header(&self) -> &FieldSection {
|
||||
&self.header
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn content(&self) -> &[u8] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn trailer(&self) -> &FieldSection {
|
||||
&self.trailer
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
fn read_chunked(r: &mut impl io::BufRead) -> Res<Vec<u8>> {
|
||||
let mut content = Vec::new();
|
||||
loop {
|
||||
let mut line = read_line(r)?;
|
||||
if let Some(i) = index_of(SEMICOLON, &line) {
|
||||
std::mem::drop(line.split_off(i));
|
||||
}
|
||||
let count_str = String::from_utf8(line)?;
|
||||
let count = usize::from_str_radix(&count_str, 16)?;
|
||||
if count == 0 {
|
||||
return Ok(content);
|
||||
}
|
||||
let mut buf = vec![0; count];
|
||||
r.read_exact(&mut buf)?;
|
||||
assert!(read_line(r)?.is_empty());
|
||||
content.append(&mut buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
#[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274
|
||||
pub fn read_http(r: &mut impl io::BufRead) -> Res<Self> {
|
||||
let line = read_line(r)?;
|
||||
let mut control = ControlData::read_http(line)?;
|
||||
let mut informational = Vec::new();
|
||||
while let Some(status) = control.informational() {
|
||||
let fields = FieldSection::read_http(r)?;
|
||||
informational.push(InformationalResponse::new(status, fields));
|
||||
let line = read_line(r)?;
|
||||
control = ControlData::read_http(line)?;
|
||||
}
|
||||
|
||||
let mut header = FieldSection::read_http(r)?;
|
||||
|
||||
let (content, trailer) = if matches!(control.status(), Some(204) | Some(304)) {
|
||||
// 204 and 304 have no body, no matter what Content-Length says.
|
||||
// Unfortunately, we can't do the same for responses to HEAD.
|
||||
(Vec::new(), FieldSection::default())
|
||||
} else if header.is_chunked() {
|
||||
let content = Self::read_chunked(r)?;
|
||||
let trailer = FieldSection::read_http(r)?;
|
||||
(content, trailer)
|
||||
} else {
|
||||
let mut content = Vec::new();
|
||||
if let Some(cl) = header.get(CONTENT_LENGTH) {
|
||||
let cl_str = String::from_utf8(Vec::from(cl))?;
|
||||
let cl_int = cl_str.parse::<usize>()?;
|
||||
if cl_int > 0 {
|
||||
content.resize(cl_int, 0);
|
||||
r.read_exact(&mut content)?;
|
||||
}
|
||||
} else {
|
||||
// Note that for a request, the spec states that the content is
|
||||
// empty, but this just reads all input like for a response.
|
||||
r.read_to_end(&mut content)?;
|
||||
}
|
||||
(content, FieldSection::default())
|
||||
};
|
||||
|
||||
header.strip_connection_headers();
|
||||
Ok(Self {
|
||||
informational,
|
||||
control,
|
||||
header,
|
||||
content,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-http")]
|
||||
pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> {
|
||||
for info in &self.informational {
|
||||
ControlData::Response(info.status()).write_http(w)?;
|
||||
info.fields().write_http(w)?;
|
||||
}
|
||||
self.control.write_http(w)?;
|
||||
if !self.content.is_empty() {
|
||||
if self.trailer.is_empty() {
|
||||
write!(w, "Content-Length: {}\r\n", self.content.len())?;
|
||||
} else {
|
||||
w.write_all(b"Transfer-Encoding: chunked\r\n")?;
|
||||
}
|
||||
}
|
||||
self.header.write_http(w)?;
|
||||
|
||||
if self.header.is_chunked() {
|
||||
write!(w, "{:x}\r\n", self.content.len())?;
|
||||
w.write_all(&self.content)?;
|
||||
w.write_all(b"\r\n0\r\n")?;
|
||||
self.trailer.write_http(w)?;
|
||||
} else {
|
||||
w.write_all(&self.content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a BHTTP message.
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
pub fn read_bhttp(r: &mut impl io::BufRead) -> Res<Self> {
|
||||
let t = read_varint(r)?.ok_or(Error::Truncated)?;
|
||||
let request = t == 0 || t == 2;
|
||||
let mode = match t {
|
||||
0 | 1 => Mode::KnownLength,
|
||||
2 | 3 => Mode::IndefiniteLength,
|
||||
_ => return Err(Error::InvalidMode),
|
||||
};
|
||||
|
||||
let mut control = ControlData::read_bhttp(request, r)?;
|
||||
let mut informational = Vec::new();
|
||||
while let Some(status) = control.informational() {
|
||||
let fields = FieldSection::read_bhttp(mode, r)?;
|
||||
informational.push(InformationalResponse::new(status, fields));
|
||||
control = ControlData::read_bhttp(request, r)?;
|
||||
}
|
||||
let header = FieldSection::read_bhttp(mode, r)?;
|
||||
|
||||
let mut content = read_vec(r)?.unwrap_or_default();
|
||||
if mode == Mode::IndefiniteLength && !content.is_empty() {
|
||||
loop {
|
||||
let mut extra = read_vec(r)?.unwrap_or_default();
|
||||
if extra.is_empty() {
|
||||
break;
|
||||
}
|
||||
content.append(&mut extra);
|
||||
}
|
||||
}
|
||||
|
||||
let trailer = FieldSection::read_bhttp(mode, r)?;
|
||||
|
||||
Ok(Self {
|
||||
informational,
|
||||
control,
|
||||
header,
|
||||
content,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> {
|
||||
write_varint(self.control.code(mode), w)?;
|
||||
for info in &self.informational {
|
||||
info.write_bhttp(mode, w)?;
|
||||
}
|
||||
self.control.write_bhttp(w)?;
|
||||
self.header.write_bhttp(mode, w)?;
|
||||
|
||||
write_vec(&self.content, w)?;
|
||||
if mode == Mode::IndefiniteLength && !self.content.is_empty() {
|
||||
write_len(0, w)?;
|
||||
}
|
||||
self.trailer.write_bhttp(mode, w)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-http")]
|
||||
impl std::fmt::Debug for Message {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
let mut buf = Vec::new();
|
||||
self.write_http(&mut buf).map_err(|_| std::fmt::Error)?;
|
||||
write!(f, "{:?}", String::from_utf8_lossy(&buf))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
#[cfg(feature = "read-http")]
|
||||
use crate::{Error, Res};
|
||||
|
||||
pub const HTAB: u8 = 0x09;
|
||||
#[cfg(feature = "read-http")]
|
||||
pub const NL: u8 = 0x0a;
|
||||
#[cfg(feature = "read-http")]
|
||||
pub const CR: u8 = 0x0d;
|
||||
pub const SP: u8 = 0x20;
|
||||
pub const COMMA: u8 = 0x2c;
|
||||
#[cfg(feature = "read-http")]
|
||||
pub const SLASH: u8 = 0x2f;
|
||||
#[cfg(feature = "read-http")]
|
||||
pub const COLON: u8 = 0x3a;
|
||||
#[cfg(feature = "read-http")]
|
||||
pub const SEMICOLON: u8 = 0x3b;
|
||||
|
||||
pub fn is_ows(x: u8) -> bool {
|
||||
x == SP || x == HTAB
|
||||
}
|
||||
|
||||
pub fn trim_ows(v: &[u8]) -> &[u8] {
|
||||
for s in 0..v.len() {
|
||||
if !is_ows(v[s]) {
|
||||
for e in (s..v.len()).rev() {
|
||||
if !is_ows(v[e]) {
|
||||
return &v[s..=e];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&v[..0]
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn downcase(n: &mut [u8]) {
|
||||
for i in n {
|
||||
if *i >= 0x41 && *i <= 0x5a {
|
||||
*i += 0x20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_of(v: u8, line: &[u8]) -> Option<usize> {
|
||||
for (i, x) in line.iter().enumerate() {
|
||||
if *x == v {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn split_at(v: u8, mut line: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)> {
|
||||
index_of(v, &line).map(|i| {
|
||||
let tail = line.split_off(i + 1);
|
||||
let _ = line.pop();
|
||||
(line, tail)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-http")]
|
||||
pub fn read_line(r: &mut impl std::io::BufRead) -> Res<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
r.read_until(NL, &mut buf)?;
|
||||
let tail = buf.pop();
|
||||
if tail != Some(NL) {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
if buf.pop().ok_or(Error::Missing(CR))? == CR {
|
||||
Ok(buf)
|
||||
} else {
|
||||
Err(Error::Missing(CR))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
#[cfg(feature = "read-bhttp")]
|
||||
use crate::err::Error;
|
||||
use crate::err::Res;
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn write_uint(n: u8, v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> {
|
||||
let v = v.into();
|
||||
assert!(n > 0 && usize::from(n) < std::mem::size_of::<u64>());
|
||||
for i in 0..n {
|
||||
w.write_all(&[((v >> (8 * (n - i - 1))) & 0xff) as u8])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_varint(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> {
|
||||
let v = v.into();
|
||||
match () {
|
||||
_ if v < (1 << 6) => write_uint(1, v, w),
|
||||
_ if v < (1 << 14) => write_uint(2, v | (1 << 14), w),
|
||||
_ if v < (1 << 30) => write_uint(4, v | (2 << 30), w),
|
||||
_ if v < (1 << 62) => write_uint(8, v | (3 << 62), w),
|
||||
_ => panic!("Varint value too large"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_len(len: usize, w: &mut impl io::Write) -> Res<()> {
|
||||
write_varint(u64::try_from(len).unwrap(), w)
|
||||
}
|
||||
|
||||
#[cfg(feature = "write-bhttp")]
|
||||
pub fn write_vec(v: &[u8], w: &mut impl io::Write) -> Res<()> {
|
||||
write_len(v.len(), w)?;
|
||||
w.write_all(v)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
fn read_uint(n: usize, r: &mut impl io::BufRead) -> Res<Option<u64>> {
|
||||
let mut buf = [0; 7];
|
||||
let count = r.read(&mut buf[..n])?;
|
||||
if count == 0 {
|
||||
return Ok(None);
|
||||
} else if count < n {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let mut v = 0;
|
||||
for i in &buf[..n] {
|
||||
v = (v << 8) | u64::from(*i);
|
||||
}
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
pub fn read_varint(r: &mut impl io::BufRead) -> Res<Option<u64>> {
|
||||
if let Some(b1) = read_uint(1, r)? {
|
||||
Ok(Some(match b1 >> 6 {
|
||||
0 => b1 & 0x3f,
|
||||
1 => ((b1 & 0x3f) << 8) | read_uint(1, r)?.ok_or(Error::Truncated)?,
|
||||
2 => ((b1 & 0x3f) << 24) | read_uint(3, r)?.ok_or(Error::Truncated)?,
|
||||
3 => ((b1 & 0x3f) << 56) | read_uint(7, r)?.ok_or(Error::Truncated)?,
|
||||
_ => unreachable!(),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "read-bhttp")]
|
||||
pub fn read_vec(r: &mut impl io::BufRead) -> Res<Option<Vec<u8>>> {
|
||||
if let Some(len) = read_varint(r)? {
|
||||
let mut v = vec![0; usize::try_from(len).unwrap()];
|
||||
r.read_exact(&mut v)?;
|
||||
Ok(Some(v))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
// Rather than grapple with #[cfg(...)] for every variable and import.
|
||||
#![cfg(all(feature = "http", feature = "bhttp"))]
|
||||
|
||||
use bhttp::{Error, Message, Mode};
|
||||
use std::io::BufReader;
|
||||
use std::mem::drop;
|
||||
|
||||
const CHUNKED_HTTP: &[u8] = b"HTTP/1.1 200 OK\r\n\
|
||||
Transfer-Encoding: camel, chunked\r\n\
|
||||
\r\n\
|
||||
4\r\n\
|
||||
This\r\n\
|
||||
6\r\n \
|
||||
conte\r\n\
|
||||
13;chunk-extension=foo\r\n\
|
||||
nt contains CRLF.\r\n\
|
||||
\r\n\
|
||||
0\r\n\
|
||||
Trailer: text\r\n\
|
||||
\r\n";
|
||||
const TRANSFER_ENCODING: &[u8] = b"transfer-encoding";
|
||||
const CHUNKED_KNOWN: &[u8] = &[
|
||||
0x01, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
|
||||
0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e,
|
||||
0x0d, 0x0a, 0x0d, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74,
|
||||
];
|
||||
const CHUNKED_INDEFINITE: &[u8] = &[
|
||||
0x03, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
|
||||
0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e,
|
||||
0x0d, 0x0a, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74,
|
||||
0x00,
|
||||
];
|
||||
|
||||
const REQUEST: &[u8] = b"GET /hello.txt HTTP/1.1\r\n\
|
||||
user-agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3\r\n\
|
||||
host: www.example.com\r\n\
|
||||
accept-language: en, mi\r\n\
|
||||
\r\n";
|
||||
const REQUEST_KNOWN: &[u8] = &[
|
||||
0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, 0x65,
|
||||
0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x40, 0x6c, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61,
|
||||
0x67, 0x65, 0x6e, 0x74, 0x34, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33,
|
||||
0x20, 0x6c, 0x69, 0x62, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33, 0x20,
|
||||
0x4f, 0x70, 0x65, 0x6e, 0x53, 0x53, 0x4c, 0x2f, 0x30, 0x2e, 0x39, 0x2e, 0x37, 0x6c, 0x20, 0x7a,
|
||||
0x6c, 0x69, 0x62, 0x2f, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x0f, 0x77,
|
||||
0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x0f, 0x61,
|
||||
0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x06, 0x65,
|
||||
0x6e, 0x2c, 0x20, 0x6d, 0x69, 0x00, 0x00,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn chunked_read() {
|
||||
drop(Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_read_known() {
|
||||
drop(Message::read_bhttp(&mut BufReader::new(CHUNKED_KNOWN)).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_read_indefinite() {
|
||||
drop(Message::read_bhttp(&mut BufReader::new(CHUNKED_INDEFINITE)).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_to_known() {
|
||||
let m = Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap();
|
||||
assert!(m.header().get(TRANSFER_ENCODING).is_none());
|
||||
|
||||
let mut buf = Vec::new();
|
||||
m.write_bhttp(Mode::KnownLength, &mut buf).unwrap();
|
||||
println!("result: {}", hex::encode(&buf));
|
||||
assert_eq!(&buf[..], CHUNKED_KNOWN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_to_indefinite() {
|
||||
let m = Message::read_http(&mut BufReader::new(CHUNKED_HTTP)).unwrap();
|
||||
assert!(m.header().get(TRANSFER_ENCODING).is_none());
|
||||
|
||||
let mut buf = Vec::new();
|
||||
m.write_bhttp(Mode::IndefiniteLength, &mut buf).unwrap();
|
||||
println!("result: {}", hex::encode(&buf));
|
||||
assert_eq!(&buf[..], CHUNKED_INDEFINITE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_request() {
|
||||
let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
m.write_bhttp(Mode::KnownLength, &mut buf).unwrap();
|
||||
println!("result: {}", hex::encode(&buf));
|
||||
assert_eq!(&buf[..], REQUEST_KNOWN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padded_to_http() {
|
||||
let mut padded = Vec::from(REQUEST_KNOWN);
|
||||
padded.resize(padded.len() + 100, 0);
|
||||
let m = Message::read_bhttp(&mut BufReader::new(&padded[..])).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
m.write_http(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..], REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_to_http() {
|
||||
let mut padded = Vec::from(REQUEST_KNOWN);
|
||||
assert_eq!(2, padded.iter().rev().take_while(|&x| *x == 0).count());
|
||||
padded.truncate(padded.len() - 2);
|
||||
|
||||
let m = Message::read_bhttp(&mut BufReader::new(&padded[..])).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
m.write_http(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..], REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_request() {
|
||||
const REQUEST: &[u8] = &[
|
||||
0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x01, 0x2f,
|
||||
];
|
||||
let m = Message::read_bhttp(&mut BufReader::new(REQUEST)).unwrap();
|
||||
assert_eq!(m.control().method().unwrap(), b"GET");
|
||||
assert_eq!(m.control().scheme().unwrap(), b"https");
|
||||
assert_eq!(m.control().authority().unwrap(), b"example.com");
|
||||
assert_eq!(m.control().path().unwrap(), b"/");
|
||||
assert!(m.control().status().is_none());
|
||||
assert!(m.header().is_empty());
|
||||
assert!(m.content().is_empty());
|
||||
assert!(m.trailer().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_response() {
|
||||
const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8];
|
||||
let m = Message::read_bhttp(&mut BufReader::new(RESPONSE)).unwrap();
|
||||
assert!(m.informational().is_empty());
|
||||
assert_eq!(m.control().status().unwrap(), 200);
|
||||
assert!(m.control().method().is_none());
|
||||
assert!(m.control().scheme().is_none());
|
||||
assert!(m.control().authority().is_none());
|
||||
assert!(m.control().path().is_none());
|
||||
assert!(m.header().is_empty());
|
||||
assert!(m.content().is_empty());
|
||||
assert!(m.trailer().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_request() {
|
||||
const REQUEST: &[u8] = b"CONNECT test.example HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
\r\n";
|
||||
let err = Message::read_http(&mut BufReader::new(REQUEST)).unwrap_err();
|
||||
assert!(matches!(err, Error::ConnectUnsupported));
|
||||
}
|
||||
|
||||
/// Verify that Connection and Proxy-Connection are stripped out properly.
|
||||
#[test]
|
||||
fn connection_header() {
|
||||
const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
other: test\r\n\
|
||||
Connection: sample\r\n\
|
||||
Connection: other, garbage\r\n\
|
||||
sample: test2\r\n\
|
||||
px: test3\r\n\
|
||||
proXy-connection: px\r\n\
|
||||
\r\n";
|
||||
|
||||
let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
|
||||
assert!(m.header().get(b"other").is_none());
|
||||
assert!(m.header().get(b"sample").is_none());
|
||||
assert!(m.header().get(b"garbage").is_none());
|
||||
assert!(m.header().get(b"connection").is_none());
|
||||
assert!(m.header().get(b"proxy-connection").is_none());
|
||||
assert!(m.header().get(b"px").is_none());
|
||||
}
|
||||
|
||||
/// Verify that hop-by-hop headers (other than transfer-encoding) are stripped out properly.
|
||||
#[test]
|
||||
fn hop_by_hop() {
|
||||
const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
keep-alive: 1\r\n\
|
||||
te: trailers\r\n\
|
||||
trailer: te\r\n\
|
||||
upgrade: h2c\r\n\
|
||||
\r\n";
|
||||
|
||||
let m = Message::read_http(&mut BufReader::new(REQUEST)).unwrap();
|
||||
assert!(m.header().get(b"keep-alive").is_none());
|
||||
assert!(m.header().get(b"te").is_none());
|
||||
assert!(m.header().get(b"trailer").is_none());
|
||||
assert!(m.header().get(b"transfer-encoding").is_none());
|
||||
assert!(m.header().get(b"upgrade").is_none());
|
||||
}
|
||||
|
||||
/// Verify that very bad chunked encoding produces a result.
|
||||
#[test]
|
||||
fn bad_chunked() {
|
||||
const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n";
|
||||
|
||||
let e = Message::read_http(&mut BufReader::new(REQUEST)).unwrap_err();
|
||||
assert!(matches!(e, Error::Truncated));
|
||||
}
|
|
@ -82,6 +82,7 @@ uniffi-example-sprites = { git = "https://github.com/mozilla/uniffi-rs.git", rev
|
|||
uniffi-example-todolist = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "bb2039f077a29dba0879372a67e764e6ace8e33f", optional = true }
|
||||
uniffi-example-custom-types = { path = "../../../components/uniffi-example-custom-types/", optional = true }
|
||||
uniffi-fixture-external-types = { path = "../../../components/uniffi-fixture-external-types/", optional = true }
|
||||
binary_http = { path = "../../../../netwerk/protocol/http/binary_http" }
|
||||
oblivious_http = { path = "../../../../netwerk/protocol/http/oblivious_http" }
|
||||
|
||||
# Note: `modern_sqlite` means rusqlite's bindings file be for a sqlite with
|
||||
|
|
|
@ -109,6 +109,7 @@ extern crate dap_ffi;
|
|||
|
||||
extern crate data_encoding_ffi;
|
||||
|
||||
extern crate binary_http;
|
||||
extern crate oblivious_http;
|
||||
|
||||
#[cfg(feature = "uniffi_fixtures")]
|
||||
|
|
Загрузка…
Ссылка в новой задаче