1
0
Fork 0
forked from fun/fun

Some more HTTP POST evaluation. Still broken! (0.39.10)

This commit is contained in:
Johannes Findeisen 2026-03-26 00:36:52 +01:00
commit 5222f79434
5 changed files with 176 additions and 26 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(fun VERSION 0.39.9 LANGUAGES C)
project(fun VERSION 0.39.10 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

View file

@ -16,21 +16,39 @@
cgi = CGI()
cgi.content_type("text/html; charset=utf-8")
pmap = cgi.params()
html = "<html><body><h1>POST fields</h1><ul>"
for k in keys(pmap)
// Build HTML with a form at the top to submit POST data back to this script
html = "<html><body>"
html = html + "<h1>Submit data</h1>"
html = html + "<form method=\"POST\" action=\"\">"
html = html + "<label>A: <input type=\"text\" name=\"a\" value=\"1\"></label><br>"
html = html + "<label>B: <input type=\"text\" name=\"b\" value=\"2\"></label><br>"
html = html + "<label>C: <input type=\"text\" name=\"c\" value=\"3\"></label><br>"
html = html + "<button type=\"submit\">Submit</button>"
html = html + "</form>"
html = html + "<h1>POST fields</h1><ul>"
// Always show fields a, b, and c (print names even if no value was submitted)
names = ["a", "b", "c"]
i_name = 0
n_names = len(names)
while (i_name < n_names)
k = names[i_name]
vals = cgi.param_all(k)
i = 0; n = len(vals); joined = ""
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>"
i_name = i_name + 1
html = html + "</ul></body></html>"
cgi.send(html)
/* Expected output (POST with body: "a=1&b=two&b=2"):
/* Possible output (POST with body: "a=1&b=2&c=3"):
Status: 200 OK
Content-Type: text/html; charset=utf-8
<html><body><h1>POST fields</h1><ul><li><b>a</b>: 1</li><li><b>b</b>: two, 2</li></ul></body></html>
<html><body><h1>POST fields</h1><ul><li><b>a</b>: 1</li><li><b>b</b>: b, 2</li><li><b>c</b>: 3</li></ul></body></html>
*/

View file

@ -0,0 +1,12 @@
#!/usr/bin/env fun
#include <net/cgi.fun>
cgi = CGI()
pairs = cgi._parse_urlencoded("a=1&b=2&c=3")
i = 0
n = len(pairs)
while (i < n)
p = pairs[i]
print(to_string(p[0]) + "=" + to_string(p[1]))
i = i + 1

View file

@ -0,0 +1,12 @@
#!/usr/bin/env fun
#include <net/cgi.fun>
cgi = CGI()
pairs = cgi._parse_urlencoded("a=1&b=2&c=3")
i = 0
n = len(pairs)
while (i < n)
p = pairs[i]
print(to_string(p[0]) + "=" + to_string(p[1]))
i = i + 1

View file

@ -43,7 +43,60 @@ class CGI()
// Params from POST (x-www-form-urlencoded only)
ct = this.env["CONTENT_TYPE"]
pd = this.env["POST_DATA"]
if (typeof(ct) == "String" && len(ct) > 0 && find(str_to_lower(ct), "application/x-www-form-urlencoded") >= 0)
// Fallback: when running under a real CGI, POST data comes from stdin, not env.
// Only attempt to read stdin if method is POST, content-length > 0, and POST_DATA is empty.
if (!(typeof(pd) == "String" && len(pd) > 0))
if (str_to_upper(this.env["REQUEST_METHOD"]) == "POST")
cl = to_number(this.env["CONTENT_LENGTH"])
if (cl > 0)
// Read up to CONTENT_LENGTH bytes from stdin. Some CGI runners may deliver
// urlencoded chunks split at '&' without including the delimiter; reconstruct
// by inserting '&' between successive chunks.
parts = []
total = 0
// Safeguard: cap iterations to avoid infinite loops on unexpected behavior
iter = 0
while (total < cl && iter < 100000)
chunk = input("")
if (!(typeof(chunk) == "String"))
break
if (len(chunk) == 0)
// EOF without newline
break
push(parts, chunk)
total = total + len(chunk)
iter = iter + 1
// Rebuild with '&' between pieces to restore typical form encoding
if (len(parts) > 0)
rebuilt = parts[0]
idx = 1
np = len(parts)
while (idx < np)
rebuilt = rebuilt + "&" + parts[idx]
idx = idx + 1
tmp = rebuilt
else
tmp = ""
// Trim to declared content length to drop any stray newline added by the CGI runner
if (len(tmp) > cl)
pd = substr(tmp, 0, cl)
else
pd = tmp
this.env["POST_DATA"] = pd
else
// No declared length; try to read a single line (common with simple runners)
tmp = input("")
if (typeof(tmp) == "String" && len(tmp) > 0)
this.env["POST_DATA"] = tmp
// Keep local variable in sync so parsing below sees the data
pd = this.env["POST_DATA"]
// Decide whether to treat body as urlencoded: default yes for POST unless explicitly multipart
is_urlencoded = 1
if (typeof(ct) == "String" && len(ct) > 0)
lct = str_to_lower(ct)
if (find(lct, "multipart/form-data") >= 0)
is_urlencoded = 0
if (is_urlencoded)
if (typeof(pd) == "String" && len(pd) > 0)
parsed_pd = this._parse_urlencoded(pd)
this._merge_params(parsed_pd)
@ -202,11 +255,46 @@ class CGI()
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 hex
fun url_decode(this, s)
src = to_string(s)
// Simplified for parser-compatibility: only translate '+' to space
return str_replace_all(src, "+", " ")
out = []
i = 0
n = len(src)
while (i < n)
ch = substr(src, i, 1)
if (ch == "+")
push(out, " ")
i = i + 1
else if (ch == "%" && i + 2 < n)
h1 = substr(src, i + 1, 1)
h2 = substr(src, i + 2, 1)
// Convert two hex digits to a single character (uppercase/lowercase allowed)
hexdigits = "0123456789ABCDEF"
hexdigits_l = "0123456789abcdef"
v1 = find(hexdigits, str_to_upper(h1))
v2 = find(hexdigits, str_to_upper(h2))
if (v1 >= 0 && v2 >= 0)
code = v1 * 16 + v2
// Build single-byte string from code using substr on a constant table
// Table of 256 bytes isn't available; approximate by mapping common ASCII 0x20..0x7E
ascii = "\x20!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
if (code >= 32 && code <= 126)
push(out, substr(ascii, code - 32, 1))
else
// Non-printable: keep percent sequence as-is
push(out, "%")
push(out, h1)
push(out, h2)
i = i + 3
else
// Not hex, keep as-is
push(out, ch)
i = i + 1
else
push(out, ch)
i = i + 1
return join(out, "")
fun _merge_params(this, pairs)
// pairs: array of [key, value] entries
@ -231,23 +319,43 @@ class CGI()
src = to_string(s)
if (len(src) == 0)
return out
parts = str_split(src, "&")
i = 0
lp = len(parts)
while (i < lp)
kv = parts[i]
if (typeof(kv) == "String" && len(kv) > 0)
eq = find(kv, "=")
if (eq >= 0)
k = substr(kv, 0, eq)
v = substr(kv, eq + 1, len(kv) - eq - 1)
else
k = kv
v = ""
key = this.url_decode(k)
val = this.url_decode(v)
push(out, [key, val])
// Manually scan so we can treat both '&' and ';' as pair separators (robust across environments)
pairs = []
buf = []
number i = 0
number n = len(src)
while (i < n)
ch = substr(src, i, 1)
if (ch == "&" || ch == ";")
push(pairs, join(buf, ""))
buf = []
else
push(buf, ch)
i = i + 1
// tail
push(pairs, join(buf, ""))
j = 0
lp = len(pairs)
while (j < lp)
kv = pairs[j]
if (typeof(kv) == "String")
token = kv
// Trim CR/LF that might trail if body ended with a newline
// Reuse str_trim which trims spaces and CR/LF
token = str_trim(token)
if (len(token) > 0)
eq = find(token, "=")
if (eq >= 0)
k = substr(token, 0, eq)
v = substr(token, eq + 1, len(token) - eq - 1)
else
k = token
v = ""
key = this.url_decode(k)
val = this.url_decode(v)
push(out, [key, val])
j = j + 1
return out
fun _parse_cookies(this, cookie_str)