added http package; updated data.odin
This commit is contained in:
@@ -0,0 +1,273 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
http_get :: proc(url: string) -> (data: string, ok: bool) {
|
||||||
|
current := url
|
||||||
|
owns := false
|
||||||
|
|
||||||
|
for _ in 0 ..= MAX_REDIRECTS {
|
||||||
|
next_url, body, _, request_ok := perform_request(current)
|
||||||
|
if owns do delete(current)
|
||||||
|
|
||||||
|
if !request_ok {
|
||||||
|
if next_url != "" do delete(next_url)
|
||||||
|
if body != "" do delete(body)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if next_url != "" {
|
||||||
|
if body != "" do delete(body)
|
||||||
|
current = next_url
|
||||||
|
owns = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
perform_request :: proc(url: string) -> (next_url: string, body: string, status: int, ok: bool) {
|
||||||
|
host, port, path, parse_ok := parse_http_url(url)
|
||||||
|
if !parse_ok {
|
||||||
|
return "", "", 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, dial_err := net.dial_tcp_from_hostname_with_port_override(host, port)
|
||||||
|
if dial_err != nil {
|
||||||
|
return "", "", 0, false
|
||||||
|
}
|
||||||
|
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 "", "", 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, parsed := parse_response(buf[:])
|
||||||
|
if !parsed {
|
||||||
|
return "", "", 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.status >= 300 && resp.status < 400 && resp.location != "" {
|
||||||
|
resolved := resolve_redirect(url, resp.location)
|
||||||
|
if resolved == "" {
|
||||||
|
return "", "", resp.status, false
|
||||||
|
}
|
||||||
|
return resolved, "", resp.status, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.chunked {
|
||||||
|
decoded, decode_ok := decode_chunked(resp.body)
|
||||||
|
if !decode_ok {
|
||||||
|
return "", "", resp.status, false
|
||||||
|
}
|
||||||
|
return "", decoded, resp.status, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.content_length >= 0 && resp.content_length < len(resp.body) {
|
||||||
|
resp.body = resp.body[:resp.content_length]
|
||||||
|
}
|
||||||
|
if resp.body == "" {
|
||||||
|
return "", "", resp.status, true
|
||||||
|
}
|
||||||
|
return "", strings.clone(resp.body), resp.status, true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package http_test
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:net"
|
||||||
|
import "core:testing"
|
||||||
|
import "core:thread"
|
||||||
|
|
||||||
|
import http "../src"
|
||||||
|
|
||||||
|
SERVER_BODY :: "hello world"
|
||||||
|
SERVER_RESPONSE := "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" +
|
||||||
|
"Content-Length: 11\r\n" +
|
||||||
|
"Connection: close\r\n\r\n" +
|
||||||
|
SERVER_BODY
|
||||||
|
|
||||||
|
Serve_Args :: struct {
|
||||||
|
listener: net.TCP_Socket,
|
||||||
|
}
|
||||||
|
|
||||||
|
serve_proc :: proc(data: rawptr) {
|
||||||
|
args := (^Serve_Args)(data)
|
||||||
|
|
||||||
|
conn, _, err := net.accept_tcp(args.listener)
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("test server: accept failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer net.close(conn)
|
||||||
|
|
||||||
|
request: [4096]byte
|
||||||
|
if _, err := net.recv_tcp(conn, request[:]); err != nil {
|
||||||
|
log.errorf("test server: recv failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := net.send_tcp(conn, transmute([]u8)SERVER_RESPONSE); err != nil {
|
||||||
|
log.errorf("test server: send failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_http_get_request :: proc(t: ^testing.T) {
|
||||||
|
listener, err := net.listen_tcp({net.IP4_Loopback, 0})
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("could not start test server: %v", err)
|
||||||
|
testing.fail(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer net.close(listener)
|
||||||
|
|
||||||
|
bound, berr := net.bound_endpoint(listener)
|
||||||
|
if berr != nil {
|
||||||
|
log.errorf("could not read test server port: %v", berr)
|
||||||
|
testing.fail(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args: Serve_Args = {listener = listener}
|
||||||
|
server_thread := thread.create_and_start_with_data(&args, serve_proc)
|
||||||
|
defer thread.destroy(server_thread)
|
||||||
|
|
||||||
|
url_buf: [64]byte
|
||||||
|
url := fmt.bprintf(url_buf[:], "http://127.0.0.1:%d/", bound.port)
|
||||||
|
|
||||||
|
log.infof("requesting %s", url)
|
||||||
|
body, ok := http.http_get(url)
|
||||||
|
if !testing.expect(t, ok, "http_get returned ok=false") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer delete(body)
|
||||||
|
|
||||||
|
testing.expectf(t, body == SERVER_BODY, "expected %q, got %q", SERVER_BODY, body)
|
||||||
|
}
|
||||||
+9
-5
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:net"
|
import http "lib:http/src"
|
||||||
import "vendor:curl"
|
|
||||||
|
|
||||||
Catalog :: struct {
|
Catalog :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -25,10 +24,15 @@ APIError :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_catalogs :: proc(url: string) -> ([dynamic]Catalog, ^APIError) {
|
get_catalogs :: proc(url: string) -> ([dynamic]Catalog, ^APIError) {
|
||||||
ucurl := curl.url()
|
data, ok := http.http_get(url)
|
||||||
|
if !ok {
|
||||||
|
err := new(APIError)
|
||||||
|
err.code = 1
|
||||||
|
err.message = "request failed"
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer delete(data)
|
||||||
|
|
||||||
|
|
||||||
defer curl.url_cleanup(ucurl)
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user