package http import "core:fmt" import "core:net" import "core:strconv" import "core:strings" import "core:time" DEFAULT_HTTP_PORT :: 80 MAX_REDIRECTS :: 5 HTTP_TIMEOUT :: 5 * time.Second Response :: struct { status: int, location: string, body: string, content_length: int, chunked: bool, } Error :: struct { code: int, message: string, } Error_Code :: enum int { Invalid_Url, Dial_Failed, Send_Failed, Invalid_Response, Invalid_Redirect, Invalid_Chunked, Too_Many_Redirects, } error_make :: proc(code: Error_Code) -> ^Error { err := new(Error) err.code = int(code) switch code { case .Invalid_Url: err.message = "invalid url" case .Dial_Failed: err.message = "could not connect to host" case .Send_Failed: err.message = "could not send request" case .Invalid_Response: err.message = "could not parse response" case .Invalid_Redirect: err.message = "invalid redirect" case .Invalid_Chunked: err.message = "invalid chunked response" case .Too_Many_Redirects: err.message = "too many redirects" } return err } http_get :: proc(url: string) -> (resp: Response, err: ^Error) { current := url owns := false for _ in 0 ..= MAX_REDIRECTS { next_url, next_resp, request_err := perform_request(current) if owns do delete(current) if request_err != nil { return {}, request_err } if next_url != "" { current = next_url owns = true continue } return next_resp, nil } if owns do delete(current) return {}, error_make(.Too_Many_Redirects) } perform_request :: proc(url: string) -> (next_url: string, resp: Response, err: ^Error) { host, port, path, parse_ok := parse_http_url(url) if !parse_ok { return "", {}, error_make(.Invalid_Url) } conn, dial_err := net.dial_tcp_from_hostname_with_port_override(host, port) if dial_err != nil { return "", {}, error_make(.Dial_Failed) } defer net.close(conn) net.set_option(conn, .Receive_Timeout, HTTP_TIMEOUT) net.set_option(conn, .Send_Timeout, HTTP_TIMEOUT) host_buf: [256]byte host_head := host if port != DEFAULT_HTTP_PORT { host_head = fmt.bprintf(host_buf[:], "%s:%d", host, port) } request_buf: [1024]byte request := fmt.bprintf( request_buf[:], "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nUser-Agent: desi-explorer-http\r\nAccept: */*\r\n\r\n", path, host_head, ) if _, send_err := net.send_tcp(conn, transmute([]u8)request); send_err != nil { return "", {}, error_make(.Send_Failed) } buf: [dynamic]u8 defer delete(buf) scratch: [4096]byte for { n, recv_err := net.recv_tcp(conn, scratch[:]) if n > 0 do append(&buf, ..scratch[:n]) if recv_err != nil || n == 0 do break } parsed, response_ok := parse_response(buf[:]) if !response_ok { return "", {}, error_make(.Invalid_Response) } resp = parsed if resp.status >= 300 && resp.status < 400 && resp.location != "" { resolved := resolve_redirect(url, resp.location) if resolved == "" { return "", {}, error_make(.Invalid_Redirect) } resp.body = "" return resolved, resp, nil } if resp.chunked { decoded, decode_ok := decode_chunked(resp.body) if !decode_ok { return "", {}, error_make(.Invalid_Chunked) } resp.body = decoded return "", resp, nil } if resp.content_length >= 0 && resp.content_length < len(resp.body) { resp.body = resp.body[:resp.content_length] } resp.body = strings.clone(resp.body) return "", resp, nil } parse_response :: proc(raw: []byte) -> (resp: Response, ok: bool) { resp.content_length = -1 raw_str := string(raw) header_end := strings.index(raw_str, "\r\n\r\n") if header_end < 0 { return resp, false } lines := strings.split(raw_str[:header_end], "\r\n") defer delete(lines) if len(lines) == 0 { return resp, false } resp.status = parse_status_code(lines[0]) if resp.status == 0 { return resp, false } for _, i in lines { if i == 0 do continue colon := strings.index_byte(lines[i], ':') if colon < 0 do continue key := strings.trim_space(lines[i][:colon]) value := strings.trim_space(lines[i][colon + 1:]) switch { case fold_eq(key, "content-length"): if n, number_ok := strconv.parse_int(value); number_ok { resp.content_length = n } case fold_eq(key, "transfer-encoding"): if strings.contains(value, "chunked") { resp.chunked = true } case fold_eq(key, "location"): resp.location = value } } body_start := header_end + 4 if body_start <= len(raw_str) { resp.body = raw_str[body_start:] } return resp, true } parse_status_code :: proc(status_line: string) -> int { parts := strings.fields(status_line) defer delete(parts) if len(parts) < 2 { return 0 } code, ok := strconv.parse_int(parts[1]) return code if ok else 0 } parse_http_url :: proc(url: string) -> (host: string, port: int, path: string, ok: bool) { if strings.has_prefix(url, "https://") || !strings.has_prefix(url, "http://") { return "", 0, "", false } rest := url[len("http://"):] host_and_port := rest if slash := strings.index_byte(rest, '/'); slash >= 0 { host_and_port = rest[:slash] path = rest[slash:] } else { path = "/" } host = host_and_port port = DEFAULT_HTTP_PORT if colon := strings.last_index_byte(host_and_port, ':'); colon >= 0 { parsed, p_ok := strconv.parse_int(host_and_port[colon + 1:]) if !p_ok { return "", 0, "", false } host = host_and_port[:colon] port = parsed } if host == "" { return "", 0, "", false } return host, port, path, true } resolve_redirect :: proc(base_url, location: string) -> string { if strings.has_prefix(location, "http://") || strings.has_prefix(location, "https://") { return strings.clone(location) } if !strings.has_prefix(location, "/") { return "" } rest := base_url[len("http://"):] if slash := strings.index_byte(rest, '/'); slash >= 0 { rest = rest[:slash] } return strings.concatenate([]string{"http://", rest, location}) } decode_chunked :: proc(data: string) -> (body: string, ok: bool) { decoded: [dynamic]u8 defer delete(decoded) at := 0 for at < len(data) { nl := strings.index_byte(data[at:], '\n') if nl < 0 do return "", false size_line := strings.trim_space(data[at:at + nl]) at += nl + 1 if semi := strings.index_byte(size_line, ';'); semi >= 0 { size_line = size_line[:semi] } size, size_ok := strconv.parse_int(size_line, 16) if !size_ok || size < 0 { return "", false } if size == 0 { at += 2 break } if at + size > len(data) { return "", false } append(&decoded, ..transmute([]u8)data[at:at + size]) at += size + 2 } if len(decoded) == 0 { return "", true } return strings.clone(string(decoded[:])), true } fold_eq :: proc(a, b: string) -> bool { if len(a) != len(b) { return false } for i in 0 ..< len(a) { ca := a[i] cb := b[i] if ca >= 'A' && ca <= 'Z' do ca += 'a' - 'A' if cb >= 'A' && cb <= 'Z' do cb += 'a' - 'A' if ca != cb do return false } return true }