diff options
author | bptato <nincsnevem662@gmail.com> | 2024-05-30 00:19:48 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-06-20 17:50:22 +0200 |
commit | 60dc37269cd2dc8cdf23d9f77680f6af9490032f (patch) | |
tree | 9a72ba24daffa546f92704e7e06cf84fded2d89d | |
parent | a146a22b11cea39bc691417d9d9a1292b7177552 (diff) | |
download | chawan-60dc37269cd2dc8cdf23d9f77680f6af9490032f.tar.gz |
img, loader: separate out png codec into cgi, misc improvements
* multi-processed and sandboxed PNG decoding & encoding (through local CGI) * improved request body passing (including support for output id as response body) * simplified & faster blob()/text() - now every request starts suspended, and OngoingData.buf has been replaced with loader's buffering capability * image caching: we no longer pull bitmaps from the container after every single getLines call Next steps: replace our bespoke PNG decoder with something more usable, add other decoders, and make them stream.
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | adapter/img/png.nim (renamed from src/img/png.nim) | 38 | ||||
-rw-r--r-- | res/urimethodmap | 1 | ||||
-rw-r--r-- | src/html/chadombuilder.nim | 2 | ||||
-rw-r--r-- | src/html/dom.nim | 168 | ||||
-rw-r--r-- | src/img/bitmap.nim | 6 | ||||
-rw-r--r-- | src/img/painter.nim | 6 | ||||
-rw-r--r-- | src/io/bufreader.nim | 41 | ||||
-rw-r--r-- | src/io/bufwriter.nim | 26 | ||||
-rw-r--r-- | src/io/promise.nim | 10 | ||||
-rw-r--r-- | src/io/urlfilter.nim | 2 | ||||
-rw-r--r-- | src/js/console.nim | 2 | ||||
-rw-r--r-- | src/js/jscolor.nim | 4 | ||||
-rw-r--r-- | src/loader/cgi.nim | 53 | ||||
-rw-r--r-- | src/loader/loader.nim | 160 | ||||
-rw-r--r-- | src/loader/loaderhandle.nim | 41 | ||||
-rw-r--r-- | src/loader/request.nim | 49 | ||||
-rw-r--r-- | src/loader/response.nim | 124 | ||||
-rw-r--r-- | src/local/client.nim | 4 | ||||
-rw-r--r-- | src/local/container.nim | 8 | ||||
-rw-r--r-- | src/local/pager.nim | 40 | ||||
-rw-r--r-- | src/local/term.nim | 6 | ||||
-rw-r--r-- | src/server/buffer.nim | 82 | ||||
-rw-r--r-- | src/types/blob.nim | 11 | ||||
-rw-r--r-- | src/types/cookie.nim | 5 | ||||
-rw-r--r-- | src/types/urimethodmap.nim | 9 | ||||
-rw-r--r-- | src/types/url.nim | 18 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 3 | ||||
-rw-r--r-- | todo | 9 |
29 files changed, 645 insertions, 291 deletions
diff --git a/Makefile b/Makefile index f4a816a2..fdb900bb 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,7 @@ all: $(OUTDIR_BIN)/cha $(OUTDIR_BIN)/mancha $(OUTDIR_CGI_BIN)/http \ $(OUTDIR_CGI_BIN)/cha-finger $(OUTDIR_CGI_BIN)/about \ $(OUTDIR_CGI_BIN)/data $(OUTDIR_CGI_BIN)/file $(OUTDIR_CGI_BIN)/ftp \ $(OUTDIR_CGI_BIN)/man $(OUTDIR_CGI_BIN)/spartan \ + $(OUTDIR_CGI_BIN)/png \ $(OUTDIR_LIBEXEC)/urldec $(OUTDIR_LIBEXEC)/urlenc \ $(OUTDIR_LIBEXEC)/md2html $(OUTDIR_LIBEXEC)/ansi2html @@ -167,6 +168,12 @@ $(OUTDIR_CGI_BIN)/gopher: adapter/protocol/gopher.nim adapter/protocol/curlwrap. $(NIMC) $(FLAGS) -d:curlLibName:$(CURLLIBNAME) --nimcache:"$(OBJDIR)/$(TARGET)/gopher" \ -o:"$(OUTDIR_CGI_BIN)/gopher" adapter/protocol/gopher.nim +$(OUTDIR_CGI_BIN)/png: adapter/img/png.nim src/utils/sandbox.nim + @mkdir -p "$(OUTDIR_CGI_BIN)" + $(NIMC) $(FLAGS) --nimcache:"$(OBJDIR)/$(TARGET)/png" \ + -d:disableSandbox=$(DANGER_DISABLE_SANDBOX) \ + -o:"$(OUTDIR_CGI_BIN)/png" adapter/img/png.nim + $(OUTDIR_LIBEXEC)/urldec: adapter/tools/urldec.nim src/utils/twtstr.nim @mkdir -p "$(OUTDIR_LIBEXEC)" $(NIMC) $(FLAGS) --nimcache:"$(OBJDIR)/$(TARGET)/urldec" \ @@ -221,6 +228,7 @@ install: install -m755 "$(OUTDIR_CGI_BIN)/cha-finger" $(LIBEXECDIR_CHAWAN)/cgi-bin install -m755 "$(OUTDIR_CGI_BIN)/man" $(LIBEXECDIR_CHAWAN)/cgi-bin install -m755 "$(OUTDIR_CGI_BIN)/spartan" $(LIBEXECDIR_CHAWAN)/cgi-bin + install -m755 "$(OUTDIR_CGI_BIN)/png" $(LIBEXECDIR_CHAWAN)/cgi-bin install -m755 "$(OUTDIR_LIBEXEC)/urldec" $(LIBEXECDIR_CHAWAN)/urldec install -m755 "$(OUTDIR_LIBEXEC)/urlenc" $(LIBEXECDIR_CHAWAN)/urlenc mkdir -p "$(DESTDIR)$(MANPREFIX1)" diff --git a/src/img/png.nim b/adapter/img/png.nim index 667b589f..b9d56cf0 100644 --- a/src/img/png.nim +++ b/adapter/img/png.nim @@ -1,7 +1,13 @@ +import std/options +import std/os +import std/strutils + import bindings/zlib import img/bitmap import types/color import utils/endians +import utils/sandbox +import utils/twtstr type PNGWriter = object buf: pointer @@ -504,3 +510,35 @@ proc fromPNG*(iq: openArray[uint8]): Bitmap = if not reader.isend: reader.err "IEND not found" return reader.bmp + +proc main() = + enterNetworkSandbox() + case getEnv("MAPPED_URI_PATH") + of "decode": + let s = stdin.readAll() + let bmp = fromPNG(s.toOpenArrayByte(0, s.high)) + if bmp != nil: + stdout.write("X-Image-Dimensions: " & $bmp.width & "x" & $bmp.height & + "\n\n") + discard stdout.writeBuffer(addr bmp.px[0], bmp.px.len * sizeof(bmp.px[0])) + of "encode": + let headers = getEnv("REQUEST_HEADERS") + let bmp = Bitmap() + for hdr in headers.split('\n'): + if hdr.until(':') == "X-Image-Dimensions": + let s = hdr.after(':').strip().split('x') + #TODO error handling + bmp.width = parseUInt64(s[0], allowSign = false).get + bmp.height = parseUInt64(s[1], allowSign = false).get + let L = bmp.width * bmp.height + bmp.px = cast[seq[ARGBColor]](newSeqUninitialized[uint32](L)) + doAssert stdin.readBuffer(addr bmp.px[0], L * 4) == int(L * 4) + var outlen: int + stdout.write("X-Image-Dimensions: " & $bmp.width & "x" & $bmp.height & + "\n\n") + let p = bmp.toPNG(outlen) + discard stdout.writeBuffer(p, outlen) + else: + stdout.write("Cha-Control: ConnectionError 4 invalid command") + +main() diff --git a/res/urimethodmap b/res/urimethodmap index e711bed6..d11dab86 100644 --- a/res/urimethodmap +++ b/res/urimethodmap @@ -16,3 +16,4 @@ spartan: cgi-bin:spartan man: cgi-bin:man man-k: cgi-bin:man man-l: cgi-bin:man +img-codec+png: cgi-bin:png diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim index d1450f64..446972cf 100644 --- a/src/html/chadombuilder.nim +++ b/src/html/chadombuilder.nim @@ -374,7 +374,7 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string): of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml": return err(newInternalError("XML parsing is not supported yet")) else: - return err(newTypeError("Invalid mime type")) + return errTypeError("Invalid mime type") proc addHTMLModule*(ctx: JSContext) = ctx.registerType(DOMParser) diff --git a/src/html/dom.nim b/src/html/dom.nim index 0a11791f..265810ce 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -17,12 +17,12 @@ import html/script import img/bitmap import img/painter import img/path -import img/png import io/dynstream import io/promise import js/console import js/domexception import js/timeout +import loader/headers import loader/loader import loader/request import monoucha/fromjs @@ -89,6 +89,8 @@ type factory*: CAtomFactory loadingResourcePromises*: seq[EmptyPromise] images*: bool + # ID of the next image + imageId: int # Navigator stuff Navigator* = object @@ -433,6 +435,9 @@ jsDestructor(CSSStyleDeclaration) proc parseColor(element: Element; s: string): ARGBColor +func console(window: Window): Console = + return window.internalConsole + proc resetTransform(state: var DrawingState) = state.transformMatrix = newIdentityMatrix(3) @@ -606,12 +611,41 @@ proc clip(ctx: CanvasRenderingContext2D; fillRule = cfrNonZero) {.jsfunc.} = # CanvasUserInterface # CanvasText +const unifont = readFile"res/unifont_jp-15.0.05.png" +proc loadUnifont(window: Window) = + #TODO this is very wrong, we should move the unifont file in a CGI script or + # something + if unifontBitmap != nil: + return + let request = newRequest( + newURL("img-codec+png:decode").get, + httpMethod = hmPost, + body = RequestBody(t: rbtString, s: unifont) + ) + let response = window.loader.doRequest(request) + assert response.res == 0 + let dims = response.headers.table["X-Image-Dimensions"][0] + let width = parseUInt64(dims.until('x'), allowSign = false).get + let height = parseUInt64(dims.after('x'), allowSign = false).get + let len = int(width) * int(height) + let bitmap = ImageBitmap( + px: cast[seq[ARGBColor]](newSeqUninitialized[uint32](len)), + width: width, + height: height + ) + window.loader.resume(response.outputId) + response.body.recvDataLoop(addr bitmap.px[0], len * 4) + response.body.sclose() + unifontBitmap = bitmap + #TODO maxwidth proc fillText(ctx: CanvasRenderingContext2D; text: string; x, y: float64) {.jsfunc.} = for v in [x, y]: if classify(v) in {fcInf, fcNegInf, fcNan}: return + #TODO should not be loaded here... + ctx.canvas.document_internal.window.loadUnifont() let vec = ctx.transform(Vector2D(x: x, y: y)) ctx.bitmap.fillText(text, vec.x, vec.y, ctx.state.fillStyle, ctx.state.textAlign) @@ -622,6 +656,8 @@ proc strokeText(ctx: CanvasRenderingContext2D; text: string; x, y: float64) for v in [x, y]: if classify(v) in {fcInf, fcNegInf, fcNan}: return + #TODO should not be loaded here... + ctx.canvas.document_internal.window.loadUnifont() let vec = ctx.transform(Vector2D(x: x, y: y)) ctx.bitmap.strokeText(text, vec.x, vec.y, ctx.state.strokeStyle, ctx.state.textAlign) @@ -1293,7 +1329,7 @@ func supports(tokenList: DOMTokenList; token: string): if it[0] == localName: let lowercase = token.toLowerAscii() return ok(lowercase in it[1]) - return err(newTypeError("No supported tokens defined for attribute")) + return errTypeError("No supported tokens defined for attribute") func value(tokenList: DOMTokenList): string {.jsfget.} = return $tokenList @@ -1486,7 +1522,7 @@ func url(location: Location): URL = proc setLocation*(document: Document; s: string): Err[JSError] {.jsfset: "location".} = if document.location == nil: - return err(newTypeError("document.location is not an object")) + return errTypeError("document.location is not an object") let url = parseURL(s) if url.isNone: return errDOMException("Invalid URL", "SyntaxError") @@ -2823,9 +2859,6 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} = var appliesFwdDecl*: proc(mqlist: MediaQueryList; window: Window): bool {.nimcall, noSideEffect.} -func console(window: Window): Console = - return window.internalConsole - # see https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet #TODO make this somewhat compliant with ^this proc loadResource(window: Window; link: HTMLLinkElement) = @@ -2862,6 +2895,10 @@ proc loadResource(window: Window; link: HTMLLinkElement) = ) window.loadingResourcePromises.add(p) +proc getImageId(window: Window): int = + result = window.imageId + inc window.imageId + proc loadResource(window: Window; image: HTMLImageElement) = if not window.images or image.fetchStarted: return @@ -2873,19 +2910,46 @@ proc loadResource(window: Window; image: HTMLImageElement) = if url.isSome: let url = url.get let p = window.loader.fetch(newRequest(url)) - .then(proc(res: JSResult[Response]): Promise[JSResult[Blob]] = + .then(proc(res: JSResult[Response]): Promise[JSResult[Response]] = if res.isNone: return - let res = res.get - if res.getContentType() == "image/png": - return res.blob() - ).then(proc(pngData: JSResult[Blob]) = - if pngData.isNone: + let response = res.get + let contentType = response.getContentType() + if contentType.until('/') != "image": + return + let request = newRequest( + newURL("img-codec+" & contentType.after('/') & ":decode").get, + httpMethod = hmPost, + body = RequestBody(t: rbtOutput, outputId: response.outputId) + ) + let r = window.loader.fetch(request) + window.loader.resume(response.outputId) + response.unregisterFun() + response.body.sclose() + return r + ).then(proc(res: JSResult[Response]): EmptyPromise = + if res.isNone: + return + let response = res.get + # we can close immediately; loader will not clean this output up until + # the `resume' command in pager. + response.unregisterFun() + response.body.sclose() + if "X-Image-Dimensions" notin response.headers.table: + window.console.error("X-Image-Dimensions missing") + return + let dims = response.headers.table["X-Image-Dimensions"][0] + let width = parseUInt64(dims.until('x'), allowSign = false) + let height = parseUInt64(dims.after('x'), allowSign = false) + if width.isNone or height.isNone: + window.console.error("wrong X-Image-Dimensions") return - let pngData = pngData.get - let buffer = cast[ptr UncheckedArray[uint8]](pngData.buffer) - let high = int(pngData.size) - 1 - image.bitmap = fromPNG(toOpenArray(buffer, 0, high)) + image.bitmap = NetworkBitmap( + width: width.get, + height: height.get, + outputId: response.outputId, + imageId: window.getImageId() + ) ) window.loadingResourcePromises.add(p) @@ -2897,7 +2961,7 @@ proc reflectEvent(element: Element; target: EventTarget; name: StaticAtom; let fun = ctx.newFunction(["event"], value) assert ctx != nil if JS_IsException(fun): - document.window.console.log("Exception in body content attribute of", + document.window.console.error("Exception in body content attribute of", urls, ctx.getExceptionMsg()) else: let jsTarget = ctx.toJS(target) @@ -3587,9 +3651,11 @@ proc fetchClassicScript(element: HTMLScriptElement; url: URL; if response.res != 0: element.onComplete(ScriptResult(t: RESULT_NULL)) return + window.loader.resume(response.outputId) let s = response.body.recvAll() let cs = if cs == CHARSET_UNKNOWN: CHARSET_UTF_8 else: cs let source = s.decodeAll(cs) + response.body.sclose() let script = window.jsctx.createClassicScript(source, url, options, false) element.onComplete(ScriptResult(t: RESULT_SCRIPT, script: script)) @@ -3685,11 +3751,12 @@ proc execute*(element: HTMLScriptElement) = let urls = script.baseURL.serialize(excludepassword = true) let ctx = window.jsctx if JS_IsException(script.record): - window.console.log("Exception in document", urls, ctx.getExceptionMsg()) + window.console.error("Exception in document", urls, + ctx.getExceptionMsg()) else: let ret = ctx.evalFunction(script.record) if JS_IsException(ret): - window.console.log("Exception in document", urls, + window.console.error("Exception in document", urls, ctx.getExceptionMsg()) JS_FreeValue(ctx, ret) document.currentScript = oldCurrentScript @@ -4174,17 +4241,58 @@ proc getContext*(jctx: JSContext; this: HTMLCanvasElement; contextId: string; #TODO quality should be `any' proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValue; - s = "image/png", quality: float64 = 1): JSValue {.jsfunc.} = - var outlen: int - let buf = this.bitmap.toPNG(outlen) - let blob = newBlob(buf, outlen, "image/png", proc() = dealloc(buf)) - var jsBlob = toJS(ctx, blob) - let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, jsBlob.toJSValueArray()) - JS_FreeValue(ctx, jsBlob) - # Hack. TODO: implement JSValue to callback - if res == JS_EXCEPTION: - return JS_EXCEPTION - JS_FreeValue(ctx, res) + contentType = "image/png"; quality: float64 = 1): JSValue {.jsfunc.} = + if not contentType.startsWith("image/"): + return + #TODO this is dumb (and slow) + var s = newString(this.bitmap.px.len * 4) + copyMem(addr s[0], addr this.bitmap.px[0], this.bitmap.px.len * 4) + let request = newRequest( + newURL("img-codec+" & contentType.after('/') & ":encode").get, + httpMethod = hmPost, + headers = newHeaders({ + "X-Image-Dimensions": $this.bitmap.width & 'x' & $this.bitmap.height + }), + body = RequestBody(t: rbtString, s: s) + ) + # callback will go out of scope when we return, so capture a new reference. + let callback = JS_DupValue(ctx, callback) + let window = this.document.window + window.loader.fetch(request).then(proc(res: JSResult[Response]): + EmptyPromise = + if res.isNone: + if contentType != "image/png": + # redo as PNG. + # Note: this sounds dumb, and is dumb, but also standard mandated so + # whatever. + discard ctx.toBlob(this, callback, "image/png", quality) + else: # the png decoder doesn't work... + window.console.error("missing/broken PNG decoder") + JS_FreeValue(ctx, callback) + return + let response = res.get + if "X-Image-Dimensions" notin response.headers.table: + window.console.error("X-Image-Dimensions missing") + JS_FreeValue(ctx, callback) + return + let dims = response.headers.table["X-Image-Dimensions"][0] + let width = parseUInt64(dims.until('x'), allowSign = false) + let height = parseUInt64(dims.after('x'), allowSign = false) + if width.isNone or height.isNone: + window.console.error("wrong X-Image-Dimensions") + JS_FreeValue(ctx, callback) + return + response.blob().then(proc(blob: JSResult[Blob]) = + let jsBlob = toJS(ctx, blob) + let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, jsBlob.toJSValueArray()) + if JS_IsException(res): + window.console.error("Exception in canvas toBlob:", + ctx.getExceptionMsg()) + else: + JS_FreeValue(ctx, res) + JS_FreeValue(ctx, callback) + ) + ) return JS_UNDEFINED # Forward declaration hack diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim index 9f9b3401..a186b47f 100644 --- a/src/img/bitmap.nim +++ b/src/img/bitmap.nim @@ -8,7 +8,11 @@ type ImageBitmap* = ref object of Bitmap -proc newBitmap*(width, height: uint64): Bitmap = + NetworkBitmap* = ref object of Bitmap + outputId*: int + imageId*: int + +proc newBitmap*(width, height: uint64): ImageBitmap = return ImageBitmap( px: newSeq[ARGBColor](width * height), width: width, diff --git a/src/img/painter.nim b/src/img/painter.nim index a9848d7d..d02f3602 100644 --- a/src/img/painter.nim +++ b/src/img/painter.nim @@ -4,7 +4,6 @@ import std/unicode import css/cssvalues import img/bitmap import img/path -import img/png import types/color import types/line import types/vector @@ -139,15 +138,12 @@ proc clearRect*(bmp: Bitmap; x0, x1, y0, y1: uint64) = proc clear*(bmp: Bitmap) = bmp.clearRect(0, bmp.width, 0, bmp.height) -const unifont = readFile"res/unifont_jp-15.0.05.png" -var unifontBitmap: Bitmap +var unifontBitmap*: Bitmap = nil var glyphCache: seq[tuple[u: uint32, bmp: Bitmap]] var glyphCacheI = 0 proc getCharBmp(u: uint32): Bitmap = # We only have the BMP. let u = if u <= 0xFFFF: u else: 0xFFFD - if unifontBitmap == nil: - unifontBitmap = fromPNG(toOpenArrayByte(unifont, 0, unifont.high)) for (cu, bmp) in glyphCache: if cu == u: return bmp diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim index 90940424..bd2486be 100644 --- a/src/io/bufreader.nim +++ b/src/io/bufreader.nim @@ -2,8 +2,10 @@ import std/options import std/sets import std/tables +import img/bitmap import io/dynstream import io/socketstream +import loader/request import types/blob import types/color import types/formdata @@ -32,6 +34,8 @@ proc sread*(reader: var BufferedReader; blob: var Blob) proc sread*[T](reader: var BufferedReader; o: var Option[T]) proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E]) proc sread*(reader: var BufferedReader; c: var ARGBColor) {.inline.} +proc sread*(reader: var BufferedReader; o: var RequestBody) +proc sread*(reader: var BufferedReader; bmp: var Bitmap) proc initReader*(stream: DynStream; len, auxLen: int): BufferedReader = assert len != 0 @@ -169,7 +173,7 @@ proc sread*(reader: var BufferedReader; blob: var Blob) = let buffer = alloc(blob.size) reader.readData(blob.buffer, int(blob.size)) blob.buffer = buffer - blob.deallocFun = proc() = dealloc(buffer) + blob.deallocFun = deallocBlob proc sread*[T](reader: var BufferedReader; o: var Option[T]) = var x: bool @@ -201,3 +205,38 @@ proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E]) = proc sread*(reader: var BufferedReader; c: var ARGBColor) = reader.sread(uint32(c)) + +proc sread*(reader: var BufferedReader; o: var RequestBody) = + var t: RequestBodyType + reader.sread(t) + o = RequestBody(t: t) + case t + of rbtNone: discard + of rbtString: reader.sread(o.s) + of rbtMultipart: reader.sread(o.multipart) + of rbtOutput: reader.sread(o.outputId) + +proc sread*(reader: var BufferedReader; bmp: var Bitmap) = + var isImageBitmap: bool + var width: uint64 + var height: uint64 + reader.sread(isImageBitmap) + reader.sread(width) + reader.sread(height) + if isImageBitmap: + bmp = ImageBitmap( + width: width, + height: height + ) + reader.sread(bmp.px) + else: + var outputId: int + var imageId: int + reader.sread(outputId) + reader.sread(imageId) + bmp = NetworkBitmap( + width: width, + height: height, + outputId: outputId, + imageId: imageId + ) diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim index fd3c12a8..56e30f5b 100644 --- a/src/io/bufwriter.nim +++ b/src/io/bufwriter.nim @@ -5,8 +5,10 @@ import std/options import std/sets import std/tables +import img/bitmap import io/dynstream import io/socketstream +import loader/request import types/blob import types/color import types/formdata @@ -34,7 +36,7 @@ proc swrite*(writer: var BufferedWriter; b: bool) proc swrite*(writer: var BufferedWriter; url: URL) proc swrite*(writer: var BufferedWriter; tup: tuple) proc swrite*[I, T](writer: var BufferedWriter; a: array[I, T]) -proc swrite*(writer: var BufferedWriter; s: seq) +proc swrite*[T](writer: var BufferedWriter; s: openArray[T]) proc swrite*[U, V](writer: var BufferedWriter; t: Table[U, V]) proc swrite*(writer: var BufferedWriter; obj: object) proc swrite*(writer: var BufferedWriter; obj: ref object) @@ -43,6 +45,8 @@ proc swrite*(writer: var BufferedWriter; blob: Blob) proc swrite*[T](writer: var BufferedWriter; o: Option[T]) proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E]) proc swrite*(writer: var BufferedWriter; c: ARGBColor) {.inline.} +proc swrite*(writer: var BufferedWriter; o: RequestBody) +proc swrite*(writer: var BufferedWriter; bmp: Bitmap) const InitLen = sizeof(int) * 2 const SizeInit = max(64, InitLen) @@ -130,7 +134,7 @@ proc swrite*[I, T](writer: var BufferedWriter; a: array[I, T]) = for x in a: writer.swrite(x) -proc swrite*(writer: var BufferedWriter; s: seq) = +proc swrite*[T](writer: var BufferedWriter; s: openArray[T]) = writer.swrite(s.len) for x in s: writer.swrite(x) @@ -188,3 +192,21 @@ proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E]) = proc swrite*(writer: var BufferedWriter; c: ARGBColor) = writer.swrite(uint32(c)) + +proc swrite*(writer: var BufferedWriter; o: RequestBody) = + writer.swrite(o.t) + case o.t + of rbtNone: discard + of rbtString: writer.swrite(o.s) + of rbtMultipart: writer.swrite(o.multipart) + of rbtOutput: writer.swrite(o.outputId) + +proc swrite*(writer: var BufferedWriter; bmp: Bitmap) = + writer.swrite(bmp of ImageBitmap) + writer.swrite(bmp.width) + writer.swrite(bmp.height) + if bmp of ImageBitmap: + writer.swrite(bmp.px) + else: + writer.swrite(NetworkBitmap(bmp).outputId) + writer.swrite(NetworkBitmap(bmp).imageId) diff --git a/src/io/promise.nim b/src/io/promise.nim index 183091cb..a1a5cfc9 100644 --- a/src/io/promise.nim +++ b/src/io/promise.nim @@ -132,6 +132,14 @@ proc then*[T](promise: Promise[T]; cb: (proc(x: T): EmptyPromise)): EmptyPromise next.resolve()) return next +proc then*[T](promise: EmptyPromise; cb: (proc(): T)): Promise[T] + {.discardable.} = + let next = Promise[T]() + promise.then(proc() = + next.res = cb() + next.resolve()) + return next + proc then*[T, U](promise: Promise[T]; cb: (proc(x: T): U)): Promise[U] {.discardable.} = let next = Promise[U]() @@ -195,7 +203,7 @@ proc promiseThenCallback(ctx: JSContext; this_val: JSValue; argc: cint; proc fromJSEmptyPromise*(ctx: JSContext; val: JSValue): JSResult[EmptyPromise] = if not JS_IsObject(val): - return err(newTypeError("Value is not an object")) + return errTypeError("Value is not an object") var p = EmptyPromise() GC_ref(p) let tmp = JS_NewObject(ctx) diff --git a/src/io/urlfilter.nim b/src/io/urlfilter.nim index e86e0e6b..20983498 100644 --- a/src/io/urlfilter.nim +++ b/src/io/urlfilter.nim @@ -6,7 +6,7 @@ import types/url #TODO add denyhost/s for blocklists type URLFilter* = object scheme: Option[string] - allowschemes: seq[string] + allowschemes*: seq[string] allowhost*: Option[string] allowhosts: seq[Regex] default: bool diff --git a/src/js/console.nim b/src/js/console.nim index 0de66162..2ea0fa91 100644 --- a/src/js/console.nim +++ b/src/js/console.nim @@ -35,7 +35,7 @@ proc clear(console: Console) {.jsfunc.} = proc debug(console: Console; ss: varargs[string]) {.jsfunc.} = console.log(ss) -proc error(console: Console; ss: varargs[string]) {.jsfunc.} = +proc error*(console: Console; ss: varargs[string]) {.jsfunc.} = console.log(ss) proc info(console: Console; ss: varargs[string]) {.jsfunc.} = diff --git a/src/js/jscolor.nim b/src/js/jscolor.nim index aa6fd8ed..8491e778 100644 --- a/src/js/jscolor.nim +++ b/src/js/jscolor.nim @@ -12,10 +12,10 @@ import utils/twtstr func parseLegacyColor*(s: string): JSResult[RGBColor] = if s == "": - return err(newTypeError("Color value must not be the empty string")) + return errTypeError("Color value must not be the empty string") let s = s.strip(chars = AsciiWhitespace).toLowerAscii() if s == "transparent": - return err(newTypeError("Color must not be transparent")) + return errTypeError("Color must not be transparent") return ok(parseLegacyColor0(s)) proc toJS*(ctx: JSContext; rgb: RGBColor): JSValue = diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim index f3c5b1e3..ee3d3160 100644 --- a/src/loader/cgi.nim +++ b/src/loader/cgi.nim @@ -42,8 +42,8 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI, myDir: string; if url.query.isSome: putEnv("QUERY_STRING", url.query.get) if request.httpMethod == hmPost: - if request.multipart.isSome: - putEnv("CONTENT_TYPE", request.multipart.get.getContentType()) + if request.body.t == rbtMultipart: + putEnv("CONTENT_TYPE", request.body.multipart.getContentType()) else: putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", "")) putEnv("CONTENT_LENGTH", $contentLen) @@ -126,7 +126,7 @@ proc handleLine(handle: LoaderHandle; line: string; headers: Headers) = headers.add(k, v) proc loadCGI*(handle: LoaderHandle; request: Request; cgiDir: seq[string]; - prevURL: URL; insecureSSLNoVerify: bool) = + prevURL: URL; insecureSSLNoVerify: bool; ostream: var PosixStream) = if cgiDir.len == 0: handle.sendResult(ERROR_NO_CGI_DIR) return @@ -181,16 +181,11 @@ proc loadCGI*(handle: LoaderHandle; request: Request; cgiDir: seq[string]; return # Pipe the request body as stdin for POST. var pipefd_read: array[0..1, cint] # parent -> child - let needsPipe = request.body.isSome or request.multipart.isSome - if needsPipe: + if request.body.t != rbtNone: if pipe(pipefd_read) == -1: handle.sendResult(ERROR_FAIL_SETUP_CGI) return - var contentLen = 0 - if request.body.isSome: - contentLen = request.body.get.len - elif request.multipart.isSome: - contentLen = request.multipart.get.calcLength() + let contentLen = request.body.contentLength() stdout.flushFile() stderr.flushFile() let pid = fork() @@ -199,7 +194,7 @@ proc loadCGI*(handle: LoaderHandle; request: Request; cgiDir: seq[string]; elif pid == 0: discard close(pipefd[0]) # close read discard dup2(pipefd[1], 1) # dup stdout - if needsPipe: + if request.body.t != rbtNone: discard close(pipefd_read[1]) # close write if pipefd_read[0] != 0: discard dup2(pipefd_read[0], 0) # dup stdin @@ -220,38 +215,32 @@ proc loadCGI*(handle: LoaderHandle; request: Request; cgiDir: seq[string]; quit(1) else: discard close(pipefd[1]) # close write - if needsPipe: + if request.body.t != rbtNone: discard close(pipefd_read[0]) # close read let ps = newPosixStream(pipefd_read[1]) - if request.body.isSome: - ps.write(request.body.get) - elif request.multipart.isSome: - let multipart = request.multipart.get - for entry in multipart.entries: - ps.writeEntry(entry, multipart.boundary) - ps.writeEnd(multipart.boundary) - ps.sclose() + case request.body.t + of rbtString: + ps.write(request.body.s) + ps.sclose() + of rbtMultipart: + let boundary = request.body.multipart.boundary + for entry in request.body.multipart.entries: + ps.writeEntry(entry, boundary) + ps.writeEnd(boundary) + ps.sclose() + of rbtOutput: + ostream = ps + of rbtNone: discard handle.parser = HeaderParser(headers: newHeaders()) handle.istream = newPosixStream(pipefd[0]) -proc killHandle(handle: LoaderHandle) = - if handle.parser.state != hpsBeforeLines: - # not an ideal solution, but better than silently eating malformed - # headers - handle.output.ostream.setBlocking(true) - handle.sendStatus(500) - handle.sendHeaders(newHeaders()) - const msg = "Error: malformed header in CGI script" - discard handle.output.ostream.sendData(msg) - handle.parser = nil - proc parseHeaders0(handle: LoaderHandle; buffer: LoaderBuffer): int = let parser = handle.parser var s = parser.lineBuffer let L = if buffer == nil: 1 else: buffer.len for i in 0 ..< L: template die = - handle.killHandle() + handle.parser = nil return -1 let c = if buffer != nil: char(buffer.page[i]) diff --git a/src/loader/loader.nim b/src/loader/loader.nim index c84f247a..89d97cde 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -8,11 +8,17 @@ # S: output ID # S: status code # S: headers +# C: resume # S: response body # else: # S: error message # # The body is passed to the stream as-is, so effectively nothing can follow it. +# +# Note: if the consumer closes the request's body after headers have been +# passed, it will *not* be cleaned up until a `resume' command is +# received. (This allows for passing outputIds to the pager for later +# addCacheFile commands there.) import std/deques import std/nativesockets @@ -57,7 +63,7 @@ type process*: int clientPid*: int connecting*: Table[int, ConnectData] - ongoing*: Table[int, OngoingData] + ongoing*: Table[int, Response] unregistered*: seq[int] registerFun*: proc(fd: int) unregisterFun*: proc(fd: int) @@ -71,11 +77,6 @@ type stream*: SocketStream request: Request - OngoingData* = object - buf: string - response*: Response - bodyRead: Promise[string] - LoaderCommand = enum lcAddCacheFile lcAddClient @@ -155,10 +156,12 @@ proc rejectHandle(handle: LoaderHandle; code: ConnectErrorCode; msg = "") = handle.sendResult(code, msg) handle.close() -func findOutput(ctx: LoaderContext; id: int): OutputHandle = +func findOutput(ctx: LoaderContext; id: int; client: ClientData): OutputHandle = assert id != -1 for it in ctx.outputMap.values: if it.outputId == id: + # verify that it's safe to access this handle. + doAssert ctx.isPrivileged(client) or client.pid == it.ownerPid return it return nil @@ -211,11 +214,8 @@ proc getOutputId(ctx: LoaderContext): int = result = ctx.outputNum inc ctx.outputNum -proc redirectToFile(ctx: LoaderContext; output: OutputHandle; - targetPath: string): bool = - let ps = newPosixStream(targetPath, O_CREAT or O_WRONLY, 0o600) - if ps == nil: - return false +proc redirectToStream(ctx: LoaderContext; output: OutputHandle; + ps: PosixStream): bool = if output.currentBuffer != nil: let n = ps.sendData(output.currentBuffer, output.currentBufferIdx) if unlikely(n < output.currentBuffer.len - output.currentBufferIdx): @@ -226,7 +226,9 @@ proc redirectToFile(ctx: LoaderContext; output: OutputHandle; if unlikely(n < buffer.len): ps.sclose() return false - if output.parent != nil: + if output.istreamAtEnd: + ps.sclose() + elif output.parent != nil: output.parent.outputs.add(OutputHandle( parent: output.parent, ostream: ps, @@ -235,6 +237,13 @@ proc redirectToFile(ctx: LoaderContext; output: OutputHandle; )) return true +proc redirectToFile(ctx: LoaderContext; output: OutputHandle; + targetPath: string): bool = + let ps = newPosixStream(targetPath, O_CREAT or O_WRONLY, 0o600) + if ps == nil: + return false + return ctx.redirectToStream(output, ps) + type AddCacheFileResult = tuple[outputId: int; cacheFile: string] proc addCacheFile(ctx: LoaderContext; client: ClientData; output: OutputHandle): @@ -335,8 +344,7 @@ proc loadStreamRegular(ctx: LoaderContext; handle, cachedHandle: LoaderHandle) = output.ostream.sclose() output.ostream = nil handle.outputs.setLen(0) - handle.istream.sclose() - handle.istream = nil + handle.iclose() proc loadStream(ctx: LoaderContext; handle: LoaderHandle; request: Request) = ctx.passedFdMap.withValue(request.url.pathname, fdp): @@ -406,10 +414,17 @@ proc loadResource(ctx: LoaderContext; client: ClientData; config: LoaderClientCo redo = true continue if request.url.scheme == "cgi-bin": + var ostream: PosixStream = nil handle.loadCGI(request, ctx.config.cgiDir, prevurl, - config.insecureSSLNoVerify) + config.insecureSSLNoVerify, ostream) if handle.istream != nil: ctx.addFd(handle) + if ostream != nil: + let output = ctx.findOutput(request.body.outputId, client) + if output != nil: + doAssert ctx.redirectToStream(output, ostream) + else: + ostream.sclose() else: handle.close() elif request.url.scheme == "stream": @@ -451,8 +466,7 @@ proc setupRequestDefaults(request: Request; config: LoaderClientConfig) = proc load(ctx: LoaderContext; stream: SocketStream; request: Request; client: ClientData; config: LoaderClientConfig) = - let handle = newLoaderHandle(stream, ctx.getOutputId(), client.pid, - request.suspended) + let handle = newLoaderHandle(stream, ctx.getOutputId(), client.pid) when defined(debug): handle.url = request.url handle.output.url = request.url @@ -514,9 +528,12 @@ proc addCacheFile(ctx: LoaderContext; stream: SocketStream; r: var BufferedReader) = var outputId: int var targetPid: int + var sourcePid: int r.sread(outputId) r.sread(targetPid) - let output = ctx.findOutput(outputId) + r.sread(sourcePid) + let sourceClient = ctx.clientData[sourcePid] + let output = ctx.findOutput(outputId, sourceClient) assert output != nil let targetClient = ctx.clientData[targetPid] let (id, file) = ctx.addCacheFile(targetClient, output) @@ -531,7 +548,7 @@ proc redirectToFile(ctx: LoaderContext; stream: SocketStream; var targetPath: string r.sread(outputId) r.sread(targetPath) - let output = ctx.findOutput(outputId) + let output = ctx.findOutput(outputId, ctx.pagerClient) var success = false if output != nil: success = ctx.redirectToFile(output, targetPath) @@ -583,9 +600,7 @@ proc tee(ctx: LoaderContext; stream: SocketStream; client: ClientData; var targetPid: int r.sread(sourceId) r.sread(targetPid) - let output = ctx.findOutput(sourceId) - # only allow tee'ing outputs owned by client - doAssert output.ownerPid == client.pid + let output = ctx.findOutput(sourceId, client) if output != nil: let id = ctx.getOutputId() output.tee(stream, id, targetPid) @@ -602,7 +617,7 @@ proc suspend(ctx: LoaderContext; stream: SocketStream; client: ClientData; var ids: seq[int] r.sread(ids) for id in ids: - let output = ctx.findOutput(id) + let output = ctx.findOutput(id, client) if output != nil: output.suspended = true if output.registered: @@ -615,7 +630,7 @@ proc resume(ctx: LoaderContext; stream: SocketStream; client: ClientData; var ids: seq[int] r.sread(ids) for id in ids: - let output = ctx.findOutput(id) + let output = ctx.findOutput(id, client) if output != nil: output.suspended = false assert not output.registered @@ -793,10 +808,9 @@ proc finishCycle(ctx: LoaderContext; unregRead: var seq[LoaderHandle]; if handle.istream != nil: ctx.selector.unregister(handle.istream.fd) ctx.handleMap.del(handle.istream.fd) - handle.istream.sclose() - handle.istream = nil if handle.parser != nil: handle.finishParse() + handle.iclose() for output in handle.outputs: output.istreamAtEnd = true if output.isEmpty: @@ -816,10 +830,9 @@ proc finishCycle(ctx: LoaderContext; unregRead: var seq[LoaderHandle]; # premature end of all output streams; kill istream too ctx.selector.unregister(handle.istream.fd) ctx.handleMap.del(handle.istream.fd) - handle.istream.sclose() - handle.istream = nil if handle.parser != nil: handle.finishParse() + handle.iclose() proc runFileLoader*(fd: cint; config: LoaderConfig) = var ctx = initLoaderContext(fd, config) @@ -861,12 +874,7 @@ proc getRedirect*(response: Response; request: Request): Request = status == 302 and request.httpMethod == hmPost: return newRequest(url.get, hmGet) else: - return newRequest( - url.get, - request.httpMethod, - body = request.body, - multipart = request.multipart - ) + return newRequest(url.get, request.httpMethod, body = request.body) return nil template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader; @@ -898,7 +906,6 @@ proc startRequest*(loader: FileLoader; request: Request; w.swrite(config) return stream -#TODO: add init proc fetch*(loader: FileLoader; input: Request): FetchPromise = let stream = loader.startRequest(input) let fd = int(stream.fd) @@ -913,10 +920,7 @@ proc fetch*(loader: FileLoader; input: Request): FetchPromise = proc reconnect*(loader: FileLoader; data: ConnectData) = data.stream.sclose() - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcLoad) - w.swrite(data.request) + let stream = loader.startRequest(data.request) let fd = int(stream.fd) loader.registerFun(fd) loader.connecting[fd] = ConnectData( @@ -925,18 +929,6 @@ proc reconnect*(loader: FileLoader; data: ConnectData) = stream: stream ) -proc switchStream*(data: var ConnectData; stream: SocketStream) = - data.stream = stream - -proc switchStream*(loader: FileLoader; data: var OngoingData; - stream: SocketStream) = - data.response.body = stream - let fd = int(stream.fd) - data.response.unregisterFun = proc() = - loader.ongoing.del(fd) - loader.unregistered.add(fd) - loader.unregisterFun(fd) - proc suspend*(loader: FileLoader; fds: seq[int]) = let stream = loader.connect() stream.withLoaderPacketWriter loader, w: @@ -944,13 +936,16 @@ proc suspend*(loader: FileLoader; fds: seq[int]) = w.swrite(fds) stream.sclose() -proc resume*(loader: FileLoader; fds: seq[int]) = +proc resume*(loader: FileLoader; fds: openArray[int]) = let stream = loader.connect() stream.withLoaderPacketWriter loader, w: w.swrite(lcResume) w.swrite(fds) stream.sclose() +proc resume*(loader: FileLoader; fds: int) = + loader.resume([fds]) + proc tee*(loader: FileLoader; sourceId, targetPid: int): (SocketStream, int) = let stream = loader.connect() stream.withLoaderPacketWriter loader, w: @@ -962,15 +957,20 @@ proc tee*(loader: FileLoader; sourceId, targetPid: int): (SocketStream, int) = r.sread(outputId) return (stream, outputId) -proc addCacheFile*(loader: FileLoader; outputId, targetPid: int): - AddCacheFileResult = +# sourcePid is the PID of the output's owner. This is used in pager for images, +# so that we can be sure that a container only loads images on the page that +# it owns. +proc addCacheFile*(loader: FileLoader; outputId, targetPid: int; + sourcePid = -1): AddCacheFileResult = let stream = loader.connect() if stream == nil: return (-1, "") + let sourcePid = if sourcePid == -1: loader.clientPid else: sourcePid stream.withLoaderPacketWriter loader, w: w.swrite(lcAddCacheFile) w.swrite(outputId) w.swrite(targetPid) + w.swrite(sourcePid) var r = stream.initPacketReader() var outputId: int var cacheFile: string @@ -990,18 +990,18 @@ proc redirectToFile*(loader: FileLoader; outputId: int; targetPath: string): var r = stream.initPacketReader() r.sread(result) -const BufferSize = 4096 - proc onConnected*(loader: FileLoader; fd: int) = let connectData = loader.connecting[fd] let stream = connectData.stream let promise = connectData.promise let request = connectData.request + # delete before resolving the promise + loader.connecting.del(fd) var r = stream.initPacketReader() var res: int r.sread(res) # packet 1 - let response = newResponse(res, request, stream) if res == 0: + let response = newResponse(res, request, stream) r.sread(response.outputId) # packet 1 r = stream.initPacketReader() r.sread(response.status) # packet 2 @@ -1011,13 +1011,12 @@ proc onConnected*(loader: FileLoader; fd: int) = response.body = stream assert loader.unregisterFun != nil response.unregisterFun = proc() = - loader.ongoing.del(fd) - loader.unregistered.add(fd) - loader.unregisterFun(fd) - loader.ongoing[fd] = OngoingData( - response: response, - bodyRead: response.bodyRead - ) + loader.ongoing.del(response.body.fd) + loader.unregistered.add(response.body.fd) + loader.unregisterFun(response.body.fd) + response.resumeFun = proc(outputId: int) = + loader.resume(outputId) + loader.ongoing[fd] = response stream.setBlocking(false) promise.resolve(JSResult[Response].ok(response)) else: @@ -1030,40 +1029,27 @@ proc onConnected*(loader: FileLoader; fd: int) = stream.sclose() let err = newTypeError("NetworkError when attempting to fetch resource") promise.resolve(JSResult[Response].err(err)) - loader.connecting.del(fd) proc onRead*(loader: FileLoader; fd: int) = - loader.ongoing.withValue(fd, buffer): - let response = buffer[].response - while not response.body.isend: - let olen = buffer[].buf.len - try: - buffer[].buf.setLen(olen + BufferSize) - let n = response.body.recvData(addr buffer[].buf[olen], BufferSize) - buffer[].buf.setLen(olen + n) - if n == 0: - break - except ErrorAgain: - buffer[].buf.setLen(olen) - break + let response = loader.ongoing.getOrDefault(fd) + if response != nil: + response.onRead(response) if response.body.isend: - buffer[].bodyRead.resolve(buffer[].buf) - buffer[].bodyRead = nil - buffer[].buf = "" + response.bodyRead.resolve() + response.bodyRead = nil response.unregisterFun() proc onError*(loader: FileLoader; fd: int) = - loader.ongoing.withValue(fd, buffer): - let response = buffer[].response + let response = loader.ongoing.getOrDefault(fd) + if response != nil: when defined(debug): var lbuf {.noinit.}: array[BufferSize, char] if not response.body.isend: let n = response.body.recvData(addr lbuf[0], lbuf.len) assert n == 0 assert response.body.isend - buffer[].bodyRead.resolve(buffer[].buf) - buffer[].bodyRead = nil - buffer[].buf = "" + response.bodyRead.resolve() + response.bodyRead = nil response.unregisterFun() # Note: this blocks until headers are received. diff --git a/src/loader/loaderhandle.nim b/src/loader/loaderhandle.nim index 00f6f754..31a41571 100644 --- a/src/loader/loaderhandle.nim +++ b/src/loader/loaderhandle.nim @@ -3,6 +3,7 @@ import std/net import std/tables import io/bufwriter +import io/dynstream import io/posixstream import loader/headers @@ -44,14 +45,15 @@ type status*: uint16 ResponseState = enum - rsBeforeResult, rsBeforeStatus, rsBeforeHeaders, rsAfterHeaders + rsBeforeResult, rsAfterFailure, rsBeforeStatus, rsBeforeHeaders, + rsAfterHeaders LoaderHandle* = ref object istream*: PosixStream # stream for taking input outputs*: seq[OutputHandle] # list of outputs to be streamed into cacheId*: int # if cached, our ID in a client cacheMap parser*: HeaderParser # only exists for CGI handles - rstate: ResponseState # just an enum for sanity checks + rstate: ResponseState # track response state when defined(debug): url*: URL @@ -69,15 +71,14 @@ when defined(debug): return s # Create a new loader handle, with the output stream ostream. -proc newLoaderHandle*(ostream: PosixStream; outputId, pid: int; - suspended: bool): LoaderHandle = +proc newLoaderHandle*(ostream: PosixStream; outputId, pid: int): LoaderHandle = let handle = LoaderHandle(cacheId: -1) handle.outputs.add(OutputHandle( ostream: ostream, parent: handle, outputId: outputId, ownerPid: pid, - suspended: suspended + suspended: true )) return handle @@ -108,15 +109,17 @@ proc bufferCleared*(output: OutputHandle) = output.currentBuffer = nil proc tee*(outputIn: OutputHandle; ostream: PosixStream; outputId, pid: int) = - outputIn.parent.outputs.add(OutputHandle( - parent: outputIn.parent, + let parent = outputIn.parent + parent.outputs.add(OutputHandle( + parent: parent, ostream: ostream, currentBuffer: outputIn.currentBuffer, currentBufferIdx: outputIn.currentBufferIdx, buffers: outputIn.buffers, istreamAtEnd: outputIn.istreamAtEnd, outputId: outputId, - ownerPid: pid + ownerPid: pid, + suspended: outputIn.suspended )) template output*(handle: LoaderHandle): OutputHandle = @@ -133,6 +136,7 @@ proc sendResult*(handle: LoaderHandle; res: int; msg = "") = if res == 0: # success assert msg == "" w.swrite(output.outputId) + inc handle.rstate else: # error w.swrite(msg) output.ostream.setBlocking(blocking) @@ -164,12 +168,27 @@ proc sendData*(ps: PosixStream; buffer: LoaderBuffer; si = 0): int {.inline.} = assert buffer.len - si > 0 return ps.sendData(addr buffer.page[si], buffer.len - si) +proc iclose*(handle: LoaderHandle) = + if handle.istream != nil: + if handle.rstate notin {rsBeforeResult, rsAfterFailure, rsAfterHeaders}: + assert handle.outputs.len == 1 + # not an ideal solution, but better than silently eating malformed + # headers + try: + handle.sendStatus(500) + handle.sendHeaders(newHeaders()) + handle.output.ostream.setBlocking(true) + const msg = "Error: malformed header in CGI script" + discard handle.output.ostream.sendData(msg) + except ErrorBrokenPipe: + discard # receiver is dead + handle.istream.sclose() + handle.istream = nil + proc close*(handle: LoaderHandle) = + handle.iclose() for output in handle.outputs: #TODO assert not output.registered if output.ostream != nil: output.ostream.sclose() output.ostream = nil - if handle.istream != nil: - handle.istream.sclose() - handle.istream = nil diff --git a/src/loader/request.nim b/src/loader/request.nim index 277481f1..f92098cb 100644 --- a/src/loader/request.nim +++ b/src/loader/request.nim @@ -58,17 +58,27 @@ type of rwtWindow: window*: EnvironmentSettings + RequestBodyType* = enum + rbtNone, rbtString, rbtMultipart, rbtOutput + + RequestBody* = object + case t*: RequestBodyType + of rbtNone: + discard + of rbtString: + s*: string + of rbtMultipart: + multipart*: FormData + of rbtOutput: + outputId*: int + Request* = ref object httpMethod*: HttpMethod url*: URL headers*: Headers - body*: Option[string] - multipart*: Option[FormData] + body*: RequestBody referrer*: URL proxy*: URL #TODO do something with this - # when set to true, the loader will not write data from the body (not - # headers!) into the output until a resume is received. - suspended*: bool JSRequest* = ref object request*: Request @@ -81,6 +91,13 @@ type jsDestructor(JSRequest) +proc contentLength*(body: RequestBody): int = + case body.t + of rbtNone: return 0 + of rbtString: return body.s.len + of rbtMultipart: return body.multipart.calcLength() + of rbtOutput: return 0 + func headers(this: JSRequest): Headers {.jsfget.} = return this.request.headers @@ -102,17 +119,14 @@ iterator pairs*(headers: Headers): (string, string) = yield (k, v) func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders(); - body = none(string); multipart = none(FormData); proxy: URL = nil; - referrer: URL = nil; suspended = false): Request = + body = RequestBody(); proxy: URL = nil; referrer: URL = nil): Request = return Request( url: url, httpMethod: httpMethod, headers: headers, body: body, - multipart: multipart, referrer: referrer, - proxy: proxy, - suspended: suspended + proxy: proxy ) func createPotentialCORSRequest*(url: URL; destination: RequestDestination; @@ -178,7 +192,7 @@ proc fromJSBodyInit(ctx: JSContext; val: JSValue): JSResult[BodyInit] = let x = fromJS[string](ctx, val) if x.isSome: return ok(BodyInit(t: bitString, str: x.get)) - return err(newTypeError("Invalid body init type")) + return errTypeError("Invalid body init type") func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; init = none(RequestInit)): JSResult[JSRequest] {.jsctor.} = @@ -188,13 +202,12 @@ func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; when T is string: let url = ?newURL(resource) if url.username != "" or url.password != "": - return err(newTypeError("Input URL contains a username or password")) + return errTypeError("Input URL contains a username or password") var httpMethod = hmGet var headers = newHeaders() let referrer: URL = nil var credentials = cmSameOrigin - var body: Option[string] - var multipart: Option[FormData] + var body = RequestBody() var proxyUrl: URL #TODO? let fallbackMode = opt(rmCors) var window = RequestWindow(t: rwtClient) @@ -205,7 +218,6 @@ func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; let referrer = resource.request.referrer var credentials = resource.credentialsMode var body = resource.request.body - var multipart = resource.request.multipart var proxyUrl = resource.request.proxy #TODO? let fallbackMode = none(RequestMode) var window = resource.window @@ -226,8 +238,10 @@ func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; if init.body.isSome: let ibody = init.body.get case ibody.t - of bitFormData: multipart = some(ibody.formData) - of bitString: body = some(ibody.str) + of bitFormData: + body = RequestBody(t: rbtMultipart, multipart: ibody.formData) + of bitString: + body = RequestBody(t: rbtString, s: ibody.str) else: discard #TODO if httpMethod in {hmGet, hmHead}: return errTypeError("HEAD or GET Request cannot have a body.") @@ -245,7 +259,6 @@ func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; httpMethod, headers, body, - multipart, proxy = proxyUrl, referrer = referrer ), diff --git a/src/loader/response.nim b/src/loader/response.nim index 8ea17e64..3834d5a9 100644 --- a/src/loader/response.nim +++ b/src/loader/response.nim @@ -3,6 +3,8 @@ import std/tables import chagashi/charset import chagashi/decoder +import img/bitmap +import io/posixstream import io/promise import io/socketstream import loader/headers @@ -11,6 +13,7 @@ import monoucha/javascript import monoucha/jserror import monoucha/quickjs import types/blob +import types/color import types/opt import types/url import utils/mimeguess @@ -43,9 +46,12 @@ type headersGuard: HeadersGuard url*: URL #TODO should be urllist? unregisterFun*: proc() - bodyRead*: Promise[string] + resumeFun*: proc(outputId: int) + bodyRead*: EmptyPromise internalMessage*: string # should NOT be exposed to JS! outputId*: int + onRead*: proc(response: Response) {.nimcall.} + opaque*: RootRef jsDestructor(Response) @@ -54,7 +60,7 @@ proc newResponse*(res: int; request: Request; stream: SocketStream): Response = res: res, url: request.url, body: stream, - bodyRead: Promise[string](), + bodyRead: EmptyPromise(), outputId: -1 ) @@ -66,7 +72,8 @@ func makeNetworkError*(): Response {.jsstfunc: "Response.error".} = responseType: TYPE_ERROR, status: 0, headers: newHeaders(), - headersGuard: hgImmutable + headersGuard: hgImmutable, + bodyUsed: true ) func sok(response: Response): bool {.jsfget: "ok".} = @@ -102,6 +109,25 @@ func getContentType*(this: Response): string = # override buffer mime.types return DefaultGuess.guessContentType(this.url.pathname) +type TextOpaque = ref object of RootObj + buf: string + +const BufferSize = 4096 + +proc onReadText(response: Response) = + let opaque = TextOpaque(response.opaque) + while true: + let olen = opaque.buf.len + try: + opaque.buf.setLen(olen + BufferSize) + let n = response.body.recvData(addr opaque.buf[olen], BufferSize) + opaque.buf.setLen(olen + n) + if n == 0: + break + except ErrorAgain: + opaque.buf.setLen(olen) + break + proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} = if response.body == nil: let p = newPromise[JSResult[string]]() @@ -113,40 +139,96 @@ proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} = .err(newTypeError("Body has already been consumed")) p.resolve(err) return p - let bodyRead = response.bodyRead - response.bodyRead = nil - return bodyRead.then(proc(s: string): JSResult[string] = + let opaque = TextOpaque() + response.opaque = opaque + response.onRead = onReadText + response.bodyUsed = true + response.resumeFun(response.outputId) + response.resumeFun = nil + return response.bodyRead.then(proc(): JSResult[string] = let charset = response.getCharset(CHARSET_UTF_8) - #TODO this is inefficient - # maybe add a JS type that turns a seq[char] into JS strings - ok(s.decodeAll(charset)) + ok(opaque.buf.decodeAll(charset)) ) +type BlobOpaque = ref object of RootObj + p: pointer + len: int + size: int + +proc onReadBlob(response: Response) = + let opaque = BlobOpaque(response.opaque) + while true: + try: + let targetLen = opaque.len + BufferSize + if targetLen > opaque.size: + opaque.size = targetLen + opaque.p = realloc(opaque.p, targetLen) + let p = cast[ptr UncheckedArray[uint8]](opaque.p) + let n = response.body.recvData(addr p[opaque.len], BufferSize) + opaque.len += n + if n == 0: + break + except ErrorAgain: + break + proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} = - if response.bodyRead == nil: + if response.bodyUsed: let p = newPromise[JSResult[Blob]]() let err = JSResult[Blob] .err(newTypeError("Body has already been consumed")) p.resolve(err) return p - let bodyRead = response.bodyRead - response.bodyRead = nil + let opaque = BlobOpaque() + response.opaque = opaque + response.onRead = onReadBlob + response.bodyUsed = true + response.resumeFun(response.outputId) + response.resumeFun = nil let contentType = response.getContentType() - return bodyRead.then(proc(s: string): JSResult[Blob] = - if s.len == 0: + return response.bodyRead.then(proc(): JSResult[Blob] = + let p = realloc(opaque.p, opaque.len) + opaque.p = nil + if p == nil: return ok(newBlob(nil, 0, contentType, nil)) - GC_ref(s) - let deallocFun = proc() = - GC_unref(s) - let blob = newBlob(unsafeAddr s[0], s.len, contentType, deallocFun) - ok(blob)) + ok(newBlob(p, opaque.len, contentType, deallocBlob)) + ) + +type BitmapOpaque = ref object of RootObj + bmp: Bitmap + idx: int + +proc onReadBitmap(response: Response) = + let opaque = BitmapOpaque(response.opaque) + let bmp = opaque.bmp + while true: + try: + let p = cast[ptr UncheckedArray[uint8]](addr bmp.px[0]) + let L = bmp.px.len * 4 - opaque.idx + let n = response.body.recvData(addr p[opaque.idx], L) + opaque.idx += n + if n == 0: + break + except ErrorAgain: + break + +proc saveToBitmap*(response: Response; bmp: Bitmap): EmptyPromise = + assert not response.bodyUsed + let opaque = BitmapOpaque(bmp: bmp, idx: 0) + let size = bmp.width * bmp.height + bmp.px = cast[seq[ARGBColor]](newSeqUninitialized[uint32](size)) + response.opaque = opaque + response.onRead = onReadBitmap + response.bodyUsed = true + response.resumeFun(response.outputId) + response.resumeFun = nil + return response.bodyRead proc json(ctx: JSContext; this: Response): Promise[JSResult[JSValue]] {.jsfunc.} = return this.text().then(proc(s: JSResult[string]): JSResult[JSValue] = let s = ?s - return ok(JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len), - cstring"<input>"))) + return ok(JS_ParseJSON(ctx, cstring(s), csize_t(s.len), cstring"<input>")) + ) proc addResponseModule*(ctx: JSContext) = ctx.registerType(Response) diff --git a/src/local/client.nim b/src/local/client.nim index 8e83892c..ee469652 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -438,10 +438,10 @@ proc acceptBuffers(client: Client) = let pid = container.process if item.fdout == item.fdin: loader.shareCachedItem(container.cacheId, pid) - loader.resume(@[item.istreamOutputId]) + loader.resume(item.istreamOutputId) else: outCacheId = loader.addCacheFile(item.ostreamOutputId, pid).outputId - loader.resume(@[item.istreamOutputId, item.ostreamOutputId]) + loader.resume([item.istreamOutputId, item.ostreamOutputId]) w.swrite(outCacheId) if item.fdin != -1: # pass down fdout diff --git a/src/local/container.nim b/src/local/container.nim index b64606a1..8717610f 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -7,6 +7,7 @@ import std/unicode import config/config import config/mimetypes +import img/bitmap import io/bufstream import io/dynstream import io/promise @@ -157,6 +158,7 @@ type mainConfig*: Config flags*: set[ContainerFlag] images*: seq[PosBitmap] + cachedImages*: seq[NetworkBitmap] luctx: LUContext jsDestructor(Highlight) @@ -1741,6 +1743,12 @@ proc highlightMarks*(container: Container; display: var FixedGrid; hlformat.bgcolor = hlcolor display[y * display.width + x].format = hlformat +func findCachedImage*(container: Container; id: int): NetworkBitmap = + for bmp in container.cachedImages: + if bmp.imageId == id: + return bmp + return nil + proc handleEvent*(container: Container) = container.handleCommand() if container.needslines: diff --git a/src/local/pager.nim b/src/local/pager.nim index 007ad6a9..8760da86 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -11,6 +11,7 @@ import std/unicode import config/chapath import config/config import config/mailcap +import img/bitmap import io/bufreader import io/dynstream import io/posixstream @@ -474,9 +475,34 @@ proc draw*(pager: Pager) = else: pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) pager.term.outputGrid() - if container != nil and pager.redraw: + if container != nil and pager.redraw and pager.term.imageMode != imNone: pager.term.clearImages() for image in container.images: + if image.bmp of NetworkBitmap: + # add cache file to pager, but source it from the container. + let bmp = NetworkBitmap(image.bmp) + let cached = container.findCachedImage(bmp.imageId) + if cached == nil: + let (cacheId, _) = pager.loader.addCacheFile(bmp.outputId, + pager.loader.clientPid, container.process) + let request = newRequest(newURL("cache:" & $cacheId).get) + # capture bmp for the closure + (proc(bmp: Bitmap) = + pager.loader.fetch(request).then(proc(res: JSResult[Response]): + EmptyPromise = + if res.isNone: + return nil + return res.get.saveToBitmap(bmp) + ).then(proc() = + pager.redraw = true + ) + )(bmp) + pager.loader.resume(bmp.outputId) # get rid of dangling output + container.cachedImages.add(bmp) + continue + image.bmp = cached + if uint64(image.bmp.px.len) < image.bmp.width * image.bmp.height: + continue # loading pager.term.outputImage(image.bmp, image.x - container.fromx, image.y - container.fromy, pager.attrs.width, pager.attrs.height - 1) if pager.askpromise != nil: @@ -541,7 +567,6 @@ proc newContainer(pager: Pager; bufferConfig: BufferConfig; redirectDepth = 0; flags = {cfCanReinterpret, cfUserRequested}; contentType = none(string); charsetStack: seq[Charset] = @[]; url = request.url; cacheId = -1; cacheFile = ""): Container = - request.suspended = true let stream = pager.loader.startRequest(request, loaderConfig) pager.loader.registerFun(stream.fd) let container = newContainer( @@ -1021,6 +1046,9 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; loaderConfig.insecureSSLNoVerify = sc.insecure_ssl_no_verify.get if sc.autofocus.isSome: res.autofocus = sc.autofocus.get + if res.images: + loaderConfig.filter.allowschemes + .add(pager.config.external.urimethodmap.imageProtos) return res # Load request in a new buffer. @@ -1202,7 +1230,7 @@ proc updateReadLineISearch(pager: Pager; linemode: LineMode) = proc saveTo(pager: Pager; data: LineDataDownload; path: string) = if pager.loader.redirectToFile(data.outputId, path): pager.alert("Saving file to " & path) - pager.loader.resume(@[data.outputId]) + pager.loader.resume(data.outputId) data.stream.sclose() pager.lineData = nil else: @@ -1544,7 +1572,7 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string; let url = parseURL("stream:" & $pid).get pager.loader.passFd(url.pathname, FileHandle(fdout)) safeClose(fdout) - let response = pager.loader.doRequest(newRequest(url, suspended = true)) + let response = pager.loader.doRequest(newRequest(url)) return CheckMailcapResult( connect: true, fdout: response.body.fd, @@ -1607,7 +1635,7 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream; block needsConnect: if entry.flags * {COPIOUSOUTPUT, HTMLOUTPUT, ANSIOUTPUT} == {}: # No output. Resume here, so that blocking needsterminal filters work. - pager.loader.resume(@[istreamOutputId]) + pager.loader.resume(istreamOutputId) if canpipe: pager.runMailcapWritePipe(stream, needsterminal, cmd) else: @@ -1632,7 +1660,7 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream; let url = parseURL("stream:" & $pid).get pager.loader.passFd(url.pathname, FileHandle(fdout)) safeClose(cint(fdout)) - let response = pager.loader.doRequest(newRequest(url, suspended = true)) + let response = pager.loader.doRequest(newRequest(url)) return CheckMailcapResult( connect: true, fdout: response.body.fd, diff --git a/src/local/term.nim b/src/local/term.nim index 7dd6d951..db479734 100644 --- a/src/local/term.nim +++ b/src/local/term.nim @@ -66,7 +66,7 @@ type attrs*: WindowAttributes colorMode: ColorMode formatMode: FormatMode - imageMode: ImageMode + imageMode*: ImageMode smcup: bool tc: Termcap tname: string @@ -717,8 +717,6 @@ proc outputKittyImage(term: Terminal; x, y, offx, offy, dispw, disph: int; # x, y, maxw, maxh in cells # x, y can be negative, then image starts outside the screen proc outputImage*(term: Terminal; bmp: Bitmap; x, y, maxw, maxh: int) = - if term.imageMode == imNone: - return var xpx = x * term.attrs.ppc var ypx = y * term.attrs.ppl # calculate offset inside image to start from @@ -737,7 +735,7 @@ proc outputImage*(term: Terminal; bmp: Bitmap; x, y, maxw, maxh: int) = let x = max(x, 0) let y = max(y, 0) case term.imageMode - of imNone: discard + of imNone: assert false of imSixel: term.outputSixelImage(x, y, offx, offy, dispw, disph, bmp) of imKitty: term.outputKittyImage(x, y, offx, offy, dispw, disph, bmp) diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 1d172922..53b30aac 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -849,6 +849,7 @@ proc rewind(buffer: Buffer; offset: int; unregister = true): bool = let response = buffer.loader.doRequest(newRequest(url)) if response.body == nil: return false + buffer.loader.resume(response.outputId) if unregister: buffer.selector.unregister(buffer.fd) buffer.loader.unregistered.add(buffer.fd) @@ -921,13 +922,15 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} = return -1 # suspend outputs before tee'ing var ids: seq[int] = @[] - for data in buffer.loader.ongoing.values: - ids.add(data.response.outputId) + for response in buffer.loader.ongoing.values: + if response.onRead != nil: + ids.add(response.outputId) buffer.loader.suspend(ids) # ongoing transfers are now suspended; exhaust all data in the internal buffer # just to be safe. - for fd in buffer.loader.ongoing.keys: - buffer.loader.onRead(fd) + for fd, response in buffer.loader.ongoing: + if response.onRead != nil: + buffer.loader.onRead(fd) let pid = fork() if pid == -1: buffer.estream.write("Failed to clone buffer.\n") @@ -963,29 +966,21 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} = else: buffer.selector = newSelector[int]() #TODO set buffer.window.timeouts.selector - var cfds: seq[int] = @[] - for fd in buffer.loader.connecting.keys: - cfds.add(fd) - for fd in cfds: - # connecting: just reconnect - let data = buffer.loader.connecting[fd] - buffer.loader.connecting.del(fd) - buffer.loader.reconnect(data) - var ongoing: seq[OngoingData] = @[] - for data in buffer.loader.ongoing.values: - ongoing.add(data) - data.response.body.sclose() + var ongoing: seq[Response] = @[] + for response in buffer.loader.ongoing.values: + ongoing.add(response) + response.body.sclose() buffer.loader.ongoing.clear() let myPid = getCurrentProcessId() - for data in ongoing.mitems: + for response in ongoing.mitems: # tee ongoing streams - let (stream, outputId) = buffer.loader.tee(data.response.outputId, myPid) + let (stream, outputId) = buffer.loader.tee(response.outputId, myPid) # if -1, well, this side hasn't exhausted the socket's buffer doAssert outputId != -1 and stream != nil - data.response.outputId = outputId - data.response.body = stream - let fd = data.response.body.fd - buffer.loader.ongoing[fd] = data + response.outputId = outputId + response.body = stream + let fd = response.body.fd + buffer.loader.ongoing[fd] = response buffer.selector.registerHandle(fd, {Read}, 0) if buffer.istream != nil: # We do not own our input stream, so we can't tee it. @@ -1011,6 +1006,16 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} = r.sread(buffer.loader.key) buffer.rfd = buffer.pstream.fd buffer.selector.registerHandle(buffer.rfd, {Read}, 0) + # must reconnect after the new client is set up, or the client pids get + # mixed up. + var cfds: seq[int] = @[] + for fd in buffer.loader.connecting.keys: + cfds.add(fd) + for fd in cfds: + # connecting: just reconnect + let data = buffer.loader.connecting[fd] + buffer.loader.connecting.del(fd) + buffer.loader.reconnect(data) return 0 else: # parent discard close(pipefd[1]) # close write @@ -1227,10 +1232,10 @@ proc cancel*(buffer: Buffer) {.proxy.} = buffer.loader.unregistered.add(fd) data.stream.sclose() buffer.loader.connecting.clear() - for fd, data in buffer.loader.ongoing: + for fd, response in buffer.loader.ongoing: buffer.selector.unregister(fd) buffer.loader.unregistered.add(fd) - data.response.body.sclose() + response.body.sclose() buffer.loader.ongoing.clear() if buffer.istream != nil: buffer.selector.unregister(buffer.fd) @@ -1247,7 +1252,7 @@ proc cancel*(buffer: Buffer) {.proxy.} = buffer.do_reshape() #https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm -proc serializeMultipartFormData(entries: seq[FormDataEntry]): FormData = +proc serializeMultipart(entries: seq[FormDataEntry]): FormData = let formData = newFormData0() for entry in entries: let name = makeCRLF(entry.name) @@ -1298,7 +1303,7 @@ proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod; # mutate action URL let kvlist = entryList.toNameValuePairs() #TODO with charset - parsedAction.query = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) + parsedAction.query = some(serializeFormURLEncoded(kvlist)) return newRequest(parsedAction, httpMethod) return newRequest(parsedAction) # get action URL of frtMailto: @@ -1306,18 +1311,16 @@ proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod; # mailWithHeaders let kvlist = entryList.toNameValuePairs() #TODO with charset - let headers = serializeApplicationXWWWFormUrlEncoded(kvlist, - spaceAsPlus = false) + let headers = serializeFormURLEncoded(kvlist, spaceAsPlus = false) parsedAction.query = some(headers) return newRequest(parsedAction, httpMethod) # mail as body let kvlist = entryList.toNameValuePairs() let body = if enctype == fetTextPlain: - let text = serializePlainTextFormData(kvlist) - percentEncode(text, PathPercentEncodeSet) + percentEncode(serializePlainTextFormData(kvlist), PathPercentEncodeSet) else: #TODO with charset - serializeApplicationXWWWFormUrlEncoded(kvlist) + serializeFormURLEncoded(kvlist) if parsedAction.query.isNone: parsedAction.query = some("") if parsedAction.query.get != "": @@ -1329,26 +1332,24 @@ proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod; # mutate action URL let kvlist = entryList.toNameValuePairs() #TODO with charset - let query = serializeApplicationXWWWFormUrlEncoded(kvlist) + let query = serializeFormURLEncoded(kvlist) parsedAction.query = some(query) return newRequest(parsedAction, httpMethod) # submit as entity body - var body: Option[string] - var multipart: Option[FormData] - case enctype + let body = case enctype of fetUrlencoded: #TODO with charset let kvlist = entryList.toNameValuePairs() - body = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) + RequestBody(t: rbtString, s: serializeFormURLEncoded(kvlist)) of fetMultipart: #TODO with charset - multipart = some(serializeMultipartFormData(entryList)) + RequestBody(t: rbtMultipart, multipart: serializeMultipart(entryList)) of fetTextPlain: #TODO with charset let kvlist = entryList.toNameValuePairs() - body = some(serializePlainTextFormData(kvlist)) + RequestBody(t: rbtString, s: serializePlainTextFormData(kvlist)) let headers = newHeaders({"Content-Type": $enctype}) - return newRequest(parsedAction, httpMethod, headers, body, multipart) + return newRequest(parsedAction, httpMethod, headers, body) # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element): Request = @@ -1698,7 +1699,7 @@ proc getLines*(buffer: Buffer; w: Slice[int]): GetLinesResult {.proxy.} = if buffer.config.images: for image in buffer.images: if image.y <= w.b and - image.y + int(image.bmp.height) div buffer.attrs.ppl >= w.a: + image.y + int(image.bmp.height) >= w.a * buffer.attrs.ppl: result.images.add(image) proc markURL*(buffer: Buffer; schemes: seq[string]) {.proxy.} = @@ -1849,7 +1850,6 @@ proc handleRead(buffer: Buffer; fd: int): bool = buffer.onload() elif fd in buffer.loader.connecting: buffer.loader.onConnected(fd) - buffer.loader.onRead(fd) if buffer.config.scripting: buffer.window.runJSJobs() elif fd in buffer.loader.ongoing: diff --git a/src/types/blob.nim b/src/types/blob.nim index ce5311ea..fb0bd92a 100644 --- a/src/types/blob.nim +++ b/src/types/blob.nim @@ -8,7 +8,7 @@ import monoucha/jstypes import utils/mimeguess type - DeallocFun = proc() {.closure, raises: [].} + DeallocFun = proc(blob: Blob) {.closure, raises: [].} Blob* = ref object of RootObj size* {.jsget.}: uint64 @@ -37,7 +37,7 @@ proc finalize(blob: Blob) {.jsfin.} = if blob.fd.isSome: discard close(blob.fd.get) if blob.deallocFun != nil and blob.buffer != nil: - blob.deallocFun() + blob.deallocFun(blob) blob.buffer = nil proc finalize(file: WebFile) {.jsfin.} = @@ -58,6 +58,11 @@ type FilePropertyBag = object of BlobPropertyBag #TODO lastModified: int64 +proc deallocBlob*(blob: Blob) = + if blob.buffer != nil: + dealloc(blob.buffer) + blob.buffer = nil + proc newWebFile(ctx: JSContext; fileBits: seq[string]; fileName: string; options = FilePropertyBag()): WebFile {.jsctor.} = let file = WebFile( @@ -68,7 +73,7 @@ proc newWebFile(ctx: JSContext; fileBits: seq[string]; fileName: string; len += blobPart.len let buffer = alloc(len) file.buffer = buffer - file.deallocFun = proc() = dealloc(buffer) + file.deallocFun = deallocBlob var buf = cast[ptr UncheckedArray[uint8]](file.buffer) var i = 0 for blobPart in fileBits: diff --git a/src/types/cookie.nim b/src/types/cookie.nim index 2cc5928c..a303b133 100644 --- a/src/types/cookie.nim +++ b/src/types/cookie.nim @@ -207,8 +207,7 @@ proc serialize*(cookiejar: CookieJar; url: URL): string = result &= "=" result &= cookie.value -proc newCookie*(str: string; url: URL = nil): JSResult[Cookie] - {.jsctor.} = +proc newCookie*(str: string; url: URL = nil): JSResult[Cookie] {.jsctor.} = let cookie = Cookie( expires: -1, created: now().toTime().toUnix() @@ -253,7 +252,7 @@ proc newCookie*(str: string; url: URL = nil): JSResult[Cookie] cookie.domain = val hasdomain = true else: - return err(newTypeError("Domains do not match")) + return errTypeError("Domains do not match") if not hasdomain: if url != nil: cookie.domain = url.host diff --git a/src/types/urimethodmap.nim b/src/types/urimethodmap.nim index eed8b112..4cb5b9ae 100644 --- a/src/types/urimethodmap.nim +++ b/src/types/urimethodmap.nim @@ -9,6 +9,7 @@ import utils/twtstr type URIMethodMap* = object map*: Table[string, string] + imageProtos*: seq[string] func rewriteURL(pattern, surl: string): string = result = "" @@ -44,6 +45,10 @@ proc findAndRewrite*(this: URIMethodMap; url: var URL): URIMethodMapResult = return URI_RESULT_SUCCESS return URI_RESULT_NOT_FOUND +proc insert(this: var URIMethodMap; k, v: string) = + if not this.map.hasKeyOrPut(k, v) and k.startsWith("img-codec+"): + this.imageProtos.add(k.until(':')) + proc parseURIMethodMap*(this: var URIMethodMap; s: string) = for line in s.split('\n'): if line.len == 0 or line[0] == '#': @@ -68,7 +73,7 @@ proc parseURIMethodMap*(this: var URIMethodMap; s: string) = v = "cgi-bin:" & v.substr("file:///cgi-bin/".len) elif v.startsWith("/cgi-bin/"): v = "cgi-bin:" & v.substr("/cgi-bin/".len) - discard this.map.hasKeyOrPut(k, v) + this.insert(k, v) proc parseURIMethodMap*(s: string): URIMethodMap = result = URIMethodMap() @@ -76,4 +81,4 @@ proc parseURIMethodMap*(s: string): URIMethodMap = proc append*(this: var URIMethodMap; that: URIMethodMap) = for k, v in that.map: - discard this.map.hasKeyOrPut(k, v) + this.insert(k, v) diff --git a/src/types/url.nim b/src/types/url.nim index 627af4aa..f6e28d10 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -970,7 +970,7 @@ proc newURL*(url: URL): URL = proc setHref(url: URL; s: string): Err[JSError] {.jsfset: "href".} = let purl = basicParseURL(s) if purl.isNone: - return err(newTypeError(s & " is not a valid URL")) + return errTypeError(s & " is not a valid URL") purl.get.cloneInto(url) func isIP*(url: URL): bool = @@ -980,7 +980,7 @@ func isIP*(url: URL): bool = return host.ipv4.isSome or host.ipv6.isSome #https://url.spec.whatwg.org/#concept-urlencoded-serializer -proc parseApplicationXWWWFormUrlEncoded(input: string): seq[(string, string)] = +proc parseFromURLEncoded(input: string): seq[(string, string)] = for s in input.split('&'): if s == "": continue @@ -1002,8 +1002,8 @@ proc parseApplicationXWWWFormUrlEncoded(input: string): seq[(string, string)] = result.add((percentDecode(name), percentDecode(value))) #https://url.spec.whatwg.org/#concept-urlencoded-serializer -proc serializeApplicationXWWWFormUrlEncoded*(kvs: seq[(string, string)]; - spaceAsPlus = true): string = +proc serializeFormURLEncoded*(kvs: seq[(string, string)]; spaceAsPlus = true): + string = for it in kvs: let (name, value) = it if result != "": @@ -1013,7 +1013,7 @@ proc serializeApplicationXWWWFormUrlEncoded*(kvs: seq[(string, string)]; result.percentEncode(value, ApplicationXWWWFormUrlEncodedSet, spaceAsPlus) proc initURLSearchParams(params: URLSearchParams; init: string) = - params.list = parseApplicationXWWWFormUrlEncoded(init) + params.list = parseFromURLEncoded(init) proc newURLSearchParams[ T: seq[(string, string)]| @@ -1034,7 +1034,7 @@ proc newURLSearchParams[ result.initURLSearchParams(init) proc `$`*(params: URLSearchParams): string {.jsfunc.} = - return serializeApplicationXWWWFormUrlEncoded(params.list) + return serializeFormURLEncoded(params.list) proc update(params: URLSearchParams) = if params.url.isNone: @@ -1076,13 +1076,13 @@ proc parseAPIURL(s: string; base: Option[string]): JSResult[URL] = let baseURL = if base.isSome: let x = parseURL(base.get) if x.isNone: - return err(newTypeError(base.get & " is not a valid URL")) + return errTypeError(base.get & " is not a valid URL") x else: none(URL) let url = parseURL(s, baseURL) if url.isNone: - return err(newTypeError(s & " is not a valid URL")) + return errTypeError(s & " is not a valid URL") return ok(url.get) proc newURL*(s: string; base: Option[string] = none(string)): @@ -1212,7 +1212,7 @@ proc setSearch*(url: URL; s: string) {.jsfset: "search".} = let s = if s[0] == '?': s.substr(1) else: s url.query = some("") discard basicParseURL(s, url = url, stateOverride = some(usQuery)) - url.searchParams.list = parseApplicationXWWWFormUrlEncoded(s) + url.searchParams.list = parseFromURLEncoded(s) proc hash*(url: URL): string {.jsfget.} = if url.fragment.get("") == "": diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 8171010a..3744d9e2 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -291,6 +291,9 @@ func parseOctUInt32*(s: string; allowSign: static bool): Option[uint32] = func parseHexUInt32*(s: string; allowSign: static bool): Option[uint32] = return parseUIntImpl[uint32](s, allowSign, AsciiHexDigit, 16) +func parseUInt64*(s: string; allowSign: static bool): Option[uint64] = + return parseUIntImpl[uint64](s, allowSign) + #TODO not sure where this algorithm is from... # (probably from CSS) func parseFloat64*(s: string): float64 = diff --git a/todo b/todo index 32099bff..2c0be87d 100644 --- a/todo +++ b/todo @@ -75,16 +75,11 @@ layout engine: images: - more efficient kitty display (use IDs) - more efficient sixel display (store encoded images) -- more efficient display in general (why are we repainting 3-4 times per - keypress?) +- more efficient display in general (why are we repainting twice per keypress?) - document it (when performance is acceptable) -- proper sixel color register allocation (current one is a hack) +- proper sixel color register allocation, dithering - fix race condition where images decoded after buffer load won't display until reshape -- remove in-buffer decoder; instead, decode images in fully locked down CGI - scripts - * then, the pager can just read the output from the cache on-demand - instead of copying it from buffers - incremental decoding, interlaced images, animation man: - add a DOM -> man page converter so that we do not depend on pandoc |