// The async HTTP client does not expose a client-certificate transport. Keep
// this HTTPS GET narrowly scoped to the ZTS role-token exchange.
///|
async fn athenz_certificate_get(
url : String,
certificate_file : String,
private_key_file : String,
ca_pem_file : String?,
) -> (Int, Bytes) {
@async.with_timeout(10000, async fn() {
guard url.split_once("https://") is Some(("", rest)) &&
rest.split_once("/") is Some((authority, path_tail)) &&
authority != "" else {
raise AuthenticationFailed("invalid Athenz HTTPS ZTS URL")
}
let (host, port) = match authority.split_once(":") {
Some((host, port_text)) => {
let port = @string.parse_int(port_text) catch {
_ => raise AuthenticationFailed("invalid Athenz ZTS port")
}
guard host != "" && port > 0 && port < 65536 else {
raise AuthenticationFailed("invalid Athenz ZTS host or port")
}
(host.to_owned(), port)
}
None => (authority.to_owned(), 443)
}
let tcp = @socket.Tcp::connect_to_host(host, port~)
errdefer tcp.close()
let tls = @native_tls.NativeTls::connect(
tcp,
host,
ca_file=ca_pem_file.unwrap_or(""),
cert_file=certificate_file,
key_file=private_key_file,
)
defer tls.close()
tls.write(
@utf8.encode(
"GET /\{path_tail} HTTP/1.1\r\nHost: \{authority}\r\nAccept: application/json\r\nConnection: close\r\n\r\n",
),
)
let header = tls.read_until("\r\n\r\n") catch {
e => raise AuthenticationFailed("invalid Athenz ZTS HTTP response: \{e}")
}
guard header is Some(header) && header.length() <= 16384 else {
raise AuthenticationFailed("invalid Athenz ZTS HTTP header")
}
let status_line = match header.split_once("\r\n") {
Some((line, _)) => line
None => header
}
guard status_line.has_prefix("HTTP/1.") &&
status_line.split_once(" ") is Some((_, rest)) else {
raise AuthenticationFailed("invalid Athenz ZTS HTTP status")
}
let code_text = match rest.split_once(" ") {
Some((code, _)) => code
None => rest
}
let status = @string.parse_int(code_text) catch {
_ => raise AuthenticationFailed("invalid Athenz ZTS HTTP status")
}
let mut content_length : Int? = None
let mut chunked = false
for line in header.split("\r\n") {
if line.split_once(":") is Some((name, value)) {
match name.to_lower() {
"content-length" => {
let length = @string.parse_int(value.trim()) catch {
_ =>
raise AuthenticationFailed("invalid Athenz ZTS content length")
}
guard length >= 0 && length <= 1048576 else {
raise AuthenticationFailed("Athenz ZTS response is too large")
}
content_length = Some(length)
}
"transfer-encoding" =>
if value.to_lower().contains("chunked") {
chunked = true
}
_ => ()
}
}
}
let body = if chunked {
athenz_read_chunked(tls)
} else if content_length is Some(length) {
tls.read_exactly(length)
} else {
athenz_read_until_close(tls)
}
(status, body)
})
}
///|
async fn athenz_read_chunked(tls : @native_tls.NativeTls) -> Bytes {
let body = @buffer.Buffer()
for ;; {
let line = tls.read_until("\r\n") catch {
_ => raise AuthenticationFailed("invalid Athenz ZTS chunk header")
}
guard line is Some(line) else {
raise AuthenticationFailed("truncated Athenz ZTS chunk header")
}
let size_text = match line.split_once(";") {
Some((size, _)) => size
None => line
}
let size = @string.parse_int(size_text.trim(), base=16) catch {
_ => raise AuthenticationFailed("invalid Athenz ZTS chunk size")
}
guard size >= 0 && size <= 1048576 - body.length() else {
raise AuthenticationFailed("Athenz ZTS response is too large")
}
if size == 0 {
for ;; {
let trailer = tls.read_until("\r\n")
guard trailer is Some(value) else {
raise AuthenticationFailed("truncated Athenz ZTS chunk trailer")
}
if value == "" {
return body.to_bytes()
}
}
}
body.write_bytes(tls.read_exactly(size))
guard tls.read_exactly(2) == b"\r\n" else {
raise AuthenticationFailed("invalid Athenz ZTS chunk delimiter")
}
}
body.to_bytes()
}
///|
async fn athenz_read_until_close(tls : @native_tls.NativeTls) -> Bytes {
let body = @buffer.Buffer()
let chunk = FixedArray::make(4096, b'\x00')
for ;; {
let count = tls.read(chunk)
if count == 0 {
return body.to_bytes()
}
guard count <= 1048576 - body.length() else {
raise AuthenticationFailed("Athenz ZTS response is too large")
}
body.write_bytes(chunk.unsafe_reinterpret_as_bytes()[:count].to_owned())
}
body.to_bytes()
}