///|
#cfg(target="native")
priv suberror Http3CodecError {
Http3FrameTruncated
Http3FrameUnsupported
Http3ExpectedHttps
Http3HandshakeFailed
Http3ResponseIncomplete
} derive(Debug, ToJson)
///|
#cfg(target="native")
let h3_frame_data : Int = 0x0
///|
#cfg(target="native")
let h3_frame_headers : Int = 0x1
///|
#cfg(target="native")
let h3_frame_settings : Int = 0x4
///|
#cfg(target="native")
let h3_stream_control : Int = 0x0
///|
#cfg(target="native")
priv struct H3Frame {
frame_type : Int
payload : Bytes
}
///|
#cfg(target="native")
#warnings("-unused_field")
priv struct H3DecodedResponse {
status : Int
headers : Array[(String, String)]
body : Bytes
}
///|
#cfg(target="native")
fn h3_encode_frame(frame_type : Int, payload : Bytes) -> Bytes raise {
let out = @buffer.new()
quic_write_varint(out, frame_type)
quic_write_varint(out, payload.length())
out.write_bytes(payload)
out.contents()
}
///|
#cfg(target="native")
fn h3_decode_frame_at(data : Bytes, offset : Int) -> (H3Frame, Int) raise {
let (frame_type, pos) = quic_read_varint(data, offset)
let (len, pos) = quic_read_varint(data, pos)
guard pos + len <= data.length() else { raise Http3FrameTruncated }
let payload = @buffer.new()
payload.write_bytes(data[pos:pos + len])
({ frame_type, payload: payload.contents() }, pos + len)
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_decode_frame(data : Bytes, offset? : Int = 0) -> H3Frame raise {
let (frame, _) = h3_decode_frame_at(data, offset)
frame
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_decode_frames(data : Bytes) -> Array[H3Frame] raise {
let frames = []
for offset = 0; offset < data.length(); {
let (frame, next) = h3_decode_frame_at(data, offset)
frames.push(frame)
continue next
}
frames
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_encode_data(data : Bytes) -> Bytes raise {
h3_encode_frame(h3_frame_data, data)
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_encode_headers(headers : Array[(String, String)]) -> Bytes raise {
h3_encode_frame(h3_frame_headers, qpack_encode_header_block(headers))
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_decode_headers(frame : H3Frame) -> Array[(String, String)] raise {
guard frame.frame_type == h3_frame_headers else { raise Http3FrameTruncated }
qpack_decode_header_block(frame.payload)
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_encode_settings(settings : Array[(Int, Int)]) -> Bytes raise {
let payload = @buffer.new()
for setting in settings {
quic_write_varint(payload, setting.0)
quic_write_varint(payload, setting.1)
}
h3_encode_frame(h3_frame_settings, payload.contents())
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_decode_settings(frame : H3Frame) -> Array[(Int, Int)] raise {
guard frame.frame_type == h3_frame_settings else {
raise Http3FrameUnsupported
}
let settings = []
for offset = 0; offset < frame.payload.length(); {
let (identifier, pos) = quic_read_varint(frame.payload, offset)
let (value, pos) = quic_read_varint(frame.payload, pos)
settings.push((identifier, value))
continue pos
}
settings
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_encode_control_stream(settings : Array[(Int, Int)]) -> Bytes raise {
let out = @buffer.new()
quic_write_varint(out, h3_stream_control)
out.write_bytes(h3_encode_settings(settings))
out.contents()
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_encode_request_stream(
headers : Array[(String, String)],
body : Bytes,
) -> Bytes raise {
let out = @buffer.new()
out.write_bytes(h3_encode_headers(headers))
if body.length() > 0 {
out.write_bytes(h3_encode_data(body))
}
out.contents()
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn h3_decode_response_stream(stream_data : Bytes) -> H3DecodedResponse raise {
let frames = h3_decode_frames(stream_data)
let mut status = -1
let mut headers = []
let body = @buffer.new()
for frame in frames {
if frame.frame_type == h3_frame_headers {
headers = h3_decode_headers(frame)
for header in headers {
if header.0 == ":status" {
status = @string.parse_int(header.1, base=10) catch { _ => -1 }
}
}
} else if frame.frame_type == h3_frame_data {
body.write_bytes(frame.payload)
} else {
raise Http3FrameUnsupported
}
}
guard status >= 0 else { raise Http3FrameUnsupported }
{ status, headers, body: body.contents() }
}
///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_decode_h3_response_packets(
packets : Array[Bytes],
destination_connection_id_length : Int,
keys : QuicPacketKeys,
stream_id? : Int = 0,
) -> H3DecodedResponse raise {
let stream_data = @buffer.new()
for packet in packets {
let opened = quic_open_short_packet(
packet, destination_connection_id_length, keys,
)
stream_data.write_bytes(quic_collect_stream_data(opened.payload, stream_id))
}
h3_decode_response_stream(stream_data.contents())
}
///|
#cfg(target="native")
fn h3_response_from_decoded(decoded : H3DecodedResponse) -> Response raise {
h2_response_from_headers(decoded.headers)
}
///|
#cfg(target="native")
fn h3_request_headers(
protocol : Protocol,
host : String,
path : String,
meth : RequestMethod,
headers : Map[String, String],
body_len : Int,
) -> Array[(String, String)] {
let header_list = []
header_list.push((":method", meth.h2_text()))
header_list.push(
(
":scheme",
match protocol {
Http => "http"
Https => "https"
},
),
)
header_list.push((":authority", host))
header_list.push((":path", path))
if body_len > 0 {
header_list.push(("content-length", body_len.to_string()))
}
let mut has_accept_encoding = false
for item in headers {
let (key, value) = item
let key = key.to_lower()
match key {
"connection"
| "host"
| "keep-alive"
| "proxy-connection"
| "transfer-encoding"
| "upgrade" => ()
"accept-encoding" => {
has_accept_encoding = true
header_list.push((key, value))
}
_ => header_list.push((key, value))
}
}
if !has_accept_encoding {
header_list.push(("accept-encoding", "gzip, deflate, br, zstd"))
}
header_list
}
///|
#cfg(target="native")
fn h3_random_bytes(len : Int) -> Bytes {
let seed = @buffer.new()
let now = @async.now()
for i in 0..<4 {
seed.write_int64_le(now + i.to_int64())
}
let rand = @random.Rand::chacha8(seed=seed.contents())
let out = @buffer.new()
for _ in 0.. ResponseBody {
let (protocol, port, host, path) = resolve_url(uri)
guard protocol == Https else { raise Http3ExpectedHttps }
let addr = @socket.Addr::resolve(host, port~)
let conn = @socket.UdpClient(addr)
defer conn.close()
let initial = quic_send_tls13_client_initial_on_conn(
conn, host, destination_connection_id, source_connection_id, client_private_key,
random, legacy_session_id,
)
let handshake = quic_recv_tls13_server_handshake(
conn,
client_private_key,
initial.client_hello,
destination_connection_id,
verify_certificate=verify,
retry_source_connection_id=source_connection_id,
)
if verify {
guard handshake.peer_certificate_info is Some(peer_info) else {
raise X509DerMissingField
}
x509_verify_hostname(peer_info, host)
}
let _ = quic_send_tls13_client_finished(
conn,
handshake.server_connection_id,
source_connection_id,
0,
handshake.client_quic_handshake_keys,
handshake.client_finished_message,
ack_largest=handshake.server_handshake_largest_ack,
)
let request_body = body.binary()
let request_headers = h3_request_headers(
protocol,
host,
path,
meth,
headers,
request_body.length(),
)
let request_packet = quic_build_h3_client_request_packet(
handshake.server_connection_id,
0,
handshake.client_quic_application_keys,
0,
request_headers,
request_body,
)
conn.send(request_packet.packet)
let decoded = quic_recv_h3_response_loop(
conn,
source_connection_id.length(),
handshake.server_quic_application_keys,
)
let response = h3_response_from_decoded(decoded)
{ response, body: h2_decode_body(response, decoded.body) as &@io.Data }
}
///|
#cfg(target="native")
async fn h3_perform_request(
uri : String,
meth : RequestMethod,
headers : Map[String, String],
body : &@io.Data,
verify? : Bool = true,
) -> ResponseBody {
@async.with_timeout(10000, () => {
h3_perform_request_with_config(
uri,
meth,
headers,
body,
verify~,
client_private_key=h3_random_bytes(32),
destination_connection_id=h3_random_bytes(8),
source_connection_id=h3_random_bytes(8),
random=h3_random_bytes(32),
legacy_session_id=b"",
)
})
}