diff options
Diffstat (limited to 'src')
-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/curlwrap.nim | 10 | ||||
-rw-r--r-- | src/loader/dirlist.nim | 60 | ||||
-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 |
14 files changed, 195 insertions, 358 deletions
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/curlwrap.nim b/src/loader/curlwrap.nim deleted file mode 100644 index 7aef4182..00000000 --- a/src/loader/curlwrap.nim +++ /dev/null @@ -1,10 +0,0 @@ -import bindings/curl - -template setopt*(curl: CURL, opt: CURLoption, arg: typed) = - discard curl_easy_setopt(curl, opt, arg) - -template setopt*(curl: CURL, opt: CURLoption, arg: string) = - discard curl_easy_setopt(curl, opt, cstring(arg)) - -template getinfo*(curl: CURL, info: CURLINFO, arg: typed) = - discard curl_easy_getinfo(curl, info, arg) diff --git a/src/loader/dirlist.nim b/src/loader/dirlist.nim deleted file mode 100644 index 2328cd55..00000000 --- a/src/loader/dirlist.nim +++ /dev/null @@ -1,60 +0,0 @@ -import algorithm - -import utils/twtstr - -type DirlistItemType = enum - ITEM_FILE, ITEM_LINK, ITEM_DIR - -type DirlistItem* = object - name*: string - modified*: string - case t*: DirlistItemType - of ITEM_LINK: - linkto*: string - of ITEM_FILE: - nsize*: int - of ITEM_DIR: - discard - -type NameWidthTuple = tuple[name: string, width: int] - -func makeDirlist*(items: seq[DirlistItem]): string = - var names: seq[NameWidthTuple] - var maxw = 20 - for item in items: - var name = item.name - if item.t == ITEM_LINK: - name &= '@' - elif item.t == ITEM_DIR: - name &= '/' - let w = name.width() - maxw = max(w, maxw) - names.add((name, w)) - names.sort(proc(a, b: NameWidthTuple): int = cmp(a.name, b.name)) - var outs = "<A HREF=\"../\">[Upper Directory]</A>\n" - for i in 0 ..< items.len: - let item = items[i] - var (name, width) = names[i] - var path = percentEncode(item.name, PathPercentEncodeSet) - if item.t == ITEM_LINK: - if item.linkto.len > 0 and item.linkto[^1] == '/': - # If the target is a directory, treat it as a directory. (For FTP.) - path &= '/' - elif item.t == ITEM_DIR: - path &= '/' - var line = "<A HREF=\"" & path & "\">" & htmlEscape(name) & "</A>" - while width <= maxw: - if width mod 2 == 0: - line &= ' ' - else: - line &= '.' - inc width - if line[^1] != ' ': - line &= ' ' - line &= htmlEscape(item.modified) - if item.t == ITEM_FILE: - line &= ' ' & convert_size(item.nsize) - elif item.t == ITEM_LINK: - line &= " -> " & htmlEscape(item.linkto) - outs &= line & '\n' - return outs 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(): |