///| Native HTTP client implementation using @http package
///| This file is only compiled for native target
///|
pub struct NativeHttpClient {
// No state needed, uses @http directly
dummy : Int
}
///|
pub fn NativeHttpClient::new() -> NativeHttpClient {
{ dummy: 0 }
}
///|
async fn apply_auth_header(
url : String,
headers : Map[String, String],
) -> Map[String, String] raise @bit.GitError {
if headers.contains("Authorization") {
return headers
}
let auth = resolve_auth_header(url)
match auth {
None => headers
Some(value) => {
let out : Map[String, String] = Map([])
for k, v in headers {
out[k] = v
}
out["Authorization"] = value
out
}
}
}
///|
async fn resolve_auth_header(url : String) -> String? raise @bit.GitError {
match parse_http_url(url) {
None => None
Some((protocol, host, path, username, password)) =>
match password {
Some(pass) => {
let user = username.unwrap_or("")
Some(build_basic_auth(user, pass))
}
None => {
let cred_path = normalize_credential_path(path)
match bit_credential_fill(protocol, host, cred_path, username) {
None => None
Some((user, pass)) => Some(build_basic_auth(user, pass))
}
}
}
}
}
///|
fn build_basic_auth(username : String, password : String) -> String {
let raw = username + ":" + password
let encoded = @base64.encode(@utf8.encode(raw), url_safe=false)
"Basic " + encoded
}
///|
fn parse_http_url(url : String) -> (String, String, String, String?, String?)? {
let (protocol, rest) = if url.has_prefix("https://") {
("https", String::unsafe_substring(url, start=8, end=url.length()))
} else if url.has_prefix("http://") {
("http", String::unsafe_substring(url, start=7, end=url.length()))
} else {
return None
}
let slash = rest.find("/")
let authority = match slash {
Some(idx) => String::unsafe_substring(rest, start=0, end=idx)
None => rest
}
let path = match slash {
Some(idx) =>
String::unsafe_substring(rest, start=idx + 1, end=rest.length())
None => ""
}
let mut host = authority
let mut username : String? = None
let mut password : String? = None
match authority.rev_find("@") {
Some(at) => {
let userinfo = String::unsafe_substring(authority, start=0, end=at)
host = String::unsafe_substring(
authority,
start=at + 1,
end=authority.length(),
)
match userinfo.find(":") {
Some(colon) => {
username = Some(
String::unsafe_substring(userinfo, start=0, end=colon),
)
password = Some(
String::unsafe_substring(
userinfo,
start=colon + 1,
end=userinfo.length(),
),
)
}
None => username = Some(userinfo)
}
}
None => ()
}
Some((protocol, host, path, username, password))
}
///|
fn normalize_credential_path(path : String) -> String {
let mut out = path
if out.find("?") is Some(q) {
out = String::unsafe_substring(out, start=0, end=q)
}
let suffix_info = "/info/refs"
if out.has_suffix(suffix_info) {
out = String::unsafe_substring(
out,
start=0,
end=out.length() - suffix_info.length(),
)
}
let suffix_recv = "/git-receive-pack"
if out.has_suffix(suffix_recv) {
out = String::unsafe_substring(
out,
start=0,
end=out.length() - suffix_recv.length(),
)
}
out
}
///|
async fn bit_credential_fill(
protocol : String,
host : String,
path : String,
username : String?,
) -> (String, String)? raise @bit.GitError {
let input = build_credential_input(protocol, host, path, username)
let (stdin, writer) = @process.write_to_process() catch {
err => raise @bit.GitError::IoError("git credential pipe failed: \{err}")
}
writer.write(input) catch {
err => raise @bit.GitError::IoError("git credential write failed: \{err}")
}
writer.close()
let (code, stdout, stderr) = @process.collect_output(
"git",
["credential", "fill"],
inherit_env=true,
stdin~,
extra_env={ "GIT_TERMINAL_PROMPT": "0" },
) catch {
err => raise @bit.GitError::IoError("git credential fill failed: \{err}")
}
if code != 0 {
ignore(stderr)
return None
}
let text = stdout.text() catch {
err if @async.is_cancellation_error(err) =>
raise @bit.GitError::IoError(err.to_string())
_ => ""
}
let mut user : String? = None
let mut pass : String? = None
for line_view in text.split("\n") {
let line = line_view.to_owned()
match line.find("=") {
Some(eq) => {
let key = String::unsafe_substring(line, start=0, end=eq)
let value = String::unsafe_substring(
line,
start=eq + 1,
end=line.length(),
)
if key == "username" {
user = Some(value)
} else if key == "password" || key == "token" || key == "oauth_token" {
pass = Some(value)
}
}
None => ()
}
}
match pass {
Some(p) => Some((user.unwrap_or(""), p))
None => None
}
}
///|
fn build_credential_input(
protocol : String,
host : String,
path : String,
username : String?,
) -> String {
let sb = StringBuilder::new()
sb.write_string("protocol=" + protocol + "\n")
sb.write_string("host=" + host + "\n")
if path.length() > 0 {
sb.write_string("path=" + path + "\n")
}
if username is Some(u) {
sb.write_string("username=" + u + "\n")
}
sb.write_string("\n")
sb.to_string()
}
///|
pub async fn native_http_get(
url : String,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
let auth_headers = apply_auth_header(url, headers)
let (response, client) = @http.get_stream(url, headers=auth_headers) catch {
e => raise @bit.GitError::IoError("HTTP GET failed: \{e}")
}
let chunks : Array[Byte] = []
while true {
let chunk = client.read_some() catch {
err if @async.is_cancellation_error(err) =>
raise @bit.GitError::IoError(err.to_string())
_ => break
}
match chunk {
Some(b) =>
for byte in b {
chunks.push(byte)
}
None => break
}
}
client.close()
let data = Bytes::from_array(
FixedArray::makei(chunks.length(), i => chunks[i]),
)
(@bit.HttpResponse::new(response.code), data)
}
///|
pub async fn native_http_post(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
let auth_headers = apply_auth_header(url, headers)
let client = @http.post_stream(url, headers=auth_headers) catch {
e => raise @bit.GitError::IoError("HTTP POST failed: \{e}")
}
client.write(body) catch {
e => raise @bit.GitError::IoError("HTTP POST write failed: \{e}")
}
let response = client.end_request() catch {
e => raise @bit.GitError::IoError("HTTP POST end_request failed: \{e}")
}
let chunks : Array[Byte] = []
while true {
let chunk = client.read_some() catch {
err if @async.is_cancellation_error(err) =>
raise @bit.GitError::IoError(err.to_string())
_ => break
}
match chunk {
Some(b) =>
for byte in b {
chunks.push(byte)
}
None => break
}
}
client.close()
let data = Bytes::from_array(
FixedArray::makei(chunks.length(), i => chunks[i]),
)
(@bit.HttpResponse::new(response.code), data)
}
///|
pub impl @types.AsyncHttpClient for NativeHttpClient with fn get(
_self,
url,
headers,
) {
native_http_get(url, headers)
}
///|
pub impl @types.AsyncHttpClient for NativeHttpClient with fn post(
_self,
url,
body,
headers,
) {
native_http_post(url, body, headers)
}
///|
pub async fn native_http_put(
url : String,
body : Bytes,
headers : Map[String, String],
) -> (@bit.HttpResponse, Bytes) raise @bit.GitError {
let auth_headers = apply_auth_header(url, headers)
let client = @http.put_stream(url, headers=auth_headers) catch {
e => raise @bit.GitError::IoError("HTTP PUT failed: \{e}")
}
client.write(body) catch {
e => raise @bit.GitError::IoError("HTTP PUT write failed: \{e}")
}
let response = client.end_request() catch {
e => raise @bit.GitError::IoError("HTTP PUT end_request failed: \{e}")
}
let chunks : Array[Byte] = []
while true {
let chunk = client.read_some() catch {
err if @async.is_cancellation_error(err) =>
raise @bit.GitError::IoError(err.to_string())
_ => break
}
match chunk {
Some(b) =>
for byte in b {
chunks.push(byte)
}
None => break
}
}
client.close()
let data = Bytes::from_array(
FixedArray::makei(chunks.length(), i => chunks[i]),
)
(@bit.HttpResponse::new(response.code), data)
}