1
1
Fork 0
forked from fun/fun

Added HTTP library http.fun and an examples script. (0.42.2)

This commit is contained in:
Johannes Findeisen 2026-07-04 19:54:07 +02:00
commit 302caf3ce7
3 changed files with 403 additions and 0 deletions

View file

@ -0,0 +1,31 @@
/*
* Example: Simple HTTP server using stdlib net/http_server
*
* Run:
* $ fun examples/blocking/net/http.fun
*
* It will serve files from the ./web directory on http://127.0.0.1:8080
*
* Test with curl in another terminal:
* $ curl -v http://127.0.0.1:8080/
* $ curl -v http://127.0.0.1:8080/index.html
*
* You can also place a .fun script in ./web (e.g. hello.fun) and call it:
* $ curl -v 'http://127.0.0.1:8080/hello.fun?name=Fun'
*/
#include <net/http_server.fun>
// Configure
PORT = 8080
DOC_ROOT = "./examples/data/htdocs/"
print("Starting simple HTTP server example…")
print("Serving '" + DOC_ROOT + "' on http://127.0.0.1:" + to_string(PORT))
print("Press Ctrl+C to stop.")
srv = HTTPServer(PORT)
srv.set_htdocs(DOC_ROOT)
// Start (blocking loop)
srv.start()

370
lib/net/http.fun Normal file
View file

@ -0,0 +1,370 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2026 Johannes Findeisen
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2026-07-04
*/
#include <io/socket.fun>
#include <strings.fun>
/*
* HTTP — minimal HTTP/1.1 server library (Phase 1)
*
* Goals for Phase 1:
* - HTTP/1.1 request parsing (request-line, headers)
* - Message body handling via Content-Length and Transfer-Encoding: chunked
* - Persistent connections (keep-alive) with Connection: close handling
* - Simple handler callback API: handler(req) -> res
* - No TLS, no pipelining, no compression, no auth (to be added later)
*
* API overview:
* class HTTP(number port)
* fun start(this, handler)
* - handler: function taking `req` map and returning `res` map
*
* Request map `req` fields:
* - method: string
* - target: raw request-target (string)
* - path: path part without query (string)
* - query: query string without '?' (string)
* - version: "HTTP/1.1" or "HTTP/1.0"
* - headers: map<string,string> (lower-cased header names)
* - body: string (for Phase 1; streaming may arrive later)
* - remote_fd: number (client socket fd)
*
* Response map `res` fields:
* - status: number (e.g. 200)
* - reason: optional string (defaults from common codes)
* - headers: optional map<string,string>
* - body: optional string (defaults to empty)
*/
// Utility: common status text
fun _http_reason(status)
code = to_number(status)
if (code == 200) return "OK"
if (code == 201) return "Created"
if (code == 202) return "Accepted"
if (code == 204) return "No Content"
if (code == 301) return "Moved Permanently"
if (code == 302) return "Found"
if (code == 303) return "See Other"
if (code == 304) return "Not Modified"
if (code == 307) return "Temporary Redirect"
if (code == 308) return "Permanent Redirect"
if (code == 400) return "Bad Request"
if (code == 401) return "Unauthorized"
if (code == 403) return "Forbidden"
if (code == 404) return "Not Found"
if (code == 405) return "Method Not Allowed"
if (code == 408) return "Request Timeout"
if (code == 411) return "Length Required"
if (code == 413) return "Payload Too Large"
if (code == 414) return "URI Too Long"
if (code == 415) return "Unsupported Media Type"
if (code == 417) return "Expectation Failed"
if (code == 426) return "Upgrade Required"
if (code == 431) return "Request Header Fields Too Large"
if (code == 500) return "Internal Server Error"
if (code == 501) return "Not Implemented"
if (code == 503) return "Service Unavailable"
if (code == 505) return "HTTP Version Not Supported"
return ""
// Utility: write full response and return whether connection should stay open
fun _http_write_response(fd, res, keep_alive)
status = to_number(res["status"])
reason = res["reason"]
if (len(to_string(reason)) == 0)
reason = _http_reason(status)
if (len(reason) == 0)
reason = "OK"
body = res["body"]
if (typeof(body) != "string")
body = to_string(body)
blen = len(body)
headers = res["headers"]
if (typeof(headers) != "map")
headers = {}
// Ensure Content-Length for simple bodies (no streaming in Phase 1)
headers["content-length"] = to_string(blen)
if (len(to_string(headers["content-type"])) == 0)
headers["content-type"] = "text/plain; charset=utf-8"
// Connection header per keep-alive flag
if (keep_alive)
headers["connection"] = "keep-alive"
else
headers["connection"] = "close"
// Build response string
start = "HTTP/1.1 " + to_string(status) + " " + reason + "\r\n"
head = ""
// Normalize: send headers with canonical casing best-effort
// but here we just send lower-case names as kept
for (k in keys(headers))
v = to_string(headers[k])
// join: "name: value\r\n"
head = head + k + ": " + v + "\r\n"
resp = start + head + "\r\n" + body
sock_send(fd, resp)
return keep_alive
// Utility: decode chunked transfer-encoding body from a buffer and fd (if needs more)
fun _http_read_chunked(fd, after_headers, max_body)
// after_headers: initial buffer containing any bytes after CRLFCRLF
data = after_headers
body = ""
// Read line by line: <hex-size> [;ext] CRLF, then that many bytes, then CRLF, repeat, then trailer part ends with CRLF CRLF
// For simplicity, we ignore chunk extensions and trailers in Phase 1.
while true
// ensure we have a full line
while find(data, "\r\n") < 0
more = sock_recv(fd, 4096)
if (len(more) == 0) return {"ok": 0, "body": body}
data = data + more
if (max_body > 0 && len(body) + len(data) > max_body) return {"ok": 0, "body": body}
pos = find(data, "\r\n")
line = substr(data, 0, pos)
data = substr(data, pos + 2, len(data) - pos - 2)
semi = find(line, ";")
size_str = line
if (semi >= 0)
size_str = substr(line, 0, semi)
size_str = str_trim(size_str)
// hex to number
// Fun doesn’t have hex parse; do manual
n = 0
i = 0
while i < len(size_str)
ch = substr(size_str, i, 1)
cl = str_to_lower(ch)
d = -1
if (cl >= "0" && cl <= "9") d = to_number(cl) - to_number("0")
else if (cl >= "a" && cl <= "f") d = 10 + (to_number(cl) - to_number("a"))
if (d < 0)
return {"ok": 0, "body": body}
n = n * 16 + d
i = i + 1
// read exactly n bytes
while len(data) < n + 2
more2 = sock_recv(fd, 4096)
if (len(more2) == 0) return {"ok": 0, "body": body}
data = data + more2
if (max_body > 0 && len(body) + len(data) > max_body) return {"ok": 0, "body": body}
chunk = substr(data, 0, n)
crlf = substr(data, n, 2)
// Must be CRLF after chunk
if (crlf != "\r\n") return {"ok": 0, "body": body}
data = substr(data, n + 2, len(data) - n - 2)
body = body + chunk
if (max_body > 0 && len(body) > max_body) return {"ok": 0, "body": body}
if (n == 0)
// reach end; ignore trailers (until CRLFCRLF)
// optional: consume trailers
// try to find CRLFCRLF in remaining data; if not there, read minimal amount
// but for Phase 1 we stop here
return {"ok": 1, "body": body}
class HTTP(number port)
// Configuration limits (conservative defaults)
max_header_bytes = 32 * 1024
max_start_line_bytes = 8 * 1024
max_body_bytes = 8 * 1024 * 1024 // 8 MiB for Phase 1
read_chunk_size = 4096
fun _construct(this, port)
this.port = port
this.server = TcpServer(port, 128)
// Start the HTTP server. `handler` is a function: handler(req) -> res
fun start(this, handler)
if (this.server.listen() <= 0)
print("HTTP: failed to listen on port " + to_string(this.port))
return 0
print("HTTP: listening on port " + to_string(this.port))
while true
cfd = this.server.accept()
if (cfd > 0)
this._handle_connection(cfd, handler)
return 1
// Connection loop with keep-alive
fun _handle_connection(this, fd, handler)
// read-buffer that may carry over data between requests on same socket
carry = ""
while true
parsed = this._read_and_parse_request(fd, carry)
if (parsed["ok"] != 1)
break
req = parsed["req"]
carry = parsed["carry"]
// handler returns res map
res = handler(req)
if (typeof(res) != "map")
res = {"status": 500, "reason": "Internal Server Error", "body": "Handler error"}
// decide keep-alive
h_conn = req["headers"]["connection"]
ver = req["version"]
// default keep-alive for HTTP/1.1 unless Connection: close
keep = (ver == "HTTP/1.1") && (str_to_lower(to_string(h_conn)) != "close")
if (str_to_lower(to_string(h_conn)) == "keep-alive")
keep = true
if (str_to_lower(to_string(h_conn)) == "close")
keep = false
stayed = _http_write_response(fd, res, keep)
if (!stayed)
break
sock_close(fd)
return 1
// Reads from fd until a full request is available or error; returns {ok, req, carry}
fun _read_and_parse_request(this, fd, carry)
data = carry
// Accumulate until we see end of headers (CRLFCRLF or LFLF)
// also enforce size limits
while true
// Check if headers end exists
p = find(data, "\r\n\r\n")
if (p < 0)
p = find(data, "\n\n")
if (p >= 0)
break
if (len(data) > this.max_header_bytes + this.max_start_line_bytes)
return {"ok": 0}
more = sock_recv(fd, this.read_chunk_size)
if (len(more) == 0)
return {"ok": 0}
data = data + more
// Split start-line and headers
// Find first line break
lf = find(data, "\n")
if (lf < 0 || lf > this.max_start_line_bytes)
return {"ok": 0}
// Handle CRLF or LF
first_line = substr(data, 0, (lf > 0 && substr(data, lf-1, 1) == "\r") ? lf-1 : lf)
headers_block = substr(data, (substr(data, lf-1, 1) == "\r") ? lf+1 : lf+1, // start after first line CRLF/LF
0) // placeholder, will correct below
// Find header-body separator
sep = find(data, "\r\n\r\n")
sep_len = 4
if (sep < 0)
sep = find(data, "\n\n")
sep_len = 2
headers_raw = substr(data, (substr(data, lf-1, 1) == "\r") ? lf+1 : lf+1, sep - ((substr(data, lf-1, 1) == "\r") ? (lf+1) : (lf+1)))
after = substr(data, sep + sep_len, len(data) - sep - sep_len)
// Parse start-line: METHOD SP request-target SP HTTP/version
parts = str_split(first_line, " ")
if (len(parts) < 3)
return {"ok": 0}
method = str_trim(parts[0])
target = str_trim(parts[1])
version = str_trim(parts[2])
if (version != "HTTP/1.1" && version != "HTTP/1.0")
return {"ok": 0}
// Parse headers into map with lower-cased names; ignore obs-fold
hdr_lines = str_split(headers_raw, "\n")
headers = {}
i = 0
while i < len(hdr_lines)
line = hdr_lines[i]
line = str_trim(line)
if (len(line) == 0)
i = i + 1
continue
colon = find(line, ":")
if (colon <= 0)
// invalid header; reject
return {"ok": 0}
name = str_to_lower(str_trim(substr(line, 0, colon)))
value = str_trim(substr(line, colon + 1, len(line) - colon - 1))
// merge duplicates with comma
prev = headers[name]
if (len(to_string(prev)) > 0)
headers[name] = prev + ", " + value
else
headers[name] = value
i = i + 1
// Handle request-target: split path and query
path = target
query = ""
qpos = find(target, "?")
if (qpos >= 0)
path = substr(target, 0, qpos)
query = substr(target, qpos + 1, len(target) - qpos - 1)
// Determine body
body = ""
// transfer-encoding has precedence
te = str_to_lower(to_string(headers["transfer-encoding"]))
cl = to_string(headers["content-length"])
if (len(te) > 0)
if (te == "chunked")
dec = _http_read_chunked(fd, after, this.max_body_bytes)
if (dec["ok"] != 1)
return {"ok": 0}
body = dec["body"]
after = ""
else
// unsupported TE
return {"ok": 0}
else if (len(cl) > 0)
// parse numeric Content-Length
n = 0
j = 0
while j < len(cl)
ch = substr(cl, j, 1)
if (!(ch >= "0" && ch <= "9"))
return {"ok": 0}
n = n * 10 + (to_number(ch) - to_number("0"))
j = j + 1
if (n > this.max_body_bytes)
return {"ok": 0}
// ensure we have n bytes in `after`, otherwise read
while len(after) < n
moreb = sock_recv(fd, this.read_chunk_size)
if (len(moreb) == 0)
return {"ok": 0}
after = after + moreb
if (len(after) > this.max_body_bytes)
return {"ok": 0}
body = substr(after, 0, n)
after = substr(after, n, len(after) - n)
else
// no body; leave `after` as carry for potential next requests
body = ""
req = {
"method": method,
"target": target,
"path": path,
"query": query,
"version": version,
"headers": headers,
"body": body,
"remote_fd": fd
}
return {"ok": 1, "req": req, "carry": after}

View file

@ -19,6 +19,8 @@ permalink: /
<h2>What is Fun?</h2>
<p>This is volunteer-maintained open-source software. Help is welcome. Personal attacks, memes and broad outrage in issue threads will not be. Please remain respectful.</p>
<p>Fun is a small, strict, and simple programming language that runs on a compact stack-based virtual machine. The C core is intentionally minimal; most functionality and standard libraries are implemented in Fun itself.</p>
<p>Fun is dynamically typed with optional static type annotations, <a href="/features/">featuring first-class functions</a>, classes with inheritance, pattern matching, and a rich standard library. It supports everything from basic scripting to TCP sockets, serial communication, threading, cryptography (MD5, SHA-1/256/384/512, CRC-32, AES-256), and a built-in debugger.</p>