Some more HTTP POST evaluation. Still broken! (0.39.10)
This commit is contained in:
parent
e240768c26
commit
5222f79434
5 changed files with 176 additions and 26 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.10)
|
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 99)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,39 @@
|
||||||
cgi = CGI()
|
cgi = CGI()
|
||||||
cgi.content_type("text/html; charset=utf-8")
|
cgi.content_type("text/html; charset=utf-8")
|
||||||
pmap = cgi.params()
|
pmap = cgi.params()
|
||||||
html = "<html><body><h1>POST fields</h1><ul>"
|
// Build HTML with a form at the top to submit POST data back to this script
|
||||||
for k in keys(pmap)
|
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)
|
vals = cgi.param_all(k)
|
||||||
i = 0; n = len(vals); joined = ""
|
i = 0
|
||||||
|
n = len(vals)
|
||||||
|
joined = ""
|
||||||
while (i < n)
|
while (i < n)
|
||||||
if (i > 0) joined = joined + ", "
|
if (i > 0) joined = joined + ", "
|
||||||
joined = joined + cgi.escape_html(vals[i])
|
joined = joined + cgi.escape_html(vals[i])
|
||||||
i = i + 1
|
i = i + 1
|
||||||
html = html + "<li><b>" + cgi.escape_html(k) + "</b>: " + joined + "</li>"
|
html = html + "<li><b>" + cgi.escape_html(k) + "</b>: " + joined + "</li>"
|
||||||
|
i_name = i_name + 1
|
||||||
html = html + "</ul></body></html>"
|
html = html + "</ul></body></html>"
|
||||||
cgi.send(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
|
Status: 200 OK
|
||||||
Content-Type: text/html; charset=utf-8
|
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>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
12
examples/snippets/parse_test2.fun
Executable file
12
examples/snippets/parse_test2.fun
Executable 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
|
||||||
12
examples/snippets/test_parse.fun
Executable file
12
examples/snippets/test_parse.fun
Executable 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
|
||||||
148
lib/net/cgi.fun
148
lib/net/cgi.fun
|
|
@ -43,7 +43,60 @@ class CGI()
|
||||||
// Params from POST (x-www-form-urlencoded only)
|
// Params from POST (x-www-form-urlencoded only)
|
||||||
ct = this.env["CONTENT_TYPE"]
|
ct = this.env["CONTENT_TYPE"]
|
||||||
pd = this.env["POST_DATA"]
|
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)
|
if (typeof(pd) == "String" && len(pd) > 0)
|
||||||
parsed_pd = this._parse_urlencoded(pd)
|
parsed_pd = this._parse_urlencoded(pd)
|
||||||
this._merge_params(parsed_pd)
|
this._merge_params(parsed_pd)
|
||||||
|
|
@ -202,11 +255,46 @@ class CGI()
|
||||||
resp = resp + "Connection: close\r\n\r\n" + b
|
resp = resp + "Connection: close\r\n\r\n" + b
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
// Minimal url-decoder: '+' -> space; %XX for ASCII printable
|
// Minimal url-decoder: '+' -> space; %XX for ASCII hex
|
||||||
fun url_decode(this, s)
|
fun url_decode(this, s)
|
||||||
src = to_string(s)
|
src = to_string(s)
|
||||||
// Simplified for parser-compatibility: only translate '+' to space
|
out = []
|
||||||
return str_replace_all(src, "+", " ")
|
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)
|
fun _merge_params(this, pairs)
|
||||||
// pairs: array of [key, value] entries
|
// pairs: array of [key, value] entries
|
||||||
|
|
@ -231,23 +319,43 @@ class CGI()
|
||||||
src = to_string(s)
|
src = to_string(s)
|
||||||
if (len(src) == 0)
|
if (len(src) == 0)
|
||||||
return out
|
return out
|
||||||
parts = str_split(src, "&")
|
// Manually scan so we can treat both '&' and ';' as pair separators (robust across environments)
|
||||||
i = 0
|
pairs = []
|
||||||
lp = len(parts)
|
buf = []
|
||||||
while (i < lp)
|
number i = 0
|
||||||
kv = parts[i]
|
number n = len(src)
|
||||||
if (typeof(kv) == "String" && len(kv) > 0)
|
while (i < n)
|
||||||
eq = find(kv, "=")
|
ch = substr(src, i, 1)
|
||||||
if (eq >= 0)
|
if (ch == "&" || ch == ";")
|
||||||
k = substr(kv, 0, eq)
|
push(pairs, join(buf, ""))
|
||||||
v = substr(kv, eq + 1, len(kv) - eq - 1)
|
buf = []
|
||||||
else
|
else
|
||||||
k = kv
|
push(buf, ch)
|
||||||
v = ""
|
|
||||||
key = this.url_decode(k)
|
|
||||||
val = this.url_decode(v)
|
|
||||||
push(out, [key, val])
|
|
||||||
i = i + 1
|
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
|
return out
|
||||||
|
|
||||||
fun _parse_cookies(this, cookie_str)
|
fun _parse_cookies(this, cookie_str)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue