From ed03c6403f14186531b5804972bfd35fdd8fea37 Mon Sep 17 00:00:00 2001 From: Luuk Verweij Date: Sun, 10 Dec 2023 20:29:22 +0100 Subject: [PATCH 1/4] prevent collision of identifier with itself --- aaa-stdlib/src/stack.rs | 15 ++++++++++++--- aaa/cross_referencer/cross_referencer.py | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/aaa-stdlib/src/stack.rs b/aaa-stdlib/src/stack.rs index 42506053..60b17729 100644 --- a/aaa-stdlib/src/stack.rs +++ b/aaa-stdlib/src/stack.rs @@ -19,8 +19,8 @@ use nix::{ fcntl::{open, OFlag}, sys::{ socket::{ - accept, bind, connect, getpeername, listen, socket, AddressFamily, SockFlag, SockType, - SockaddrIn, + accept, bind, connect, getpeername, listen, setsockopt, socket, sockopt::ReuseAddr, + AddressFamily, SockFlag, SockType, SockaddrIn, }, stat::Mode, wait::{WaitPidFlag, WaitStatus}, @@ -600,7 +600,16 @@ where let addr = SockaddrIn::from_str(&format!("{ip_addr}:{port}")).unwrap(); let result = bind(fd as i32, &addr); - self.push_bool(result.is_ok()); + + if !result.is_ok() { + self.push_bool(false); + } + + // We allow reuse addresses for all bind() calls. + // Implementing it separately would get messy, because the + // second argument cannot be loaded from int without usage of unsafe. + let setsockopt_result = setsockopt(fd as i32, ReuseAddr, &true); + self.push_bool(setsockopt_result.is_ok()); } pub fn listen(&mut self) { diff --git a/aaa/cross_referencer/cross_referencer.py b/aaa/cross_referencer/cross_referencer.py index d5ac5bfc..a1a5a139 100644 --- a/aaa/cross_referencer/cross_referencer.py +++ b/aaa/cross_referencer/cross_referencer.py @@ -84,6 +84,10 @@ def _save_identifier(self, identifiable: Identifiable) -> None: except KeyError: self.identifiers[key] = identifiable else: + if found.position == identifiable.position: + # Cannot collide with same item + return + self.exceptions += [CollidingIdentifier([identifiable, found])] def run(self) -> CrossReferencerOutput: From c391edcc2d0f9709ab959cfeb73503633f2d751f Mon Sep 17 00:00:00 2001 From: Luuk Verweij Date: Fri, 21 Jul 2023 14:30:29 +0200 Subject: [PATCH 2/4] add http server example, parsing requests supporting routers --- README.md | 2 +- examples/{http_client.aaa => http/client.aaa} | 3 +- examples/http/request.aaa | 93 +++++++++ examples/http/response.aaa | 83 ++++++++ examples/http/router.aaa | 122 +++++++++++ examples/http/server.aaa | 197 ++++++++++++++++++ examples/http/util.aaa | 27 +++ examples/http_server.aaa | 97 ++++----- tests/aaa/docs/test_examples.py | 29 ++- 9 files changed, 596 insertions(+), 57 deletions(-) rename examples/{http_client.aaa => http/client.aaa} (91%) create mode 100644 examples/http/request.aaa create mode 100644 examples/http/response.aaa create mode 100644 examples/http/router.aaa create mode 100644 examples/http/server.aaa create mode 100644 examples/http/util.aaa diff --git a/README.md b/README.md index 3d6ba679..d7621dbb 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ pdm run ./manage.py run 'fn main { "Hello world\n" . }' pdm run ./manage.py run examples/fizzbuzz.aaa # Run bare-bones HTTP server in Aaa -pdm run ./manage.py run examples/http_server.aaa +pdm run ./manage.py run examples/http/server.aaa # Send request from different shell curl http://localhost:8080 diff --git a/examples/http_client.aaa b/examples/http/client.aaa similarity index 91% rename from examples/http_client.aaa rename to examples/http/client.aaa index 8a1285aa..3b9468ef 100644 --- a/examples/http_client.aaa +++ b/examples/http/client.aaa @@ -13,13 +13,14 @@ fn make_http_socket return int, bool { AF_INET SOCK_STREAM 0 socket } -fn raw_http_request return str { +fn raw_http_request return str { // TODO use Request from request.aaa "GET / HTTP/1.1\r\n" "Host: icanhazip.com\r\n" str:append "User-Agent: curl/7.68.0\r\n" str:append "Accept: */*\r\n\r\n" str:append } +// TODO move main out of http package fn main return int { make_http_socket use fd, socket_ok { diff --git a/examples/http/request.aaa b/examples/http/request.aaa new file mode 100644 index 00000000..db00b20c --- /dev/null +++ b/examples/http/request.aaa @@ -0,0 +1,93 @@ +from "util" import make_range + +struct Request { + method as str, + path as str, + headers as map[str, str], + body as str, +} + +enum RequestParseResult { + ok as Request, + err as str, +} + +fn parse_request args raw as str return RequestParseResult { // TODO refactor + Request + + use request { + raw "\r\n" str:split + use lines { + lines 0 vec:get + use first_line { + first_line " " str:split + use split_first_line { + if split_first_line vec:len 3 != { + "could not parse first line" RequestParseResult:err return + } + + request "method" { split_first_line 0 vec:get } ! + request "path" { split_first_line 1 vec:get } ! + } + } + + + -1 + use body_separator_line_offset { + 1 lines vec:len 1 - make_range + foreach { + use offset { + lines offset vec:get + use line { + if line "" str:equals body_separator_line_offset -1 = and { + body_separator_line_offset <- { offset } + } + } + } + } + + 1 body_separator_line_offset make_range + foreach { + use offset { + lines offset vec:get ": " str:split + + use parts { + if parts vec:len 2 != { + drop + "Found invalid line " + lines offset vec:get repr str:append + RequestParseResult:err return + } + + + if parts vec:len 2 >= { + parts 0 vec:get + parts 1 vec:get + use header_name, header_value { + request "headers" ? + header_name str:lower + header_value + map:set + } + } + } + } + } + + "" + use body { + body_separator_line_offset 1 + lines vec:len make_range + foreach { + use offset { + body <- { body lines offset vec:get str:append } + } + } + + request "body" { body } ! + } + } + } + + request RequestParseResult:ok + } +} diff --git a/examples/http/response.aaa b/examples/http/response.aaa new file mode 100644 index 00000000..31481e9a --- /dev/null +++ b/examples/http/response.aaa @@ -0,0 +1,83 @@ + +struct Response { + status_code as int, + content as str, + headers as map[str, str] +} + +fn make_internal_server_error_response return Response { + Response + dup 500 Response:set_status_code + dup "Internal Server Error\n" Response:set_body +} + +fn make_not_found_response return Response { + Response + dup 404 Response:set_status_code + dup "Not Found\n" Response:set_body +} + +fn make_response return Response { + Response + dup "" Response:set_body +} + +fn Response:set_header args response as Response, header_name as str, header_value as str { + response "headers" ? + header_name str:lower + header_value str:lower + map:set +} + +fn Response:set_status_code args response as Response, status_code as int { + response "status_code" { status_code } ! +} + +fn Response:set_body args response as Response, body as str { + response "content" { body } ! + response "Content-Length" body str:len repr Response:set_header +} + +fn Response:set_json_body args response as Response, body as str { + response body Response:set_body + response "Content-Type" "application/json" Response:set_header +} + +fn Response:status_code_to_str args response as Response return str { + response "status_code" ? + use status_code { + if status_code 200 = { "200 OK" return } + if status_code 400 = { "400 Bad Request" return } + if status_code 404 = { "404 Not Found" return } + if status_code 500 = { "500 Internal Server Error" return } + + "WARNING: Can't find string for status code " . + status_code . + "\n" . + + "500 Internal Server Error" + } +} + +fn Response:to_str args response as Response return str { + "HTTP/1.1 " + use raw { + raw <- { raw response Response:status_code_to_str str:append } + raw <- { raw "\r\n" str:append } + + response "headers" ? + foreach { + use name, value { + raw <- { raw name copy swap drop str:append } + raw <- { raw ": " str:append } + raw <- { raw value str:append } + raw <- { raw "\r\n" str:append } + } + } + + raw <- { raw "\r\n" str:append } + raw <- { raw response "content" ? str:append } + + raw + } +} diff --git a/examples/http/router.aaa b/examples/http/router.aaa new file mode 100644 index 00000000..7ace4907 --- /dev/null +++ b/examples/http/router.aaa @@ -0,0 +1,122 @@ +from "request" import Request +from "response" import make_not_found_response, Response + +// TODO move into builtins +fn str_has_prefix args string as str, prefix as str return bool { + string 0 prefix str:len str:substr + use string_prefix, ok { + if ok not { + false return + } + string_prefix prefix str:equals + } +} + +fn get_suffix args path_suffix as str, matched_prefix as str return str { + path_suffix matched_prefix str:len path_suffix str:len str:substr assert +} + +struct EndpointRouterItem { + method as str, + path as str, + handler as fn[Request][Response], +} + +fn make_endpoint_router_item args + method as str, + path as str, + handler as fn[Request][Response], +return EndpointRouterItem { + EndpointRouterItem + dup "method" { method } ! + dup "path" { path } ! + dup "handler" { handler } ! +} + +struct ChildRouterItem { + prefix as str, + child as Router, +} + +fn make_child_router args prefix as str, child as Router return ChildRouterItem { + ChildRouterItem + dup "prefix" { prefix } ! + dup "child" { child } ! +} + +enum RouterItem { + endpoint as EndpointRouterItem, + child_router_item as ChildRouterItem, +} + +fn RouterItem:is_match args + router_item as RouterItem, + request as Request, + path_suffix as str, +return bool { + router_item + match { + case RouterItem:endpoint as endpoint { + request "method" ? endpoint "method" ? str:equals + path_suffix endpoint "path" ? str:equals and + } + case RouterItem:child_router_item as child_router_item { + path_suffix child_router_item "prefix" ? str_has_prefix + } + } +} + +struct Router { + items as vec[RouterItem], +} + +fn Router:add_router args router as Router, prefix as str, child_router as Router { + prefix child_router make_child_router RouterItem:child_router_item + + use router_item { + router "items" ? router_item vec:push + } +} + +fn Router:add_endpoint args router as Router, method as str, path as str, endpoint as fn[Request][Response] { + method path endpoint make_endpoint_router_item RouterItem:endpoint + + use router_item { + router "items" ? router_item vec:push + } +} + +fn Router:_route args router as Router, request as Request, path_suffix as str return Response { + router "items" ? + foreach { + use router_item { + if router_item request path_suffix RouterItem:is_match { + router_item + match { + case RouterItem:endpoint as endpoint { + drop + request endpoint "handler" ? call return + } + case RouterItem:child_router_item as child_router_item { + path_suffix child_router_item "prefix" ? get_suffix + use child_path_suffix { + drop + child_router_item "child" ? + use child_router { + child_router request child_path_suffix Router:_route return + } + } + } + } + } + } + } + make_not_found_response +} + +fn Router:route args router as Router, request as Request return Response { + request "path" ? + use path_suffix { + router request path_suffix Router:_route + } +} diff --git a/examples/http/server.aaa b/examples/http/server.aaa new file mode 100644 index 00000000..75028ded --- /dev/null +++ b/examples/http/server.aaa @@ -0,0 +1,197 @@ +from "request" import parse_request, Request, RequestParseResult +from "router" import Router + +from "response" import + make_internal_server_error_response, + make_not_found_response, + make_response, + Response, + +// used to create socket for IPv4 connections +fn AF_INET return int { + 2 +} + +// used to create TCP connections +fn SOCK_STREAM return int { + 1 +} + +struct WebServer { + port as int, + host as str, + socket_fd as int, + request_handler as RequestHandler, +} + +enum WebServerExitStatus { + ok, + socket_failed, + bind_failed, + listen_failed, + accept_failed, + write_failed, + read_failed, +} + +fn WebServerExitStatus:to_str args status as WebServerExitStatus return str { + status + match { + case WebServerExitStatus:ok { "OK" } + case WebServerExitStatus:socket_failed { "socket failed" } + case WebServerExitStatus:bind_failed { "bind failed" } + case WebServerExitStatus:listen_failed { "listen failed" } + case WebServerExitStatus:accept_failed { "accept failed" } + case WebServerExitStatus:write_failed { "write failed" } + case WebServerExitStatus:read_failed { "read failed" } + } +} + +fn make_web_server args host as str, port as int, request_handler as RequestHandler return WebServer { + WebServer + dup "host" { host } ! + dup "port" { port } ! + dup "request_handler" { request_handler } ! +} + +fn WebServer:run args web_server as WebServer return WebServerExitStatus { + AF_INET SOCK_STREAM 0 socket + use ok { + if ok not { + drop + WebServerExitStatus:socket_failed return + } + } + + use socket_fd { + web_server "socket_fd" { socket_fd } ! + + web_server "host" ? + web_server "port" ? + + use host, port { + socket_fd host port bind + } + use ok { + if ok not { + WebServerExitStatus:bind_failed return + } + } + + socket_fd 5 listen + use ok { + if ok not { + WebServerExitStatus:listen_failed return + } + } + + "Webserver running at " . + web_server "host" ? . + ":" . + web_server "port" ? . + "\n" . + + while true { + socket_fd accept + use ok { + if ok not { + drop drop drop + WebServerExitStatus:accept_failed return + } + } + + use client_ip, client_port, client_fd { + web_server client_ip client_port client_fd WebServer:handle_connection + } + + dup + match { + case WebServerExitStatus:ok { drop } + default { return } + } + } + } +} + +fn WebServer:handle_connection args web_server as WebServer, client_ip as str, client_port as int, client_fd as int return WebServerExitStatus { + // TODO account for larger requests + client_fd 2048 read + + use ok { + if ok not { + drop + WebServerExitStatus:read_failed return + } + } + + use raw_request { + web_server raw_request WebServer:handle_raw_request + } + + use raw_response { + client_fd raw_response write + } + + use ok { + if ok not { + drop + WebServerExitStatus:write_failed return + } + } + + drop + + WebServerExitStatus:ok +} + +fn WebServer:handle_raw_request args web_server as WebServer, raw_request as str return str { + raw_request parse_request + match { + case RequestParseResult:ok as request { + web_server request WebServer:handle_request + } + case RequestParseResult:err as message { + "Error parsing request: " . + message . + "\n" . + make_internal_server_error_response + } + } + + Response:to_str +} + + + +enum RequestHandler { + router as Router, + endpoint as fn[Request][Response], +} + +fn RequestHandler:route args request_handler as RequestHandler, request as Request return Response { + request_handler + match { + case RequestHandler:router as router { router request Router:route } + case RequestHandler:endpoint as endpoint_handler { request endpoint_handler call } + } +} + +fn WebServer:handle_request args web_server as WebServer, request as Request return Response { + request "method" ? . + " " . + request "path" ? . + " " . + + web_server "request_handler" ? + use request_handler { + request_handler request RequestHandler:route + } + + + use response { + response "status_code" ? . + "\n" . + + response + } +} diff --git a/examples/http/util.aaa b/examples/http/util.aaa new file mode 100644 index 00000000..56585190 --- /dev/null +++ b/examples/http/util.aaa @@ -0,0 +1,27 @@ +// TODO #13 move items here to builtins or standard library + +struct Range { + next_value as int, + end as int, +} + +fn make_range args start as int, end as int return Range { + Range + dup "next_value" { start } ! + dup "end" { end } ! +} + +fn Range:iter args r as Range return Range { + r +} + +fn Range:next args r as Range return int, bool { + r "next_value" ? + if dup r "end" ? < { + true + r "next_value" { r "next_value" ? 1 + } ! + } else { + drop 0 + false + } +} diff --git a/examples/http_server.aaa b/examples/http_server.aaa index 2267ad8f..000e0d84 100644 --- a/examples/http_server.aaa +++ b/examples/http_server.aaa @@ -1,70 +1,61 @@ -// used to create socket for IPv4 connections -fn AF_INET return int { - 2 +from "http.request" import Request +from "http.response" import make_internal_server_error_response, make_response, Response +from "http.router" import Router, +from "http.server" import + make_web_server, + RequestHandler, + WebServer, + WebServerExitStatus + +fn hello_world_endpoint args request as Request return Response { + make_response + dup 200 Response:set_status_code + dup "{\"message\": \"Hello world!\"}\n" Response:set_json_body } -// used to create TCP connections -fn SOCK_STREAM return int { - 1 +fn internal_server_error_endpoint args request as Request return Response { + make_internal_server_error_response } -fn make_http_socket return int, bool { - AF_INET SOCK_STREAM 0 socket -} - -fn raw_http_response return str { - "HTTP/1.1 200 OK\r\n" - "Content-Type: application/json\r\n" str:append - "Content-Length: 28\r\n" str:append - "\r\n" str:append - "{\"message\": \"Hello world!\"}\n" str:append +fn bad_request_endpoint args request as Request return Response { + make_response + dup 400 Response:set_status_code + dup "Bad Request\n" Response:set_body } fn main return int { - make_http_socket - use fd, socket_ok { - if socket_ok not { - "socket failed\n" . - 1 return - } + Router - fd "0.0.0.0" 8080 bind - use bind_ok { - if bind_ok not { - "bind failed\n" . - 1 return - } - } + use root_router { + root_router "GET" "/" "hello_world_endpoint" fn Router:add_endpoint + root_router "GET" "/hello" "hello_world_endpoint" fn Router:add_endpoint - fd 5 listen - use listen_ok { - if listen_ok not { - "listen failed\n" . - 1 return - } + Router + use statuses_router { + statuses_router "GET" "/error" "internal_server_error_endpoint" fn Router:add_endpoint + statuses_router "GET" "/bad-request" "bad_request_endpoint" fn Router:add_endpoint + + root_router "/statuses" statuses_router Router:add_router } - while true { - fd accept - use client_ip, client_port, response_fd, accept_ok { - if accept_ok not { - "accept ok\n" . - 1 return - } + root_router RequestHandler:router + } - response_fd raw_http_response write - use written_bytes, write_ok { - if write_ok not { - "write failed\n" . - 1 return - } - } + use request_handler { + "0.0.0.0" 8080 request_handler make_web_server + } - "Sent response to: " . - client_ip . - ":" . - client_port . + WebServer:run + use exit_status { + exit_status + match { + case WebServerExitStatus:ok { + 0 + } + default { + exit_status WebServerExitStatus:to_str . "\n" . + 1 } } } diff --git a/tests/aaa/docs/test_examples.py b/tests/aaa/docs/test_examples.py index cf78f08c..cb531b1a 100644 --- a/tests/aaa/docs/test_examples.py +++ b/tests/aaa/docs/test_examples.py @@ -84,20 +84,45 @@ def test_http_server() -> None: ) assert exit_code == 0 - subproc = subprocess.Popen(binary) + subproc = subprocess.Popen(binary, stdout=subprocess.PIPE) + + stdout = subproc.stdout + assert stdout + + # Wait until webserver prints first line, indicating server is running + first_line = stdout.readline().decode() + assert "Webserver running at" in first_line try: r = requests.get("http://localhost:8080") assert r.status_code == 200 assert r.json() == {"message": "Hello world!"} assert r.headers["Content-Type"] == "application/json" + + r = requests.get("http://localhost:8080/") + assert r.status_code == 200 + assert r.json() == {"message": "Hello world!"} + assert r.headers["Content-Type"] == "application/json" + + r = requests.get("http://localhost:8080/statuses/error") + assert r.status_code == 500 + assert r.text == "Internal Server Error\n" + + r = requests.get("http://localhost:8080/statuses/bad-request") + assert r.status_code == 400 + assert r.text == "Bad Request\n" + + r = requests.get("http://localhost:8080/foo") + assert r.status_code == 404 + assert r.text == "Not Found\n" + finally: subproc.terminate() subproc.wait() def test_http_client(capfd: CaptureFixture[str]) -> None: - entrypoint = Path("examples/http_client.aaa") + entrypoint = Path("examples/http/client.aaa") runner = Runner(entrypoint) runner.run( compile=True, binary_path=None, run=True, args=[], runtime_type_checks=True From 7dd0ac31bf4074fbfbd18736737d815dbddedd70 Mon Sep 17 00:00:00 2001 From: Luuk Verweij Date: Sun, 10 Dec 2023 21:23:16 +0100 Subject: [PATCH 3/4] wip --- examples/http/client.aaa | 43 ++------------------------------- examples/http_client.aaa | 39 ++++++++++++++++++++++++++++++ tests/aaa/docs/test_examples.py | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 examples/http_client.aaa diff --git a/examples/http/client.aaa b/examples/http/client.aaa index 3b9468ef..5364caf4 100644 --- a/examples/http/client.aaa +++ b/examples/http/client.aaa @@ -15,46 +15,7 @@ fn make_http_socket return int, bool { fn raw_http_request return str { // TODO use Request from request.aaa "GET / HTTP/1.1\r\n" - "Host: icanhazip.com\r\n" str:append - "User-Agent: curl/7.68.0\r\n" str:append + "Host: icanhazip.com\r\n" str:append // TODO don't hardcode host + "User-Agent: curl/7.68.0\r\n" str:append // TODO change User-Agent "Accept: */*\r\n\r\n" str:append } - -// TODO move main out of http package -fn main return int { - make_http_socket - use fd, socket_ok { - if socket_ok not { - "socket failed\n" . - 1 return - } - - fd "icanhazip.com" 80 connect - use connect_ok { - if connect_ok not { - "connect failed\n" . - 1 return - } - } - - fd raw_http_request write - use written, write_ok { - if write_ok not { - "write failed\n" . - 1 return - } - } - - fd 2048 read - use response, read_ok { - if read_ok not { - "read failed\n" . - 1 return - } - - response . - } - } - - 0 -} diff --git a/examples/http_client.aaa b/examples/http_client.aaa new file mode 100644 index 00000000..79888e0a --- /dev/null +++ b/examples/http_client.aaa @@ -0,0 +1,39 @@ +from "http.client" import make_http_socket, raw_http_request + +fn main return int { + make_http_socket + use fd, socket_ok { + if socket_ok not { + "socket failed\n" . + 1 return + } + + fd "icanhazip.com" 80 connect + use connect_ok { + if connect_ok not { + "connect failed\n" . + 1 return + } + } + + fd raw_http_request write + use written, write_ok { + if write_ok not { + "write failed\n" . + 1 return + } + } + + fd 2048 read + use response, read_ok { + if read_ok not { + "read failed\n" . + 1 return + } + + response . + } + } + + 0 +} diff --git a/tests/aaa/docs/test_examples.py b/tests/aaa/docs/test_examples.py index cb531b1a..e1f6c616 100644 --- a/tests/aaa/docs/test_examples.py +++ b/tests/aaa/docs/test_examples.py @@ -122,7 +122,7 @@ def test_http_server() -> None: def test_http_client(capfd: CaptureFixture[str]) -> None: - entrypoint = Path("examples/http/client.aaa") + entrypoint = Path("examples/http_client.aaa") runner = Runner(entrypoint) runner.run( compile=True, binary_path=None, run=True, args=[], runtime_type_checks=True From 321dc36395df89c5f831b15477dd7f343e2618c3 Mon Sep 17 00:00:00 2001 From: Luuk Verweij Date: Sun, 10 Dec 2023 21:26:41 +0100 Subject: [PATCH 4/4] wip --- examples/http/client.aaa | 10 +++++++--- examples/http_client.aaa | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/http/client.aaa b/examples/http/client.aaa index 5364caf4..10a54b73 100644 --- a/examples/http/client.aaa +++ b/examples/http/client.aaa @@ -13,9 +13,13 @@ fn make_http_socket return int, bool { AF_INET SOCK_STREAM 0 socket } -fn raw_http_request return str { // TODO use Request from request.aaa +fn raw_http_request args host as str return str { // TODO use Request from request.aaa "GET / HTTP/1.1\r\n" - "Host: icanhazip.com\r\n" str:append // TODO don't hardcode host - "User-Agent: curl/7.68.0\r\n" str:append // TODO change User-Agent + + "Host: " str:append + host str:append + "\r\n" str:append + + "User-Agent: Aaa\r\n" str:append "Accept: */*\r\n\r\n" str:append } diff --git a/examples/http_client.aaa b/examples/http_client.aaa index 79888e0a..a1ea2f7f 100644 --- a/examples/http_client.aaa +++ b/examples/http_client.aaa @@ -1,6 +1,8 @@ from "http.client" import make_http_socket, raw_http_request fn main return int { + // TODO refactor this so we can just call one function that sends an http request + make_http_socket use fd, socket_ok { if socket_ok not { @@ -16,7 +18,7 @@ fn main return int { } } - fd raw_http_request write + fd "icanhazip.com" raw_http_request write use written, write_ok { if write_ok not { "write failed\n" .