More CGI Fun. (0.39.7)
This commit is contained in:
parent
db9665f62a
commit
3b70d6c92a
7 changed files with 494 additions and 175 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(fun VERSION 0.39.6 LANGUAGES C)
|
project(fun VERSION 0.39.7 LANGUAGES C)
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 99)
|
set(CMAKE_C_STANDARD 99)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
|
||||||
|
|
@ -13,184 +13,18 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Minimal CGI-capable HTTP server (blocking)
|
* Minimal CGI-capable HTTP server (blocking)
|
||||||
|
*
|
||||||
|
* Try:
|
||||||
|
* - http://127.0.0.1:8080/
|
||||||
|
* - http://127.0.0.1:8080/hello.fun?name=Fun
|
||||||
|
+ - http://127.0.0.1:8080/info.fun?name=Fun
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <net/http_server.fun>
|
#include <net/http_cgi_server.fun>
|
||||||
#include <io/socket.fun>
|
|
||||||
#include <strings.fun>
|
|
||||||
|
|
||||||
port = 8080
|
port = 8080
|
||||||
htdocs = "./examples/data/htdocs"
|
htdocs = "./examples/data/htdocs"
|
||||||
|
|
||||||
// Lightweight inline HTTP server: reuse TcpServer directly so we can inject CGI env
|
|
||||||
class HTTPCGIServer(number port)
|
|
||||||
fun _construct(this, port)
|
|
||||||
this.port = port
|
|
||||||
this.srv = TcpServer(port, 10)
|
|
||||||
this.htdocs = "./"
|
|
||||||
|
|
||||||
fun set_htdocs(this, path)
|
|
||||||
this.htdocs = to_string(path)
|
|
||||||
|
|
||||||
fun start(this)
|
|
||||||
if (this.srv.listen() <= 0)
|
|
||||||
print("HTTP CGI Server: failed to listen on port " + to_string(this.port))
|
|
||||||
return 0
|
|
||||||
print("HTTP CGI Server: serving " + this.htdocs + " on port " + to_string(this.port))
|
|
||||||
while true
|
|
||||||
fd = this.srv.accept()
|
|
||||||
if (fd > 0)
|
|
||||||
this.handle_client(fd)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
fun _parse_headers(this, request)
|
|
||||||
headers = {}
|
|
||||||
lines = str_split(request, "\n")
|
|
||||||
i = 1 // start after request line
|
|
||||||
while (i < len(lines))
|
|
||||||
ln = str_trim(lines[i])
|
|
||||||
if (len(ln) == 0)
|
|
||||||
break
|
|
||||||
colon = find(ln, ":")
|
|
||||||
if (colon > 0)
|
|
||||||
k = str_to_upper(str_trim(substr(ln, 0, colon)))
|
|
||||||
v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
|
|
||||||
headers[k] = v
|
|
||||||
i = i + 1
|
|
||||||
return headers
|
|
||||||
|
|
||||||
fun handle_client(this, fd)
|
|
||||||
request = sock_recv(fd, 65536)
|
|
||||||
if (len(request) == 0)
|
|
||||||
sock_close(fd)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
lines = str_split(request, "\n")
|
|
||||||
if (len(lines) == 0)
|
|
||||||
sock_close(fd)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
reqline = str_trim(lines[0])
|
|
||||||
parts = str_split(reqline, " ")
|
|
||||||
if (len(parts) < 2)
|
|
||||||
sock_close(fd)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
method = str_trim(parts[0])
|
|
||||||
target = str_trim(parts[1])
|
|
||||||
|
|
||||||
// Split query string
|
|
||||||
path = target
|
|
||||||
query = ""
|
|
||||||
q = find(target, "?")
|
|
||||||
if (q >= 0)
|
|
||||||
path = substr(target, 0, q)
|
|
||||||
query = substr(target, q + 1, len(target) - q - 1)
|
|
||||||
|
|
||||||
if (path == "/")
|
|
||||||
path = "/index.html"
|
|
||||||
|
|
||||||
file_path = this.htdocs + path
|
|
||||||
|
|
||||||
// Headers and body
|
|
||||||
headers = this._parse_headers(request)
|
|
||||||
|
|
||||||
// Extract POST body
|
|
||||||
body = ""
|
|
||||||
header_end = find(request, "\r\n\r\n")
|
|
||||||
if (header_end >= 0)
|
|
||||||
body = substr(request, header_end + 4, len(request) - header_end - 4)
|
|
||||||
else
|
|
||||||
header_end = find(request, "\n\n")
|
|
||||||
if (header_end >= 0)
|
|
||||||
body = substr(request, header_end + 2, len(request) - header_end - 2)
|
|
||||||
|
|
||||||
// Serve CGI when file ends with .fun
|
|
||||||
if (str_ends_with(path, ".fun"))
|
|
||||||
// Build env vars according to CGI conventions
|
|
||||||
host = headers["HOST"]
|
|
||||||
if (len(host) == 0) host = "localhost"
|
|
||||||
ua = headers["USER-AGENT"]
|
|
||||||
cookie = headers["COOKIE"]
|
|
||||||
ctype = headers["CONTENT-TYPE"]
|
|
||||||
clen = headers["CONTENT-LENGTH"]
|
|
||||||
|
|
||||||
envs = []
|
|
||||||
// Ensure Fun stdlib is available to CGI child process
|
|
||||||
funlib = env("FUN_LIB_DIR")
|
|
||||||
if (len(funlib) == 0)
|
|
||||||
// Best-effort default when running from project root
|
|
||||||
funlib = "./lib"
|
|
||||||
push(envs, "FUN_LIB_DIR='" + funlib + "'")
|
|
||||||
push(envs, "REQUEST_METHOD='" + method + "'")
|
|
||||||
push(envs, "QUERY_STRING='" + query + "'")
|
|
||||||
push(envs, "SCRIPT_NAME='" + path + "'")
|
|
||||||
push(envs, "PATH_INFO='" + path + "'")
|
|
||||||
push(envs, "SERVER_NAME='" + host + "'")
|
|
||||||
push(envs, "SERVER_PORT='" + to_string(this.port) + "'")
|
|
||||||
push(envs, "SERVER_PROTOCOL='HTTP/1.1'")
|
|
||||||
push(envs, "HTTP_HOST='" + host + "'")
|
|
||||||
if (len(ua) > 0)
|
|
||||||
push(envs, "HTTP_USER_AGENT='" + ua + "'")
|
|
||||||
if (len(cookie) > 0)
|
|
||||||
push(envs, "HTTP_COOKIE='" + cookie + "'")
|
|
||||||
if (len(ctype) > 0)
|
|
||||||
push(envs, "CONTENT_TYPE='" + ctype + "'")
|
|
||||||
if (len(clen) > 0)
|
|
||||||
push(envs, "CONTENT_LENGTH='" + clen + "'")
|
|
||||||
if (len(body) > 0)
|
|
||||||
push(envs, "POST_DATA='" + body + "'")
|
|
||||||
|
|
||||||
// Decide which Fun interpreter to use for the CGI child
|
|
||||||
// Priority:
|
|
||||||
// 1) $FUN_EXEC (explicit override)
|
|
||||||
// 2) ./build_debug/fun (when running from project root)
|
|
||||||
// 3) ./build_release/fun (release build)
|
|
||||||
// 4) fun (from PATH)
|
|
||||||
exec = env("FUN_EXEC")
|
|
||||||
if (len(exec) == 0)
|
|
||||||
// crude existence check using read_file; good enough here
|
|
||||||
if (len(read_file("./build_debug/fun")) > 0)
|
|
||||||
exec = "./build_debug/fun"
|
|
||||||
else
|
|
||||||
if (len(read_file("./build_release/fun")) > 0)
|
|
||||||
exec = "./build_release/fun"
|
|
||||||
else
|
|
||||||
exec = "fun"
|
|
||||||
|
|
||||||
// Run the Fun script as a CGI program
|
|
||||||
cmd = join(envs, " ") + " " + exec + " " + file_path
|
|
||||||
res = proc_run(cmd)
|
|
||||||
out = res["out"]
|
|
||||||
|
|
||||||
if (len(out) == 0)
|
|
||||||
this._send(fd, 500, "Internal Server Error", "<h1>CGI produced no output</h1>")
|
|
||||||
else
|
|
||||||
// If CGI output already includes HTTP headers, pass through
|
|
||||||
// Otherwise, wrap it in a basic 200 response.
|
|
||||||
if (find(out, "\r\n\r\n") >= 0 || find(out, "\n\n") >= 0)
|
|
||||||
sock_send(fd, out)
|
|
||||||
else
|
|
||||||
this._send(fd, 200, "OK", out)
|
|
||||||
else
|
|
||||||
// Static file
|
|
||||||
content = read_file(file_path)
|
|
||||||
if (len(content) > 0)
|
|
||||||
this._send(fd, 200, "OK", content)
|
|
||||||
else
|
|
||||||
this._send(fd, 404, "Not Found", "<h1>404 Not Found</h1>")
|
|
||||||
|
|
||||||
sock_close(fd)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
fun _send(this, fd, code, text, body)
|
|
||||||
b = to_string(body)
|
|
||||||
resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
|
|
||||||
resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
|
|
||||||
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
|
||||||
resp = resp + "Connection: close\r\n\r\n" + b
|
|
||||||
sock_send(fd, resp)
|
|
||||||
|
|
||||||
server = HTTPCGIServer(port)
|
server = HTTPCGIServer(port)
|
||||||
server.set_htdocs(htdocs)
|
server.set_htdocs(htdocs)
|
||||||
server.start()
|
server.start()
|
||||||
|
|
|
||||||
31
examples/blocking/http_server_cgi_lib.fun
Executable file
31
examples/blocking/http_server_cgi_lib.fun
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env fun
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Fun programming language.
|
||||||
|
* https://fun-lang.xyz/
|
||||||
|
*
|
||||||
|
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||||
|
* Licensed under the terms of the Apache-2.0 license.
|
||||||
|
* https://opensource.org/license/apache-2-0
|
||||||
|
*
|
||||||
|
* Added: 2026-03-25
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Minimal blocking HTTP server that serves static files and
|
||||||
|
* handles a built-in CGI endpoint using the net/cgi.fun library.
|
||||||
|
*
|
||||||
|
* Try:
|
||||||
|
* - http://127.0.0.1:8080/
|
||||||
|
* - http://127.0.0.1:8080/hello.fun?name=Fun
|
||||||
|
+ - http://127.0.0.1:8080/info.fun?name=Fun
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <net/http_cgi_lib_server.fun>
|
||||||
|
|
||||||
|
port = 8080
|
||||||
|
htdocs = "./examples/data/htdocs"
|
||||||
|
|
||||||
|
server = HTTPCGILibServer(port)
|
||||||
|
server.set_htdocs(htdocs)
|
||||||
|
server.start()
|
||||||
|
|
@ -32,7 +32,22 @@ html = html + "<h1>Hello, " + cgi.escape_html(name) + "!</h1>"
|
||||||
html = html + "<p>REQUEST_METHOD: " + cgi.escape_html(cgi.env["REQUEST_METHOD"]) + "</p>"
|
html = html + "<p>REQUEST_METHOD: " + cgi.escape_html(cgi.env["REQUEST_METHOD"]) + "</p>"
|
||||||
html = html + "<p>QUERY_STRING: " + cgi.escape_html(cgi.env["QUERY_STRING"]) + "</p>"
|
html = html + "<p>QUERY_STRING: " + cgi.escape_html(cgi.env["QUERY_STRING"]) + "</p>"
|
||||||
html = html + "<p>User-Agent cookie: " + cgi.escape_html(ua) + "</p>"
|
html = html + "<p>User-Agent cookie: " + cgi.escape_html(ua) + "</p>"
|
||||||
html = html + "<h2>Params</h2><pre>" + to_string(cgi.params()) + "</pre>"
|
// Render parsed CGI params as key => value(s)
|
||||||
|
pmap = cgi.params()
|
||||||
|
html = html + "<h2>Params</h2><ul>"
|
||||||
|
for k in keys(pmap)
|
||||||
|
vals = cgi.param_all(k)
|
||||||
|
// Join multiple values with ", "
|
||||||
|
i = 0
|
||||||
|
n = len(vals)
|
||||||
|
joined = ""
|
||||||
|
while (i < n)
|
||||||
|
if (i > 0)
|
||||||
|
joined = joined + ", "
|
||||||
|
joined = joined + cgi.escape_html(vals[i])
|
||||||
|
i = i + 1
|
||||||
|
html = html + "<li><b>" + cgi.escape_html(k) + "</b>: " + joined + "</li>"
|
||||||
|
html = html + "</ul>"
|
||||||
html = html + "</body></html>"
|
html = html + "</body></html>"
|
||||||
|
|
||||||
// Send CGI headers + body
|
// Send CGI headers + body
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,81 @@ class CGI()
|
||||||
a = str_replace_all(a, "'", "'")
|
a = str_replace_all(a, "'", "'")
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
// Translate CGI output (headers + body) into a full HTTP/1.1 response string
|
||||||
|
// Input: raw string as emitted by a CGI script (e.g., via CGI.send()), containing
|
||||||
|
// CGI headers followed by an empty line and then the body.
|
||||||
|
// Output: a single HTTP/1.1 response string ready to be written to a socket.
|
||||||
|
fun cgi_to_http_response(this, raw)
|
||||||
|
out = to_string(raw)
|
||||||
|
// Find header/body separator
|
||||||
|
sep = find(out, "\r\n\r\n")
|
||||||
|
seplen = 4
|
||||||
|
if (sep < 0)
|
||||||
|
sep = find(out, "\n\n")
|
||||||
|
seplen = 2
|
||||||
|
if (sep < 0)
|
||||||
|
// No CGI headers, treat whole as body
|
||||||
|
b = out
|
||||||
|
resp = "HTTP/1.1 200 OK\r\n"
|
||||||
|
resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
|
||||||
|
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
||||||
|
resp = resp + "Connection: close\r\n\r\n" + b
|
||||||
|
return resp
|
||||||
|
|
||||||
|
header_str = substr(out, 0, sep)
|
||||||
|
body = substr(out, sep + seplen, len(out) - sep - seplen)
|
||||||
|
|
||||||
|
// Parse headers
|
||||||
|
lines = str_split(header_str, "\n")
|
||||||
|
code = 200
|
||||||
|
text = "OK"
|
||||||
|
// Accumulate headers (excluding Status)
|
||||||
|
hh = [] // array of [k, v]
|
||||||
|
i = 0
|
||||||
|
n = len(lines)
|
||||||
|
while (i < n)
|
||||||
|
ln = str_trim(lines[i])
|
||||||
|
if (len(ln) > 0)
|
||||||
|
colon = find(ln, ":")
|
||||||
|
if (colon > 0)
|
||||||
|
k = str_trim(substr(ln, 0, colon))
|
||||||
|
v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
|
||||||
|
if (str_to_upper(k) == "STATUS")
|
||||||
|
// Expect like: 200 OK
|
||||||
|
sp = find(v, " ")
|
||||||
|
if (sp > 0)
|
||||||
|
code = to_number(substr(v, 0, sp))
|
||||||
|
text = str_trim(substr(v, sp + 1, len(v) - sp - 1))
|
||||||
|
else
|
||||||
|
code = to_number(v)
|
||||||
|
if (code == 0) code = 200
|
||||||
|
text = "OK"
|
||||||
|
else
|
||||||
|
// Keep other headers
|
||||||
|
push(hh, [k, v])
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
// Build HTTP response
|
||||||
|
b = to_string(body)
|
||||||
|
resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
|
||||||
|
// Emit collected headers
|
||||||
|
j = 0
|
||||||
|
m = len(hh)
|
||||||
|
has_len = false
|
||||||
|
while (j < m)
|
||||||
|
p = hh[j]
|
||||||
|
if (typeof(p) == "Array" && len(p) >= 2)
|
||||||
|
hk = to_string(p[0])
|
||||||
|
hv = to_string(p[1])
|
||||||
|
if (str_to_upper(hk) == "CONTENT-LENGTH")
|
||||||
|
has_len = true
|
||||||
|
resp = resp + hk + ": " + hv + "\r\n"
|
||||||
|
j = j + 1
|
||||||
|
if (!has_len)
|
||||||
|
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
||||||
|
resp = resp + "Connection: close\r\n\r\n" + b
|
||||||
|
return resp
|
||||||
|
|
||||||
// Minimal url-decoder: '+' -> space; %XX for ASCII printable
|
// Minimal url-decoder: '+' -> space; %XX for ASCII printable
|
||||||
fun url_decode(this, s)
|
fun url_decode(this, s)
|
||||||
src = to_string(s)
|
src = to_string(s)
|
||||||
|
|
|
||||||
185
lib/net/http_cgi_lib_server.fun
Normal file
185
lib/net/http_cgi_lib_server.fun
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* 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-03-25
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Blocking HTTP server that uses Fun interpreter to execute .fun CGI files
|
||||||
|
|
||||||
|
#include <io/socket.fun>
|
||||||
|
#include <strings.fun>
|
||||||
|
#include <net/cgi.fun>
|
||||||
|
|
||||||
|
class HTTPCGILibServer(number port)
|
||||||
|
fun _construct(this, port)
|
||||||
|
this.port = port
|
||||||
|
this.srv = TcpServer(port, 10)
|
||||||
|
this.htdocs = "./"
|
||||||
|
|
||||||
|
fun set_htdocs(this, path)
|
||||||
|
this.htdocs = to_string(path)
|
||||||
|
|
||||||
|
fun start(this)
|
||||||
|
if (this.srv.listen() <= 0)
|
||||||
|
print("HTTP CGI(lib) Server: failed to listen on port " + to_string(this.port))
|
||||||
|
return 0
|
||||||
|
print("HTTP CGI(lib) Server: serving " + this.htdocs + " on port " + to_string(this.port))
|
||||||
|
while true
|
||||||
|
fd = this.srv.accept()
|
||||||
|
if (fd > 0)
|
||||||
|
this.handle_client(fd)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fun _parse_headers(this, request)
|
||||||
|
headers = {}
|
||||||
|
lines = str_split(request, "\n")
|
||||||
|
i = 1
|
||||||
|
while (i < len(lines))
|
||||||
|
ln = str_trim(lines[i])
|
||||||
|
if (len(ln) == 0)
|
||||||
|
break
|
||||||
|
colon = find(ln, ":")
|
||||||
|
if (colon > 0)
|
||||||
|
k = str_to_upper(str_trim(substr(ln, 0, colon)))
|
||||||
|
v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
|
||||||
|
headers[k] = v
|
||||||
|
i = i + 1
|
||||||
|
return headers
|
||||||
|
|
||||||
|
fun handle_client(this, fd)
|
||||||
|
request = sock_recv(fd, 65536)
|
||||||
|
if (len(request) == 0)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
lines = str_split(request, "\n")
|
||||||
|
if (len(lines) == 0)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
reqline = str_trim(lines[0])
|
||||||
|
parts = str_split(reqline, " ")
|
||||||
|
if (len(parts) < 2)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
method = str_trim(parts[0])
|
||||||
|
target = str_trim(parts[1])
|
||||||
|
|
||||||
|
// Split query string
|
||||||
|
path = target
|
||||||
|
query = ""
|
||||||
|
q = find(target, "?")
|
||||||
|
if (q >= 0)
|
||||||
|
path = substr(target, 0, q)
|
||||||
|
query = substr(target, q + 1, len(target) - q - 1)
|
||||||
|
|
||||||
|
if (path == "/")
|
||||||
|
path = "/index.html"
|
||||||
|
|
||||||
|
file_path = this.htdocs + path
|
||||||
|
|
||||||
|
// Headers and body
|
||||||
|
headers = this._parse_headers(request)
|
||||||
|
|
||||||
|
// Extract POST body
|
||||||
|
body = ""
|
||||||
|
header_end = find(request, "\r\n\r\n")
|
||||||
|
if (header_end >= 0)
|
||||||
|
body = substr(request, header_end + 4, len(request) - header_end - 4)
|
||||||
|
else
|
||||||
|
header_end = find(request, "\n\n")
|
||||||
|
if (header_end >= 0)
|
||||||
|
body = substr(request, header_end + 2, len(request) - header_end - 2)
|
||||||
|
|
||||||
|
// Treat any .fun under htdocs as a CGI script executed by the Fun interpreter
|
||||||
|
if (str_ends_with(path, ".fun"))
|
||||||
|
// Build env vars according to CGI conventions
|
||||||
|
host = headers["HOST"]
|
||||||
|
if (len(host) == 0) host = "localhost"
|
||||||
|
ua = headers["USER-AGENT"]
|
||||||
|
cookie = headers["COOKIE"]
|
||||||
|
ctype = headers["CONTENT-TYPE"]
|
||||||
|
clen = headers["CONTENT-LENGTH"]
|
||||||
|
|
||||||
|
envs = []
|
||||||
|
// Ensure Fun stdlib is available to CGI child process
|
||||||
|
funlib = env("FUN_LIB_DIR")
|
||||||
|
if (len(funlib) == 0)
|
||||||
|
// Best-effort default when running from project root
|
||||||
|
funlib = "./lib"
|
||||||
|
push(envs, "FUN_LIB_DIR='" + funlib + "'")
|
||||||
|
push(envs, "REQUEST_METHOD='" + method + "'")
|
||||||
|
push(envs, "QUERY_STRING='" + query + "'")
|
||||||
|
push(envs, "SCRIPT_NAME='" + path + "'")
|
||||||
|
push(envs, "PATH_INFO='" + path + "'")
|
||||||
|
push(envs, "SERVER_NAME='" + host + "'")
|
||||||
|
push(envs, "SERVER_PORT='" + to_string(this.port) + "'")
|
||||||
|
push(envs, "SERVER_PROTOCOL='HTTP/1.1'")
|
||||||
|
push(envs, "HTTP_HOST='" + host + "'")
|
||||||
|
if (len(ua) > 0)
|
||||||
|
push(envs, "HTTP_USER_AGENT='" + ua + "'")
|
||||||
|
if (len(cookie) > 0)
|
||||||
|
push(envs, "HTTP_COOKIE='" + cookie + "'")
|
||||||
|
if (len(ctype) > 0)
|
||||||
|
push(envs, "CONTENT_TYPE='" + ctype + "'")
|
||||||
|
if (len(clen) > 0)
|
||||||
|
push(envs, "CONTENT_LENGTH='" + clen + "'")
|
||||||
|
if (len(body) > 0)
|
||||||
|
push(envs, "POST_DATA='" + body + "'")
|
||||||
|
|
||||||
|
// Decide which Fun interpreter to use for the CGI child
|
||||||
|
// Priority:
|
||||||
|
// 1) $FUN_EXEC (explicit override)
|
||||||
|
// 2) ./build_debug/fun (when running from project root)
|
||||||
|
// 3) ./build_release/fun (release build)
|
||||||
|
// 4) fun (from PATH)
|
||||||
|
exec = env("FUN_EXEC")
|
||||||
|
if (len(exec) == 0)
|
||||||
|
// crude existence check using read_file; good enough here
|
||||||
|
if (len(read_file("./build_debug/fun")) > 0)
|
||||||
|
exec = "./build_debug/fun"
|
||||||
|
else
|
||||||
|
if (len(read_file("./build_release/fun")) > 0)
|
||||||
|
exec = "./build_release/fun"
|
||||||
|
else
|
||||||
|
exec = "fun"
|
||||||
|
|
||||||
|
// Run the Fun script as a CGI program
|
||||||
|
cmd = join(envs, " ") + " " + exec + " " + file_path
|
||||||
|
res = proc_run(cmd)
|
||||||
|
out = res["out"]
|
||||||
|
|
||||||
|
if (len(out) == 0)
|
||||||
|
this._send(fd, 500, "Internal Server Error", "<h1>CGI produced no output</h1>")
|
||||||
|
else
|
||||||
|
this._send_cgi_response(fd, out)
|
||||||
|
else
|
||||||
|
// Static file
|
||||||
|
content = read_file(file_path)
|
||||||
|
if (len(content) > 0)
|
||||||
|
this._send(fd, 200, "OK", content)
|
||||||
|
else
|
||||||
|
this._send(fd, 404, "Not Found", "<h1>404 Not Found</h1>")
|
||||||
|
|
||||||
|
sock_close(fd)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fun _send(this, fd, code, text, body)
|
||||||
|
b = to_string(body)
|
||||||
|
resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
|
||||||
|
resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
|
||||||
|
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
||||||
|
resp = resp + "Connection: close\r\n\r\n" + b
|
||||||
|
sock_send(fd, resp)
|
||||||
|
|
||||||
|
// Translate CGI output (headers + body) into a proper HTTP/1.1 response
|
||||||
|
fun _send_cgi_response(this, fd, raw)
|
||||||
|
tmpcgi = CGI()
|
||||||
|
resp = tmpcgi.cgi_to_http_response(raw)
|
||||||
|
sock_send(fd, resp)
|
||||||
179
lib/net/http_cgi_server.fun
Normal file
179
lib/net/http_cgi_server.fun
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
/*
|
||||||
|
* 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-03-25
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Blocking HTTP server with CGI support
|
||||||
|
|
||||||
|
#include <io/socket.fun>
|
||||||
|
#include <strings.fun>
|
||||||
|
#include <net/cgi.fun>
|
||||||
|
|
||||||
|
class HTTPCGIServer(number port)
|
||||||
|
fun _construct(this, port)
|
||||||
|
this.port = port
|
||||||
|
this.srv = TcpServer(port, 10)
|
||||||
|
this.htdocs = "./"
|
||||||
|
|
||||||
|
fun set_htdocs(this, path)
|
||||||
|
this.htdocs = to_string(path)
|
||||||
|
|
||||||
|
fun start(this)
|
||||||
|
if (this.srv.listen() <= 0)
|
||||||
|
print("HTTP CGI Server: failed to listen on port " + to_string(this.port))
|
||||||
|
return 0
|
||||||
|
print("HTTP CGI Server: serving " + this.htdocs + " on port " + to_string(this.port))
|
||||||
|
while true
|
||||||
|
fd = this.srv.accept()
|
||||||
|
if (fd > 0)
|
||||||
|
this.handle_client(fd)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fun _parse_headers(this, request)
|
||||||
|
headers = {}
|
||||||
|
lines = str_split(request, "\n")
|
||||||
|
i = 1 // start after request line
|
||||||
|
while (i < len(lines))
|
||||||
|
ln = str_trim(lines[i])
|
||||||
|
if (len(ln) == 0)
|
||||||
|
break
|
||||||
|
colon = find(ln, ":")
|
||||||
|
if (colon > 0)
|
||||||
|
k = str_to_upper(str_trim(substr(ln, 0, colon)))
|
||||||
|
v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
|
||||||
|
headers[k] = v
|
||||||
|
i = i + 1
|
||||||
|
return headers
|
||||||
|
|
||||||
|
fun handle_client(this, fd)
|
||||||
|
request = sock_recv(fd, 65536)
|
||||||
|
if (len(request) == 0)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
lines = str_split(request, "\n")
|
||||||
|
if (len(lines) == 0)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
reqline = str_trim(lines[0])
|
||||||
|
parts = str_split(reqline, " ")
|
||||||
|
if (len(parts) < 2)
|
||||||
|
sock_close(fd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
method = str_trim(parts[0])
|
||||||
|
target = str_trim(parts[1])
|
||||||
|
|
||||||
|
// Split query string
|
||||||
|
path = target
|
||||||
|
query = ""
|
||||||
|
q = find(target, "?")
|
||||||
|
if (q >= 0)
|
||||||
|
path = substr(target, 0, q)
|
||||||
|
query = substr(target, q + 1, len(target) - q - 1)
|
||||||
|
|
||||||
|
if (path == "/")
|
||||||
|
path = "/index.html"
|
||||||
|
|
||||||
|
file_path = this.htdocs + path
|
||||||
|
|
||||||
|
// Headers and body
|
||||||
|
headers = this._parse_headers(request)
|
||||||
|
|
||||||
|
// Extract POST body
|
||||||
|
body = ""
|
||||||
|
header_end = find(request, "\r\n\r\n")
|
||||||
|
if (header_end >= 0)
|
||||||
|
body = substr(request, header_end + 4, len(request) - header_end - 4)
|
||||||
|
else
|
||||||
|
header_end = find(request, "\n\n")
|
||||||
|
if (header_end >= 0)
|
||||||
|
body = substr(request, header_end + 2, len(request) - header_end - 2)
|
||||||
|
|
||||||
|
// Serve CGI when file ends with .fun
|
||||||
|
if (str_ends_with(path, ".fun"))
|
||||||
|
// Build env vars according to CGI conventions
|
||||||
|
host = headers["HOST"]
|
||||||
|
if (len(host) == 0) host = "localhost"
|
||||||
|
ua = headers["USER-AGENT"]
|
||||||
|
cookie = headers["COOKIE"]
|
||||||
|
ctype = headers["CONTENT-TYPE"]
|
||||||
|
clen = headers["CONTENT-LENGTH"]
|
||||||
|
|
||||||
|
envs = []
|
||||||
|
// Ensure Fun stdlib is available to CGI child process
|
||||||
|
funlib = env("FUN_LIB_DIR")
|
||||||
|
if (len(funlib) == 0)
|
||||||
|
// Best-effort default when running from project root
|
||||||
|
funlib = "./lib"
|
||||||
|
push(envs, "FUN_LIB_DIR='" + funlib + "'")
|
||||||
|
push(envs, "REQUEST_METHOD='" + method + "'")
|
||||||
|
push(envs, "QUERY_STRING='" + query + "'")
|
||||||
|
push(envs, "SCRIPT_NAME='" + path + "'")
|
||||||
|
push(envs, "PATH_INFO='" + path + "'")
|
||||||
|
push(envs, "SERVER_NAME='" + host + "'")
|
||||||
|
push(envs, "SERVER_PORT='" + to_string(this.port) + "'")
|
||||||
|
push(envs, "SERVER_PROTOCOL='HTTP/1.1'")
|
||||||
|
push(envs, "HTTP_HOST='" + host + "'")
|
||||||
|
if (len(ua) > 0)
|
||||||
|
push(envs, "HTTP_USER_AGENT='" + ua + "'")
|
||||||
|
if (len(cookie) > 0)
|
||||||
|
push(envs, "HTTP_COOKIE='" + cookie + "'")
|
||||||
|
if (len(ctype) > 0)
|
||||||
|
push(envs, "CONTENT_TYPE='" + ctype + "'")
|
||||||
|
if (len(clen) > 0)
|
||||||
|
push(envs, "CONTENT_LENGTH='" + clen + "'")
|
||||||
|
if (len(body) > 0)
|
||||||
|
push(envs, "POST_DATA='" + body + "'")
|
||||||
|
|
||||||
|
// Decide which Fun interpreter to use for the CGI child
|
||||||
|
exec = env("FUN_EXEC")
|
||||||
|
if (len(exec) == 0)
|
||||||
|
if (len(read_file("./build_debug/fun")) > 0)
|
||||||
|
exec = "./build_debug/fun"
|
||||||
|
else
|
||||||
|
if (len(read_file("./build_release/fun")) > 0)
|
||||||
|
exec = "./build_release/fun"
|
||||||
|
else
|
||||||
|
exec = "fun"
|
||||||
|
|
||||||
|
// Run the Fun script as a CGI program
|
||||||
|
cmd = join(envs, " ") + " " + exec + " " + file_path
|
||||||
|
res = proc_run(cmd)
|
||||||
|
out = res["out"]
|
||||||
|
|
||||||
|
if (len(out) == 0)
|
||||||
|
this._send(fd, 500, "Internal Server Error", "<h1>CGI produced no output</h1>")
|
||||||
|
else
|
||||||
|
this._send_cgi_response(fd, out)
|
||||||
|
else
|
||||||
|
// Static file
|
||||||
|
content = read_file(file_path)
|
||||||
|
if (len(content) > 0)
|
||||||
|
this._send(fd, 200, "OK", content)
|
||||||
|
else
|
||||||
|
this._send(fd, 404, "Not Found", "<h1>404 Not Found</h1>")
|
||||||
|
|
||||||
|
sock_close(fd)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fun _send(this, fd, code, text, body)
|
||||||
|
b = to_string(body)
|
||||||
|
resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
|
||||||
|
resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
|
||||||
|
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
||||||
|
resp = resp + "Connection: close\r\n\r\n" + b
|
||||||
|
sock_send(fd, resp)
|
||||||
|
|
||||||
|
// Translate CGI output (headers + body) into a proper HTTP/1.1 response
|
||||||
|
fun _send_cgi_response(this, fd, raw)
|
||||||
|
tmpcgi = CGI()
|
||||||
|
resp = tmpcgi.cgi_to_http_response(raw)
|
||||||
|
sock_send(fd, resp)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue