diff options
-rw-r--r-- | Makefile | 37 | ||||
-rw-r--r-- | adapter/format/gopher2html.nim | 19 | ||||
-rw-r--r-- | adapter/gophertypes.nim | 19 | ||||
-rw-r--r-- | adapter/protocol/curlerrors.nim | 17 | ||||
-rw-r--r-- | adapter/protocol/curlwrap.nim (renamed from src/loader/curlwrap.nim) | 0 | ||||
-rw-r--r-- | adapter/protocol/data.nim | 6 | ||||
-rw-r--r-- | adapter/protocol/dirlist.nim (renamed from src/loader/dirlist.nim) | 0 | ||||
-rw-r--r-- | adapter/protocol/file.nim | 11 | ||||
-rw-r--r-- | adapter/protocol/ftp.nim | 8 | ||||
-rw-r--r-- | adapter/protocol/gopher.nim | 28 | ||||
-rw-r--r-- | adapter/protocol/http.nim | 128 | ||||
-rw-r--r-- | doc/localcgi.md | 33 | ||||
-rw-r--r-- | res/urimethodmap | 2 | ||||
-rw-r--r-- | src/bindings/curl.nim | 3 | ||||
-rw-r--r-- | src/css/cssparser.nim | 2 | ||||
-rw-r--r-- | src/loader/cgi.nim | 32 | ||||
-rw-r--r-- | src/loader/connecterror.nim | 37 | ||||
-rw-r--r-- | src/loader/curlhandle.nim | 32 | ||||
-rw-r--r-- | src/loader/http.nim | 137 | ||||
-rw-r--r-- | src/loader/loader.nim | 130 | ||||
-rw-r--r-- | src/loader/loaderhandle.nim | 4 | ||||
-rw-r--r-- | src/server/buffer.nim | 12 | ||||
-rw-r--r-- | src/types/blob.nim | 13 | ||||
-rw-r--r-- | src/types/formdata.nim | 66 | ||||
-rw-r--r-- | src/xhr/formdata.nim | 15 |
25 files changed, 433 insertions, 358 deletions
diff --git a/Makefile b/Makefile index 1a86279c..2238170c 100644 --- a/Makefile +++ b/Makefile @@ -41,15 +41,15 @@ endif FLAGS += --nimcache:"$(OBJDIR)/$(TARGET)" .PHONY: all -all: $(OUTDIR_BIN)/cha $(OUTDIR_LIBEXEC)/gopher2html $(OUTDIR_CGI_BIN)/gmifetch \ - $(OUTDIR_LIBEXEC)/gmi2html $(OUTDIR_CGI_BIN)/cha-finger $(OUTDIR_CGI_BIN)/about \ - $(OUTDIR_CGI_BIN)/data $(OUTDIR_CGI_BIN)/file $(OUTDIR_CGI_BIN)/ftp \ - $(OUTDIR_CGI_BIN)/gopher +all: $(OUTDIR_BIN)/cha $(OUTDIR_CGI_BIN)/http \ + $(OUTDIR_CGI_BIN)/gmifetch $(OUTDIR_LIBEXEC)/gmi2html \ + $(OUTDIR_CGI_BIN)/gopher $(OUTDIR_LIBEXEC)/gopher2html \ + $(OUTDIR_CGI_BIN)/cha-finger $(OUTDIR_CGI_BIN)/about \ + $(OUTDIR_CGI_BIN)/data $(OUTDIR_CGI_BIN)/file $(OUTDIR_CGI_BIN)/ftp $(OUTDIR_BIN)/cha: lib/libquickjs.a src/*.nim src/**/*.nim res/* res/**/* @mkdir -p "$(OUTDIR)/$(TARGET)/bin" - $(NIMC) -d:curlLibName:$(CURLLIBNAME) -d:libexecPath=$(LIBEXECDIR) \ - $(FLAGS) -o:"$(OUTDIR_BIN)/cha" src/main.nim + $(NIMC) -d:libexecPath=$(LIBEXECDIR) $(FLAGS) -o:"$(OUTDIR_BIN)/cha" src/main.nim ln -sf "$(OUTDIR)/$(TARGET)/bin/cha" cha $(OUTDIR_LIBEXEC)/gopher2html: adapter/format/gopher2html.nim \ @@ -69,26 +69,34 @@ $(OUTDIR_CGI_BIN)/cha-finger: adapter/protocol/cha-finger @mkdir -p $(OUTDIR_CGI_BIN) cp adapter/protocol/cha-finger $(OUTDIR_CGI_BIN) +$(OUTDIR_CGI_BIN)/http: adapter/protocol/http.nim adapter/protocol/curlwrap.nim \ + adapter/protocol/curlerrors.nim src/bindings/curl.nim \ + src/types/opt.nim src/utils/twtstr.nim + $(NIMC) $(FLAGS) -d:curlLibName:$(CURLLIBNAME) \ + -o:"$(OUTDIR_CGI_BIN)/http" adapter/protocol/http.nim + $(OUTDIR_CGI_BIN)/about: adapter/protocol/about.nim $(NIMC) $(FLAGS) -o:"$(OUTDIR_CGI_BIN)/about" adapter/protocol/about.nim $(OUTDIR_CGI_BIN)/data: adapter/protocol/data.nim src/utils/twtstr.nim $(NIMC) $(FLAGS) -o:"$(OUTDIR_CGI_BIN)/data" adapter/protocol/data.nim -$(OUTDIR_CGI_BIN)/file: adapter/protocol/file.nim src/loader/dirlist.nim \ +$(OUTDIR_CGI_BIN)/file: adapter/protocol/file.nim adapter/protocol/dirlist.nim \ src/utils/twtstr.nim src/loader/connecterror.nim $(NIMC) $(FLAGS) -o:"$(OUTDIR_CGI_BIN)/file" adapter/protocol/file.nim $(OUTDIR_CGI_BIN)/ftp: adapter/protocol/ftp.nim src/bindings/curl.nim \ - src/loader/dirlist.nim src/utils/twtstr.nim src/types/url.nim \ + adapter/protocol/dirlist.nim src/utils/twtstr.nim src/types/url.nim \ src/types/opt.nim src/loader/connecterror.nim - $(NIMC) $(FLAGS) -o:"$(OUTDIR_CGI_BIN)/ftp" adapter/protocol/ftp.nim + $(NIMC) $(FLAGS) -d:curlLibName:$(CURLLIBNAME) \ + -o:"$(OUTDIR_CGI_BIN)/ftp" adapter/protocol/ftp.nim -$(OUTDIR_CGI_BIN)/gopher: adapter/protocol/gopher.nim adapter/gophertypes.nim \ - src/bindings/curl.nim src/loader/dirlist.nim \ - src/utils/twtstr.nim src/types/url.nim src/types/opt.nim \ - src/loader/connecterror.nim - $(NIMC) $(FLAGS) -o:"$(OUTDIR_CGI_BIN)/gopher" adapter/protocol/gopher.nim +$(OUTDIR_CGI_BIN)/gopher: adapter/protocol/gopher.nim adapter/protocol/curlwrap.nim \ + adapter/protocol/curlerrors.nim adapter/gophertypes.nim \ + src/bindings/curl.nim src/loader/connecterror.nim \ + src/utils/twtstr.nim src/types/url.nim src/types/opt.nim + $(NIMC) $(FLAGS) -d:curlLibName:$(CURLLIBNAME) \ + -o:"$(OUTDIR_CGI_BIN)/gopher" adapter/protocol/gopher.nim CFLAGS = -g -Wall -O2 -DCONFIG_VERSION=\"$(shell cat lib/quickjs/VERSION)\" QJSOBJ = $(OBJDIR)/quickjs @@ -165,6 +173,7 @@ uninstall: @# intentionally not quoted rm -f $(LIBEXECDIR_CHAWAN)/gopher2html rm -f $(LIBEXECDIR_CHAWAN)/gmi2html + rm -f $(LIBEXECDIR_CHAWAN)/cgi-bin/http rm -f $(LIBEXECDIR_CHAWAN)/cgi-bin/about rm -f $(LIBEXECDIR_CHAWAN)/cgi-bin/data rm -f $(LIBEXECDIR_CHAWAN)/cgi-bin/gmifetch diff --git a/adapter/format/gopher2html.nim b/adapter/format/gopher2html.nim index 33cd564a..1004cc2c 100644 --- a/adapter/format/gopher2html.nim +++ b/adapter/format/gopher2html.nim @@ -5,26 +5,9 @@ import std/os import std/streams import std/strutils -import utils/twtstr - include ../gophertypes -func gopherType(c: char): GopherType = - return case c - of '0': TEXT_FILE - of '1': DIRECTORY - of '3': ERROR - of '5': DOS_BINARY - of '7': SEARCH - of 'm': MESSAGE - of 's': SOUND - of 'g': GIF - of 'h': HTML - of 'i': INFO - of 'I': IMAGE - of '9': BINARY - of 'p': PNG - else: UNKNOWN +import utils/twtstr const ControlPercentEncodeSet = {char(0x00)..char(0x1F), char(0x7F)..char(0xFF)} const QueryPercentEncodeSet = (ControlPercentEncodeSet + {' ', '"', '#', '<', '>'}) diff --git a/adapter/gophertypes.nim b/adapter/gophertypes.nim index dbc8d64d..a65b75fe 100644 --- a/adapter/gophertypes.nim +++ b/adapter/gophertypes.nim @@ -1,4 +1,4 @@ -type GopherType = enum +type GopherType* = enum UNKNOWN = "unsupported" TEXT_FILE = "text file" ERROR = "error" @@ -13,3 +13,20 @@ type GopherType = enum IMAGE = "image" BINARY = "binary" PNG = "png" + +func gopherType*(c: char): GopherType = + return case c + of '0': TEXT_FILE + of '1': DIRECTORY + of '3': ERROR + of '5': DOS_BINARY + of '7': SEARCH + of 'm': MESSAGE + of 's': SOUND + of 'g': GIF + of 'h': HTML + of 'i': INFO + of 'I': IMAGE + of '9': BINARY + of 'p': PNG + else: UNKNOWN diff --git a/adapter/protocol/curlerrors.nim b/adapter/protocol/curlerrors.nim new file mode 100644 index 00000000..3bb6cf6d --- /dev/null +++ b/adapter/protocol/curlerrors.nim @@ -0,0 +1,17 @@ +import bindings/curl +import loader/connecterror + +func curlErrorToChaError*(res: CURLcode): ConnectErrorCode = + return case res + of CURLE_OK: CONNECTION_SUCCESS + of CURLE_URL_MALFORMAT: ERROR_INVALID_URL #TODO should never occur... + of CURLE_COULDNT_CONNECT: ERROR_CONNECTION_REFUSED + of CURLE_COULDNT_RESOLVE_PROXY: ERROR_FAILED_TO_RESOLVE_PROXY + of CURLE_COULDNT_RESOLVE_HOST: ERROR_FAILED_TO_RESOLVE_HOST + of CURLE_PROXY: ERROR_PROXY_REFUSED_TO_CONNECT + else: ERROR_INTERNAL + +proc getCurlConnectionError*(res: CURLcode): string = + let e = $int(curlErrorToChaError(res)) + let msg = $curl_easy_strerror(res) + return "Cha-Control: ConnectionError " & e & " " & msg & "\n" diff --git a/src/loader/curlwrap.nim b/adapter/protocol/curlwrap.nim index 7aef4182..7aef4182 100644 --- a/src/loader/curlwrap.nim +++ b/adapter/protocol/curlwrap.nim diff --git a/adapter/protocol/data.nim b/adapter/protocol/data.nim index 7b022976..4f5da2e8 100644 --- a/adapter/protocol/data.nim +++ b/adapter/protocol/data.nim @@ -2,15 +2,17 @@ import std/envvars import std/base64 import std/strutils +import loader/connecterror import utils/twtstr proc main() = let str = getEnv("MAPPED_URI_PATH") const si = "data:".len # start index + const iu = $int(ERROR_INVALID_URL) var ct = str.until(',', si) for c in ct: if c notin AsciiAlphaNumeric and c != '/': - stdout.write("Cha-Control: ConnectionError -7 invalid data URL") + stdout.write("Cha-Control: ConnectionError " & iu & " invalid data URL") return let sd = si + ct.len + 1 # data start let body = percentDecode(str, sd) @@ -21,7 +23,7 @@ proc main() = stdout.write("Content-Type: " & ct & "\n\n") stdout.write(d) except ValueError: - stdout.write("Cha-Control: ConnectionError -7 invalid data URL") + stdout.write("Cha-Control: ConnectionError " & iu & " invalid data URL") else: stdout.write("Content-Type: " & ct & "\n\n") stdout.write(body) diff --git a/src/loader/dirlist.nim b/adapter/protocol/dirlist.nim index 2328cd55..2328cd55 100644 --- a/src/loader/dirlist.nim +++ b/adapter/protocol/dirlist.nim diff --git a/adapter/protocol/file.nim b/adapter/protocol/file.nim index f3ffa93e..168be58b 100644 --- a/adapter/protocol/file.nim +++ b/adapter/protocol/file.nim @@ -4,8 +4,9 @@ import std/streams import std/times import std/envvars +import dirlist + import loader/connecterror -import loader/dirlist import utils/twtstr proc loadDir(path: string) = @@ -84,14 +85,14 @@ proc loadFile(istream: Stream) = stdout.write("\n") let outs = newFileStream(stdout) while not istream.atEnd: - const bufferSize = 4096 - var buffer {.noinit.}: array[bufferSize, char] + const BufferSize = 16384 + var buffer {.noinit.}: array[BufferSize, char] while true: - let n = readData(istream, addr buffer[0], bufferSize) + let n = readData(istream, addr buffer[0], BufferSize) if n == 0: break outs.writeData(addr buffer[0], n) - if n < bufferSize: + if n < BufferSize: break proc main() = diff --git a/adapter/protocol/ftp.nim b/adapter/protocol/ftp.nim index ad4a51a3..7d072471 100644 --- a/adapter/protocol/ftp.nim +++ b/adapter/protocol/ftp.nim @@ -2,10 +2,12 @@ import std/envvars import std/options import std/strutils +import curlerrors +import curlwrap +import dirlist + import bindings/curl import loader/connecterror -import loader/curlwrap -import loader/dirlist import types/opt import types/url import utils/twtstr @@ -185,7 +187,7 @@ proc main() = let res = curl_easy_perform(curl) if res != CURLE_OK: if not op.statusline: - stdout.write("Cha-Control: ConnectionError " & $int(res) & "\n") + stdout.write(getCurlConnectionError(res)) elif op.dirmode: op.finish() curl_easy_cleanup(curl) diff --git a/adapter/protocol/gopher.nim b/adapter/protocol/gopher.nim index cf3038f7..91875a1e 100644 --- a/adapter/protocol/gopher.nim +++ b/adapter/protocol/gopher.nim @@ -1,38 +1,22 @@ import std/envvars import std/options +import curlwrap +import curlerrors + +import ../gophertypes + import bindings/curl import loader/connecterror -import loader/curlwrap -import loader/request import types/opt import types/url import utils/twtstr -include ../gophertypes - type GopherHandle = ref object curl: CURL t: GopherType statusline: bool -func gopherType(c: char): GopherType = - return case c - of '0': TEXT_FILE - of '1': DIRECTORY - of '3': ERROR - of '5': DOS_BINARY - of '7': SEARCH - of 'm': MESSAGE - of 's': SOUND - of 'g': GIF - of 'h': HTML - of 'i': INFO - of 'I': IMAGE - of '9': BINARY - of 'p': PNG - else: UNKNOWN - proc onStatusLine(op: GopherHandle) = var status: clong op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) @@ -107,7 +91,7 @@ proc main() = curl.setopt(CURLOPT_PROXY, proxy) let res = curl_easy_perform(curl) if res != CURLE_OK and not op.statusline: - stdout.write("Cha-Control: ConnectionError " & $int(res) & "\n") + stdout.write(getCurlConnectionError(res)) curl_easy_cleanup(curl) main() diff --git a/adapter/protocol/http.nim b/adapter/protocol/http.nim new file mode 100644 index 00000000..10b0c060 --- /dev/null +++ b/adapter/protocol/http.nim @@ -0,0 +1,128 @@ +import std/envvars +import std/options +import std/strutils + +import curlerrors +import curlwrap + +import bindings/curl +import types/opt +import utils/twtstr + +type + EarlyHintState = enum + NO_EARLY_HINT, EARLY_HINT_STARTED, EARLY_HINT_DONE + + HttpHandle = ref object + curl: CURL + statusline: bool + connectreport: bool + earlyhint: EarlyHintState + slist: curl_slist + +proc curlWriteHeader(p: cstring, size, nitems: csize_t, userdata: pointer): + csize_t {.cdecl.} = + var line = newString(nitems) + if nitems > 0: + prepareMutation(line) + copyMem(addr line[0], p, nitems) + + let op = cast[HttpHandle](userdata) + if not op.statusline: + op.statusline = true + var status: clong + op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) + if status == 103 and op.earlyhint == NO_EARLY_HINT: + op.earlyhint = EARLY_HINT_STARTED + else: + op.connectreport = true + stdout.write("Status: " & $status & "\n") + stdout.write("Cha-Control: ControlDone\n") + return nitems + + if line == "": + # empty line (last, before body) + if op.earlyhint == EARLY_HINT_STARTED: + # ignore; we do not have a way to stream headers yet. + op.earlyhint = EARLY_HINT_DONE + # reset statusline; we are awaiting the next line. + op.statusline = false + return nitems + return nitems + + if op.earlyhint != EARLY_HINT_STARTED: + # Regrettably, we can only write early hint headers after the status + # code is already known. + # For now, it seems easiest to just ignore them all. + stdout.write(line) + return nitems + +# From the documentation: size is always 1. +proc curlWriteBody(p: cstring, size, nmemb: csize_t, userdata: pointer): + csize_t {.cdecl.} = + return csize_t(stdout.writeBuffer(p, int(nmemb))) + +# From the documentation: size is always 1. +proc readFromStdin(buffer: cstring, size, nitems: csize_t, userdata: pointer): + csize_t {.cdecl.} = + return csize_t(stdin.readBuffer(buffer, nitems)) + +proc curlPreRequest(clientp: pointer, conn_primary_ip, conn_local_ip: cstring, + conn_primary_port, conn_local_port: cint): cint {.cdecl.} = + let op = cast[HttpHandle](clientp) + op.connectreport = true + stdout.write("Cha-Control: Connected\n") + return 0 # ok + +proc main() = + let curl = curl_easy_init() + doAssert curl != nil + let surl = getEnv("QUERY_STRING") + curl.setopt(CURLOPT_URL, surl) + let op = HttpHandle(curl: curl) + curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody) + curl.setopt(CURLOPT_HEADERDATA, op) + curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader) + curl.setopt(CURLOPT_PREREQDATA, op) + curl.setopt(CURLOPT_PREREQFUNCTION, curlPreRequest) + let proxy = getEnv("ALL_PROXY") + if proxy != "": + curl.setopt(CURLOPT_PROXY, proxy) + case getEnv("REQUEST_METHOD") + of "GET": + curl.setopt(CURLOPT_HTTPGET, 1) + of "POST": + curl.setopt(CURLOPT_POST, 1) + let len = parseInt64(getEnv("CONTENT_LENGTH")).get + # > For any given platform/compiler curl_off_t must be typedef'ed to + # a 64-bit + # > wide signed integral data type. The width of this data type must remain + # > constant and independent of any possible large file support settings. + # > + # > As an exception to the above, curl_off_t shall be typedef'ed to + # a 32-bit + # > wide signed integral data type if there is no 64-bit type. + # It seems safe to assume that if the platform has no uint64 then Nim won't + # compile either. In return, we are allowed to post >2G of data. + curl.setopt(CURLOPT_POSTFIELDSIZE_LARGE, uint64(len)) + curl.setopt(CURLOPT_READFUNCTION, readFromStdin) + else: discard #TODO + let headers = getEnv("REQUEST_HEADERS") + for line in headers.split("\r\n"): + if line.startsWithNoCase("Accept-Encoding: "): + let s = line.after(' ') + # From the CURLOPT_ACCEPT_ENCODING manpage: + # > The application does not have to keep the string around after + # > setting this option. + curl.setopt(CURLOPT_ACCEPT_ENCODING, cstring(s)) + # This is OK, because curl_slist_append strdup's line. + op.slist = curl_slist_append(op.slist, cstring(line)) + if op.slist != nil: + curl.setopt(CURLOPT_HTTPHEADER, op.slist) + let res = curl_easy_perform(curl) + if res != CURLE_OK and not op.connectreport: + stdout.write(getCurlConnectionError(res)) + op.connectreport = true + curl_easy_cleanup(curl) + +main() diff --git a/doc/localcgi.md b/doc/localcgi.md index 56927634..af8e5288 100644 --- a/doc/localcgi.md +++ b/doc/localcgi.md @@ -65,7 +65,9 @@ Currently available commands are: sent before that. * `ConnectionError`: Must be the first reported header. Parameter 1 is the error code, see below. If any following parameters are given, they are - concatenated to form a custom error message. TODO implement this + concatenated to form a custom error message. + Note: short but descriptive error messages are preferred, messages that + do not fit on the screen are currently truncated. (TODO fix this somehow :P) * `ControlDone`: Signals that no more special headers will be sent; this means that `Cha-Control` and `Status` headers sent after this must be interpreted as regular headers (and thus e.g. will be available for JS @@ -74,7 +76,25 @@ Currently available commands are: take external input. For example, a HTTP client would have to send `Cha-Control: ControlDone` before returning the retrieved headers. -TODO insert list of public error codes here +List of public error codes: + +* `1 internal error`: An internal error prevented the script from retrieving + the requested resource. CGI scripts can also use this to signal that they + have no information on what went wrong. +* `2 invalid method`: The client requested data using a method not supported + by this protocol. +* `3 invalid URL`: The request URL could not be interpreted as a valid URL + for this format. +* `4 file not found`: No file was found at the requested address, and thus + the request is meaningless. Note: this should only be used by protocols + that do not rely on a client-server architecture, e.g. local file access, + local databases, or peer-to-peer file retrieval mechanisms. A server + responding with "no file found" is NOT a connection error, and is better + represented as a response with a 404 status code. +* `5 failed to resolve host`: The hostname could not be resolved. +* `6 failed to resolve proxy`: The proxy could not be resolved. +* `7 connection refused`: The server refused to establish a connection. +* `8 proxy refused to connect`: The proxy refused to establish a connection. ## Environment variables @@ -97,12 +117,15 @@ Chawan sets the following environment variables: variable is NOT percent-encoded. * `REQUEST_URI="$SCRIPT_NAME/$PATH_INFO?$QUERY_STRING` * `REQUEST_METHOD=` HTTP method used for making the request, e.g. GET or POST +* `REQUEST_HEADERS=` A newline-separated list of all headers for this request. * `CONTENT_TYPE=` for POST requests, the Content-Type header. Not set for other request types (e.g. GET). * `CONTENT_LENGTH=` the content length, if $CONTENT_TYPE has been set. -* `HTTP_PROXY=` and (lower case) `http_proxy=`: the proxy URL if a proxy - has been set and its scheme is either `http` or `https`. -* `ALL_PROXY=` if a proxy has been set, the proxy URL. +* `ALL_PROXY=` if a proxy has been set, the proxy URL. WARNING: for security + reasons, this MUST be respected when making external connections. If a + CGI script does not support proxies, it must never make any external + connections when the `ALL_PROXY` variable is set, even if this results in it + returning an error. * `HTTP_COOKIE=` if set, the Cookie header. * `HTTP_REFERER=` if set, the Referer header. diff --git a/res/urimethodmap b/res/urimethodmap index bba78e82..21f02533 100644 --- a/res/urimethodmap +++ b/res/urimethodmap @@ -1,5 +1,7 @@ # Default urimethodmap file for Chawan. +http: cgi-bin:http?%s +https: cgi-bin:http?%s finger: cgi-bin:cha-finger gemini: cgi-bin:gmifetch?%s about: cgi-bin:about diff --git a/src/bindings/curl.nim b/src/bindings/curl.nim index cd9e409a..a36a3fb7 100644 --- a/src/bindings/curl.nim +++ b/src/bindings/curl.nim @@ -95,15 +95,18 @@ type CURLOPT_HEADERDATA = CURLOPTTYPE_CBPOINT + 29 CURLOPT_ACCEPT_ENCODING = CURLOPTTYPE_STRINGPOINT + 102 CURLOPT_MIMEPOST = CURLOPTTYPE_OBJECTPOINT + 269 + CURLOPT_PREREQDATA = CURLOPTTYPE_CBPOINT + 313 # Functionpoint CURLOPT_WRITEFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 11 CURLOPT_READFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 12 CURLOPT_HEADERFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 79 + CURLOPT_PREREQFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 312 # Off-t CURLOPT_INFILESIZE_LARGE = CURLOPTTYPE_OFF_T + 115 CURLOPT_RESUME_FROM_LARGE = CURLOPTTYPE_OFF_T + 116 + CURLOPT_POSTFIELDSIZE_LARGE = CURLOPTTYPE_OFF_T + 120 # Blob CURLOPT_SSLCERT_BLOB = CURLOPTTYPE_BLOB + 291 diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim index a8081ecc..a825e94d 100644 --- a/src/css/cssparser.nim +++ b/src/css/cssparser.nim @@ -248,7 +248,7 @@ proc consumeEscape(state: var CSSTokenizerState): string = num *= 0x10 num += hexValue(c) inc i - if state.peek() in AsciiWhitespace: + if state.has() and state.peek() in AsciiWhitespace: discard state.consume() if num == 0 or num > 0x10FFFF or num in 0xD800..0xDFFF: return $Rune(0xFFFD) diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim index 89361a7c..8fe96274 100644 --- a/src/loader/cgi.nim +++ b/src/loader/cgi.nim @@ -10,6 +10,7 @@ import loader/connecterror import loader/headers import loader/loaderhandle import loader/request +import types/formdata import types/opt import types/url import utils/twtstr @@ -37,6 +38,10 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI: string, request: Request, putEnv("SCRIPT_FILENAME", cmd) putEnv("REQUEST_URI", requestURI) putEnv("REQUEST_METHOD", $request.httpmethod) + var headers = "" + for k, v in request.headers: + headers &= k & ": " & v & "\r\n" + putEnv("REQUEST_HEADERS", headers) if prevURL != nil: putMappedURL(prevURL) if pathInfo != "": @@ -44,19 +49,17 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI: string, request: Request, if url.query.isSome: putEnv("QUERY_STRING", url.query.get) if request.httpmethod == HTTP_POST: - putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", "")) + if request.multipart.isSome: + putEnv("CONTENT_TYPE", request.multipart.get.getContentType()) + else: + putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", "")) putEnv("CONTENT_LENGTH", $contentLen) if "Cookie" in request.headers: putEnv("HTTP_COOKIE", request.headers["Cookie"]) if request.referer != nil: putEnv("HTTP_REFERER", $request.referer) if request.proxy != nil: - let s = $request.proxy - if request.proxy.scheme == "https" or request.proxy.scheme == "http": - putEnv("http_proxy", s) - putEnv("HTTP_PROXY", s) - putEnv("HTTPS_proxy", s) - putEnv("ALL_PROXY", s) + putEnv("ALL_PROXY", $request.proxy) type ControlResult = enum RESULT_CONTROL_DONE, RESULT_CONTROL_CONTINUE, RESULT_ERROR @@ -184,9 +187,7 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string], if request.body.isSome: contentLen = request.body.get.len elif request.multipart.isSome: - #TODO multipart - # maybe use curl formdata? (the mime api has no serialization functions) - discard + contentLen = request.multipart.get.calcLength() let pid = fork() if pid == -1: t handle.sendResult(ERROR_FAIL_SETUP_CGI) @@ -214,8 +215,9 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string], if request.body.isSome: ps.write(request.body.get) elif request.multipart.isSome: - #TODO - discard + let multipart = request.multipart.get + for entry in multipart.entries: + ps.writeEntry(entry, multipart.boundary) ps.close() let ps = newPosixStream(pipefd[0]) let headers = newHeaders() @@ -249,8 +251,4 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string], handle.handleLine(line, headers) t handle.sendStatus(status) t handle.sendHeaders(headers) - var buffer: array[4096, uint8] - while not ps.atEnd: - let n = ps.readData(addr buffer[0], buffer.len) - t handle.sendData(addr buffer[0], n) - ps.close() + handle.istream = ps diff --git a/src/loader/connecterror.nim b/src/loader/connecterror.nim index 913e007f..acd4a28f 100644 --- a/src/loader/connecterror.nim +++ b/src/loader/connecterror.nim @@ -1,28 +1,31 @@ -import bindings/curl - type ConnectErrorCode* = enum - ERROR_CGI_NO_DATA = (-17, "CGI script returned no data") - ERROR_CGI_MALFORMED_HEADER = (-16, "CGI script returned a malformed header") - ERROR_CGI_INVALID_CHA_CONTROL = (-15, "CGI got invalid Cha-Control header") - ERROR_TOO_MANY_REWRITES = (-14, "too many URI method map rewrites") - ERROR_INVALID_URI_METHOD_ENTRY = (-13, "invalid URI method entry") - ERROR_CGI_FILE_NOT_FOUND = (-12, "CGI file not found") - ERROR_INVALID_CGI_PATH = (-11, "invalid CGI path") - ERROR_FAIL_SETUP_CGI = (-10, "failed to set up CGI script") - ERROR_NO_CGI_DIR = (-9, "no local-CGI directory configured") - ERROR_INVALID_METHOD = (-8, "invalid method") - ERROR_INVALID_URL = (-7, "invalid URL") - ERROR_CONNECTION_REFUSED = (-6, "connection refused") - ERROR_FILE_NOT_FOUND = (-5, "file not found") + ERROR_CGI_NO_DATA = (-13, "CGI script returned no data") + ERROR_CGI_MALFORMED_HEADER = (-12, "CGI script returned a malformed header") + ERROR_CGI_INVALID_CHA_CONTROL = (-11, "CGI got invalid Cha-Control header") + ERROR_TOO_MANY_REWRITES = (-10, "too many URI method map rewrites") + ERROR_INVALID_URI_METHOD_ENTRY = (-9, "invalid URI method entry") + ERROR_CGI_FILE_NOT_FOUND = (-8, "CGI file not found") + ERROR_INVALID_CGI_PATH = (-7, "invalid CGI path") + ERROR_FAIL_SETUP_CGI = (-6, "failed to set up CGI script") + ERROR_NO_CGI_DIR = (-5, "no local-CGI directory configured") ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found") ERROR_LOADER_KILLED = (-3, "loader killed during transfer") ERROR_DISALLOWED_URL = (-2, "url not allowed by filter") ERROR_UNKNOWN_SCHEME = (-1, "unknown scheme") + CONNECTION_SUCCESS = (0, "connection successful") + ERROR_INTERNAL = (1, "internal error") + ERROR_INVALID_METHOD = (2, "invalid method") + ERROR_INVALID_URL = (3, "invalid URL") + ERROR_FILE_NOT_FOUND = (4, "file not found") + ERROR_CONNECTION_REFUSED = (5, "connection refused") + ERROR_PROXY_REFUSED_TO_CONNECT = (6, "proxy refused to connect") + ERROR_FAILED_TO_RESOLVE_HOST = (7, "failed to resolve host") + ERROR_FAILED_TO_RESOLVE_PROXY = (8, "failed to resolve proxy") converter toInt*(code: ConnectErrorCode): int = return int(code) func getLoaderErrorMessage*(code: int): string = - if code < 0: + if code in int(ConnectErrorCode.low)..int(ConnectErrorCode.high): return $ConnectErrorCode(code) - return $curl_easy_strerror(CURLcode(cint(code))) + return "unexpected error code " & $code diff --git a/src/loader/curlhandle.nim b/src/loader/curlhandle.nim deleted file mode 100644 index 3c69c6c0..00000000 --- a/src/loader/curlhandle.nim +++ /dev/null @@ -1,32 +0,0 @@ -import bindings/curl -import loader/headers -import loader/loaderhandle -import loader/request - -type - CurlHandle* = ref object of RootObj - curl*: CURL - statusline*: bool - headers*: Headers - request*: Request - handle*: LoaderHandle - mime*: curl_mime - slist*: curl_slist - finish*: proc(handle: CurlHandle) - -func newCurlHandle*(curl: CURL, request: Request, handle: LoaderHandle): - CurlHandle = - return CurlHandle( - headers: newHeaders(), - curl: curl, - handle: handle, - request: request - ) - -proc cleanup*(handleData: CurlHandle) = - handleData.handle.close() - if handleData.mime != nil: - curl_mime_free(handleData.mime) - if handleData.slist != nil: - curl_slist_free_all(handleData.slist) - curl_easy_cleanup(handleData.curl) diff --git a/src/loader/http.nim b/src/loader/http.nim deleted file mode 100644 index d7bc3a8f..00000000 --- a/src/loader/http.nim +++ /dev/null @@ -1,137 +0,0 @@ -import options -import strutils - -import bindings/curl -import loader/curlhandle -import loader/curlwrap -import loader/headers -import loader/loaderhandle -import loader/request -import types/blob -import types/formdata -import types/opt -import types/url -import utils/twtstr - -type - EarlyHintState = enum - NO_EARLY_HINT, EARLY_HINT_STARTED, EARLY_HINT_DONE - - HttpHandle = ref object of CurlHandle - earlyhint: EarlyHintState - -func newHttpHandle(curl: CURL, request: Request, handle: LoaderHandle): - HttpHandle = - return HttpHandle( - headers: newHeaders(), - curl: curl, - handle: handle, - request: request - ) - -proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, - userdata: pointer): csize_t {.cdecl.} = - var line = newString(nitems) - if nitems > 0: - prepareMutation(line) - copyMem(addr line[0], p, nitems) - - let op = cast[HttpHandle](userdata) - if not op.statusline: - op.statusline = true - if op.earlyhint == NO_EARLY_HINT: - if not op.handle.sendResult(int(CURLE_OK)): - return 0 - var status: clong - op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) - if status == 103 and op.earlyhint == NO_EARLY_HINT: - op.earlyhint = EARLY_HINT_STARTED - else: - if not op.handle.sendStatus(cast[int](status)): - return 0 - return nitems - - let k = line.until(':') - - if k.len == line.len: - # empty line (last, before body) or invalid (=> error) - if op.earlyhint == EARLY_HINT_STARTED: - # ignore; we do not have a way to stream headers yet. - op.earlyhint = EARLY_HINT_DONE - # reset statusline; we are awaiting the next line. - op.statusline = false - return nitems - if not op.handle.sendHeaders(op.headers): - return 0 - return nitems - - let v = line.substr(k.len + 1).strip() - op.headers.add(k, v) - return nitems - -# From the documentation: size is always 1. -proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, - userdata: pointer): csize_t {.cdecl.} = - let handleData = cast[HttpHandle](userdata) - if nmemb > 0: - if not handleData.handle.sendData(p, int(nmemb)): - return 0 - return nmemb - -proc applyPostBody(curl: CURL, request: Request, handleData: HttpHandle) = - if request.multipart.isOk: - handleData.mime = curl_mime_init(curl) - doAssert handleData.mime != nil - for entry in request.multipart.get: - let part = curl_mime_addpart(handleData.mime) - doAssert part != nil - curl_mime_name(part, cstring(entry.name)) - if entry.isstr: - curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len)) - else: - let blob = entry.value - if blob.isfile: #TODO ? - curl_mime_filedata(part, cstring(WebFile(blob).path)) - else: - curl_mime_data(part, blob.buffer, csize_t(blob.size)) - # may be overridden by curl_mime_filedata, so set it here - curl_mime_filename(part, cstring(entry.filename)) - curl.setopt(CURLOPT_MIMEPOST, handleData.mime) - elif request.body.issome: - curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get)) - curl.setopt(CURLOPT_POSTFIELDSIZE, request.body.get.len) - -proc loadHttp*(handle: LoaderHandle, curlm: CURLM, - request: Request): HttpHandle = - let curl = curl_easy_init() - doAssert curl != nil - let surl = request.url.serialize() - curl.setopt(CURLOPT_URL, surl) - let handleData = curl.newHttpHandle(request, handle) - curl.setopt(CURLOPT_WRITEDATA, handleData) - curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody) - curl.setopt(CURLOPT_HEADERDATA, handleData) - curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader) - if "Accept-Encoding" in request.headers: - let s = request.headers["Accept-Encoding"] - curl.setopt(CURLOPT_ACCEPT_ENCODING, cstring(s)) - if request.proxy != nil: - let purl = request.proxy.serialize() - curl.setopt(CURLOPT_PROXY, purl) - case request.httpmethod - of HTTP_GET: - curl.setopt(CURLOPT_HTTPGET, 1) - of HTTP_POST: - curl.setopt(CURLOPT_POST, 1) - curl.applyPostBody(request, handleData) - else: discard #TODO - for k, v in request.headers: - let header = k & ": " & v - handleData.slist = curl_slist_append(handleData.slist, cstring(header)) - if handleData.slist != nil: - curl.setopt(CURLOPT_HTTPHEADER, handleData.slist) - let res = curl_multi_add_handle(curlm, curl) - if res != CURLM_OK: - discard handle.sendResult(int(res)) - return nil - return handleData diff --git a/src/loader/loader.nim b/src/loader/loader.nim index 2116a403..acc7817a 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -11,15 +11,15 @@ # # The body is passed to the stream as-is, so effectively nothing can follow it. -import nativesockets -import net -import options -import posix -import streams -import strutils -import tables - -import bindings/curl +import std/nativesockets +import std/net +import std/options +import std/posix +import std/selectors +import std/streams +import std/strutils +import std/tables + import io/posixstream import io/promise import io/serialize @@ -30,9 +30,7 @@ import js/error import js/javascript import loader/cgi import loader/connecterror -import loader/curlhandle import loader/headers -import loader/http import loader/loaderhandle import loader/request import loader/response @@ -81,12 +79,11 @@ type refcount: int ssock: ServerSocket alive: bool - curlm: CURLM config: LoaderConfig - extra_fds: seq[curl_waitfd] - handleList: seq[CurlHandle] handleMap: Table[int, LoaderHandle] referrerpolicy: ReferrerPolicy + selector: Selector[int] + fd: int LoaderConfig* = object defaultheaders*: Headers @@ -102,12 +99,6 @@ type FetchPromise* = Promise[JSResult[Response]] -proc addFd(ctx: LoaderContext, fd: int, flags: int) = - ctx.extra_fds.add(curl_waitfd( - fd: cast[cint](fd), - events: cast[cshort](flags) - )) - #TODO this may be too low if we want to use urimethodmap for everything const MaxRewrites = 4 @@ -134,14 +125,18 @@ proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) = inc tries redo = true continue - case request.url.scheme - of "http", "https": - let handleData = handle.loadHttp(ctx.curlm, request) - if handleData != nil: - ctx.handleList.add(handleData) - of "cgi-bin": + if request.url.scheme == "cgi-bin": handle.loadCGI(request, ctx.config.cgiDir, prevurl) - handle.close() + if handle.istream == nil: + handle.close() + else: + let fd = handle.istream.fd + ctx.selector.registerHandle(fd, {Read}, 0) + let ofl = fcntl(fd, F_GETFL, 0) + discard fcntl(fd, F_SETFL, ofl or O_NONBLOCK) + # yes, this puts the istream fd in addition to the ostream fd in + # handlemap to point to the same ref + ctx.handleMap[fd] = handle else: prevurl = request.url case ctx.config.urimethodmap.findAndRewrite(request.url) @@ -234,39 +229,24 @@ proc acceptConnection(ctx: LoaderContext) = # (TODO: this is probably not a very good idea.) stream.close() -proc finishCurlTransfer(ctx: LoaderContext, handleData: CurlHandle, res: int) = - if res != int(CURLE_OK): - discard handleData.handle.sendResult(int(res)) - if handleData.finish != nil: - handleData.finish(handleData) - discard curl_multi_remove_handle(ctx.curlm, handleData.curl) - handleData.cleanup() - proc exitLoader(ctx: LoaderContext) = - for handleData in ctx.handleList: - ctx.finishCurlTransfer(handleData, ERROR_LOADER_KILLED) - discard curl_multi_cleanup(ctx.curlm) - curl_global_cleanup() ctx.ssock.close() quit(0) var gctx: LoaderContext proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext = - if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: - raise newException(Defect, "Failed to initialize libcurl.") - let curlm = curl_multi_init() - if curlm == nil: - raise newException(Defect, "Failed to initialize multi handle.") var ctx = LoaderContext( alive: true, - curlm: curlm, config: config, - refcount: 1 + refcount: 1, + selector: newSelector[int]() ) gctx = ctx #TODO ideally, buffered would be true. Unfortunately this conflicts with # sendFileHandle/recvFileHandle. ctx.ssock = initServerSocket(buffered = false) + ctx.fd = int(ctx.ssock.sock.getFd()) + ctx.selector.registerHandle(ctx.fd, {Read}, 0) # The server has been initialized, so the main process can resume execution. var writef: File if not open(writef, FileHandle(fd), fmWrite): @@ -278,7 +258,6 @@ proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext = onSignal SIGTERM, SIGINT: discard sig gctx.exitLoader() - ctx.addFd(int(ctx.ssock.sock.getFd()), CURL_WAIT_POLLIN) for dir in ctx.config.cgiDir.mitems: if dir.len > 0 and dir[^1] != '/': dir &= '/' @@ -286,31 +265,40 @@ proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext = proc runFileLoader*(fd: cint, config: LoaderConfig) = var ctx = initLoaderContext(fd, config) + var buffer {.noInit.}: array[16384, uint8] while ctx.alive: - var numfds: cint = 0 - #TODO do not discard - discard curl_multi_poll(ctx.curlm, addr ctx.extra_fds[0], - cuint(ctx.extra_fds.len), 30_000, addr numfds) - discard curl_multi_perform(ctx.curlm, addr numfds) - for extra_fd in ctx.extra_fds.mitems: - # For now, this is always ssock.sock.getFd(). - if extra_fd.events == extra_fd.revents: - ctx.acceptConnection() - extra_fd.revents = 0 - var msgs_left: cint = 1 - while msgs_left > 0: - let msg = curl_multi_info_read(ctx.curlm, addr msgs_left) - if msg == nil: - break - if msg.msg == CURLMSG_DONE: # the only possible value atm - var idx = -1 - for i in 0 ..< ctx.handleList.len: - if ctx.handleList[i].curl == msg.easy_handle: - idx = i - break - assert idx != -1 - ctx.finishCurlTransfer(ctx.handleList[idx], int(msg.data.result)) - ctx.handleList.del(idx) + let events = ctx.selector.select(-1) + var unreg: seq[int] + for event in events: + if Read in event.events: + if event.fd == ctx.fd: # incoming connection + ctx.acceptConnection() + else: + let handle = ctx.handleMap[event.fd] + while not handle.istream.atEnd: + try: + let n = handle.istream.readData(addr buffer[0], buffer.len) + if not handle.sendData(addr buffer[0], n): + unreg.add(event.fd) + break + except ErrorAgain, ErrorWouldBlock: + break + if Error in event.events: + assert event.fd != ctx.fd + when defined(debug): + # sanity check + let handle = ctx.handleMap[event.fd] + if not handle.istream.atEnd(): + let n = handle.istream.readData(addr buffer[0], buffer.len) + assert n == 0 + assert handle.istream.atEnd() + unreg.add(event.fd) + for fd in unreg: + ctx.selector.unregister(fd) + let handle = ctx.handleMap[fd] + ctx.handleMap.del(fd) + ctx.handleMap.del(handle.getFd()) + handle.close() ctx.exitLoader() proc getAttribute(contentType, attrname: string): string = diff --git a/src/loader/loaderhandle.nim b/src/loader/loaderhandle.nim index d8d01bb2..93367607 100644 --- a/src/loader/loaderhandle.nim +++ b/src/loader/loaderhandle.nim @@ -9,6 +9,8 @@ import loader/headers type LoaderHandle* = ref object ostream: Stream + # Stream for taking input + istream*: PosixStream # Only the first handle can be redirected, because a) mailcap can only # redirect the first handle and b) async redirects would result in race # conditions that would be difficult to untangle. @@ -100,3 +102,5 @@ proc close*(handle: LoaderHandle) = discard handle.sostream.close() handle.ostream.close() + if handle.istream != nil: + handle.istream.close() diff --git a/src/server/buffer.nim b/src/server/buffer.nim index cdbfc16b..77404c7c 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -315,7 +315,7 @@ func getClickable(styledNode: StyledNode): Element = return Element(styledNode.node) styledNode = stylednode.parent -func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] +proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] func canSubmitOnClick(fae: FormAssociatedElement): bool = if fae.form == nil: @@ -330,7 +330,7 @@ func canSubmitOnClick(fae: FormAssociatedElement): bool = return true return false -func getClickHover(styledNode: StyledNode): string = +proc getClickHover(styledNode: StyledNode): string = let clickable = styledNode.getClickable() if clickable != nil: case clickable.tagType @@ -1084,7 +1084,7 @@ proc dispatchEvent(buffer: Buffer, ctype: string, elem: Element): tuple[ break return (called, canceled) -const BufferSize = 4096 +const BufferSize = 16384 proc finishLoad(buffer: Buffer): EmptyPromise = if buffer.state != LOADING_PAGE: @@ -1148,7 +1148,7 @@ proc onload(buffer: Buffer) = of LOADING_PAGE: discard let op = buffer.sstream.getPosition() - var s = newSeqUninitialized[uint8](buffer.readbufsize) + var s {.noInit.}: array[16384, uint8] try: buffer.sstream.setPosition(op + buffer.available) let n = buffer.istream.readData(addr s[0], buffer.readbufsize) @@ -1222,7 +1222,7 @@ proc serializePlainTextFormData(kvs: seq[(string, string)]): string = result &= "\r\n" # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm -func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = +proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = if form.constructingEntryList: return let entrylist = form.constructEntryList(submitter).get(@[]) @@ -1352,7 +1352,7 @@ type ReadSuccessResult* = object open*: Option[Request] repaint*: bool -func implicitSubmit(input: HTMLInputElement): Option[Request] = +proc implicitSubmit(input: HTMLInputElement): Option[Request] = let form = input.form if form != nil and form.canSubmitImplicitly(): var defaultButton: Element diff --git a/src/types/blob.nim b/src/types/blob.nim index 9ddca2b5..5da7317d 100644 --- a/src/types/blob.nim +++ b/src/types/blob.nim @@ -1,5 +1,6 @@ -import options -import strutils +import std/options +import std/os +import std/strutils import js/dict import js/fromjs @@ -92,12 +93,14 @@ proc newWebFile(ctx: JSContext, fileBits: seq[string], fileName: string, #TODO File, Blob constructors -func size*(this: WebFile): uint64 {.jsfget.} = - #TODO use stat instead +proc getSize*(this: Blob): uint64 = if this.isfile: - return uint64(this.file.getFileSize()) + return uint64(WebFile(this).path.getFileSize()) return this.size +proc size*(this: WebFile): uint64 {.jsfget.} = + return this.getSize() + func name*(this: WebFile): string {.jsfget.} = if this.path.len > 0 and this.path[^1] != '/': return this.path.afterLast('/') diff --git a/src/types/formdata.nim b/src/types/formdata.nim index 2bc26e22..b1957998 100644 --- a/src/types/formdata.nim +++ b/src/types/formdata.nim @@ -1,5 +1,9 @@ +import std/streams +import std/strutils + import js/javascript import types/blob +import utils/twtstr type FormDataEntry* = object @@ -13,9 +17,71 @@ type FormData* = ref object entries*: seq[FormDataEntry] + boundary*: string jsDestructor(FormData) iterator items*(this: FormData): FormDataEntry {.inline.} = for entry in this.entries: yield entry + +proc calcLength*(this: FormData): int = + result = 0 + for entry in this.entries: + result += "--\r\n".len + this.boundary.len # always have boundary + #TODO maybe make CRLF for name first? + result += entry.name.len # always have name + # these must be percent-encoded, with 2 char overhead: + result += entry.name.count({'\r', '\n', '"'}) * 2 + if entry.isstr: + result += "Content-Disposition: form-data; name=\"\"\r\n".len + result += entry.svalue.len + else: + result += "Content-Disposition: form-data; name=\"\";".len + # file name + result += " filename=\"\"\r\n".len + result += entry.filename.len + # dquot must be quoted with 2 char overhead + result += entry.filename.count('"') * 2 + # content type + result += "Content-Type: \r\n".len + result += entry.value.ctype.len + if entry.value.isfile: + result += int(WebFile(entry.value).getSize()) + else: + result += int(entry.value.size) + result += "\r\n".len # header is always followed by \r\n + result += "\r\n".len # value is always followed by \r\n + +proc getContentType*(this: FormData): string = + return "multipart/form-data; boundary=" & this.boundary + +proc writeEntry*(stream: Stream, entry: FormDataEntry, boundary: string) = + stream.write("--" & boundary & "\r\n") + let name = percentEncode(entry.name, {'"', '\r', '\n'}) + if entry.isstr: + stream.write("Content-Disposition: form-data; name=\"" & name & "\"\r\n") + stream.write("\r\n") + stream.write(entry.svalue) + else: + stream.write("Content-Disposition: form-data; name=\"" & name & "\";") + let filename = percentEncode(entry.filename, {'"', '\r', '\n'}) + stream.write(" filename=\"" & filename & "\"\r\n") + let blob = entry.value + let ctype = if blob.ctype == "": + "application/octet-stream" + else: + blob.ctype + stream.write("Content-Type: " & ctype & "\r\n") + if blob.isfile: + let fs = newFileStream(WebFile(blob).path) + if fs != nil: + var buf {.noInit.}: array[4096, uint8] + while true: + let n = fs.readData(addr buf[0], 4096) + stream.writeData(addr buf[0], n) + if n != 4096: break + else: + stream.writeData(blob.buffer, int(blob.size)) + stream.write("\r\n") + stream.write("\r\n") diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim index 84c13402..98c96b54 100644 --- a/src/xhr/formdata.nim +++ b/src/xhr/formdata.nim @@ -1,3 +1,6 @@ +import std/base64 +import std/streams + import html/dom import html/enums import js/domexception @@ -12,12 +15,20 @@ import chame/tags proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): Option[seq[FormDataEntry]] + +proc generateBoundary(): string = + let urandom = newFileStream("/dev/urandom") + let s = urandom.readStr(32) + urandom.close() + # 32 * 4 / 3 (padded) = 44 + prefix string is 22 bytes = 66 bytes + return "----WebKitFormBoundary" & base64.encode(s) + proc newFormData0*(): FormData = - return FormData() + return FormData(boundary: generateBoundary()) proc newFormData*(form: HTMLFormElement = nil, submitter: HTMLElement = nil): DOMResult[FormData] {.jsctor.} = - let this = FormData() + let this = newFormData0() if form != nil: if submitter != nil: if not submitter.isSubmitButton(): |