diff options
author | bptato <nincsnevem662@gmail.com> | 2024-08-13 22:48:12 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-08-13 23:03:41 +0200 |
commit | 885a3493b6cad4b4247a200928fe61e41883aaba (patch) | |
tree | 2b823ef18043c775f21b8ad723c826ffdc6b2663 /src/loader | |
parent | 968de41082280dde47bac7c2bb59522284b4c672 (diff) | |
download | chawan-885a3493b6cad4b4247a200928fe61e41883aaba.tar.gz |
xhr: progress
* fix header case sensitivity issues -> probably still wrong as it discards the original casing. better than nothing, anyway * fix fulfill on generic promises * support standard open() async parameter weirdness * refactor loader response body reading (so bodyRead is no longer mandatory) * actually read response body still missing: response body getters
Diffstat (limited to 'src/loader')
-rw-r--r-- | src/loader/headers.nim | 28 | ||||
-rw-r--r-- | src/loader/loader.nim | 16 | ||||
-rw-r--r-- | src/loader/response.nim | 90 |
3 files changed, 90 insertions, 44 deletions
diff --git a/src/loader/headers.nim b/src/loader/headers.nim index 6b598cc2..e09d2267 100644 --- a/src/loader/headers.nim +++ b/src/loader/headers.nim @@ -5,6 +5,7 @@ import monoucha/fromjs import monoucha/javascript import monoucha/jserror import monoucha/quickjs +import monoucha/tojs import types/opt import utils/twtstr @@ -99,7 +100,8 @@ func isForbiddenRequestHeader*(name, value: string): bool = return false func isForbiddenResponseHeaderName*(name: string): bool = - return name in ["Set-Cookie", "Set-Cookie2"] + return name.equalsIgnoreCase("Set-Cookie") or + name.equalsIgnoreCase("Set-Cookie2") proc validate(this: Headers; name, value: string): JSResult[bool] = if not name.isValidHeaderName() or not value.isValidHeaderValue(): @@ -118,7 +120,6 @@ func isNoCorsSafelistedName(name: string): bool = name.equalsIgnoreCase("Content-Language") or name.equalsIgnoreCase("Content-Type") - const CorsUnsafeRequestByte = { char(0x00)..char(0x08), char(0x10)..char(0x1F), '"', '(', ')', ':', '<', '>', '?', '@', '[', '\\', ']', '{', '}', '\e' @@ -145,12 +146,14 @@ func isNoCorsSafelisted(name, value: string): bool = func get0(this: Headers; name: string): string = return this.table[name].join(", ") -proc get(this: Headers; name: string): JSResult[Option[string]] {.jsfunc.} = +proc get*(ctx: JSContext; this: Headers; name: string): JSValue {.jsfunc.} = if not name.isValidHeaderName(): - return errTypeError("Invalid header name") + JS_ThrowTypeError(ctx, "Invalid header name") + return JS_EXCEPTION + let name = name.toHeaderCase() if name notin this.table: - return ok(none(string)) - return ok(some(this.get0(name))) + return JS_NULL + return ctx.toJS(this.get0(name)) proc removeRange(this: Headers) = if this.guard == hgRequestNoCors: @@ -162,18 +165,21 @@ proc append(this: Headers; name, value: string): JSResult[void] {.jsfunc.} = let value = value.strip(chars = HTTPWhitespace) if not ?this.validate(name, value): return ok() + let name = name.toHeaderCase() if this.guard == hgRequestNoCors: if name in this.table: let tmp = this.get0(name) & ", " & value if not name.isNoCorsSafelisted(tmp): return ok() - this.table[name].add(value) - else: - this.table[name] = @[value] + if name in this.table: + this.table[name].add(value) + else: + this.table[name] = @[value] this.removeRange() ok() proc delete(this: Headers; name: string): JSResult[void] {.jsfunc.} = + let name = name.toHeaderCase() if not ?this.validate(name, "") or name notin this.table: return ok() if not name.isNoCorsSafelistedName() and not name.equalsIgnoreCase("Range"): @@ -185,6 +191,7 @@ proc delete(this: Headers; name: string): JSResult[void] {.jsfunc.} = proc has(this: Headers; name: string): JSResult[bool] {.jsfunc.} = if not name.isValidHeaderName(): return errTypeError("Invalid header name") + let name = name.toHeaderCase() return ok(name in this.table) proc set(this: Headers; name, value: string): JSResult[void] {.jsfunc.} = @@ -193,8 +200,7 @@ proc set(this: Headers; name, value: string): JSResult[void] {.jsfunc.} = return if this.guard == hgRequestNoCors and not name.isNoCorsSafelisted(value): return - #TODO do this case insensitively - this.table[name] = @[value] + this.table[name.toHeaderCase()] = @[value] this.removeRange() proc fill(headers: Headers; s: seq[(string, string)]): JSResult[void] = diff --git a/src/loader/loader.nim b/src/loader/loader.nim index 91212e24..dfc95b8b 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -1140,21 +1140,17 @@ proc onRead*(loader: FileLoader; fd: int) = if response != nil: response.onRead(response) if response.body.isend: - response.bodyRead.resolve() - response.bodyRead = nil + if response.onFinish != nil: + response.onFinish(response, true) + response.onFinish = nil response.unregisterFun() proc onError*(loader: FileLoader; fd: int) = 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(lbuf) - assert n == 0 - assert response.body.isend - response.bodyRead.resolve() - response.bodyRead = nil + if response.onFinish != nil: + response.onFinish(response, false) + response.onFinish = nil response.unregisterFun() # Note: this blocks until headers are received. diff --git a/src/loader/response.nim b/src/loader/response.nim index a3b7e3e4..8143ccbf 100644 --- a/src/loader/response.nim +++ b/src/loader/response.nim @@ -11,6 +11,7 @@ import loader/request import monoucha/javascript import monoucha/jserror import monoucha/quickjs +import monoucha/tojs import types/blob import types/color import types/opt @@ -40,10 +41,10 @@ type url*: URL #TODO should be urllist? unregisterFun*: proc() resumeFun*: proc(outputId: int) - bodyRead*: EmptyPromise internalMessage*: string # should NOT be exposed to JS! outputId*: int onRead*: proc(response: Response) {.nimcall.} + onFinish*: proc(response: Response; success: bool) {.nimcall.} opaque*: RootRef flags*: set[ResponseFlag] @@ -55,7 +56,6 @@ proc newResponse*(res: int; request: Request; stream: SocketStream; res: res, url: request.url, body: stream, - bodyRead: EmptyPromise(), outputId: outputId, status: status ) @@ -83,8 +83,10 @@ proc close*(response: Response) {.jsfunc.} = response.bodyUsed = true if response.unregisterFun != nil: response.unregisterFun() + response.unregisterFun = nil if response.body != nil: response.body.sclose() + response.body = nil func getCharset*(this: Response; fallback: Charset): Charset = if "Content-Type" notin this.headers.table: @@ -103,8 +105,17 @@ func getContentType*(this: Response; fallback = "application/octet-stream"): # override buffer mime.types return DefaultGuess.guessContentType(this.url.pathname, fallback) +func getContentLength*(this: Response): int64 = + this.headers.table.withValue("Content-Length", p): + for x in p[]: + let u = parseUInt64(x.strip(), allowSign = false) + if u.isSome and u.get <= uint64(int64.high): + return int64(u.get) + return -1 + type TextOpaque = ref object of RootObj buf: string + bodyRead: Promise[JSResult[string]] const BufferSize = 4096 @@ -122,6 +133,16 @@ proc onReadText(response: Response) = opaque.buf.setLen(olen) break +proc onFinishText(response: Response; success: bool) = + let opaque = TextOpaque(response.opaque) + let bodyRead = opaque.bodyRead + if success: + let charset = response.getCharset(CHARSET_UTF_8) + bodyRead.resolve(JSResult[string].ok(opaque.buf.decodeAll(charset))) + else: + let err = newTypeError("NetworkError when attempting to fetch resource") + bodyRead.resolve(JSResult[string].err(err)) + proc resume*(response: Response) = response.resumeFun(response.outputId) response.resumeFun = nil @@ -137,20 +158,20 @@ proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} = .err(newTypeError("Body has already been consumed")) p.resolve(err) return p - let opaque = TextOpaque() + let opaque = TextOpaque(bodyRead: newPromise[JSResult[string]]()) response.opaque = opaque response.onRead = onReadText + response.onFinish = onFinishText response.bodyUsed = true response.resume() - return response.bodyRead.then(proc(): JSResult[string] = - let charset = response.getCharset(CHARSET_UTF_8) - ok(opaque.buf.decodeAll(charset)) - ) + return opaque.bodyRead type BlobOpaque = ref object of RootObj p: pointer len: int size: int + bodyRead: Promise[JSResult[Blob]] + contentType: string proc onReadBlob(response: Response) = let opaque = BlobOpaque(response.opaque) @@ -168,29 +189,45 @@ proc onReadBlob(response: Response) = except ErrorAgain: break +proc onFinishBlob(response: Response; success: bool) = + let opaque = BlobOpaque(response.opaque) + let bodyRead = opaque.bodyRead + if success: + let p = realloc(opaque.p, opaque.len) + opaque.p = nil + let blob = if p == nil: + newBlob(nil, 0, opaque.contentType, nil) + else: + newBlob(p, opaque.len, opaque.contentType, deallocBlob) + bodyRead.resolve(JSResult[Blob].ok(blob)) + else: + if opaque.p != nil: + dealloc(opaque.p) + opaque.p = nil + let res = newTypeError("Error reading response") + bodyRead.resolve(JSResult[Blob].err(res)) + proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} = 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 opaque = BlobOpaque() + let opaque = BlobOpaque( + bodyRead: newPromise[JSResult[Blob]](), + contentType: response.getContentType() + ) response.opaque = opaque response.onRead = onReadBlob + response.onFinish = onFinishBlob response.bodyUsed = true response.resume() - let contentType = response.getContentType() - 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)) - ok(newBlob(p, opaque.len, contentType, deallocBlob)) - ) + return opaque.bodyRead type BitmapOpaque = ref object of RootObj bmp: Bitmap idx: int + bodyRead: EmptyPromise proc onReadBitmap(response: Response) = let opaque = BitmapOpaque(response.opaque) @@ -206,26 +243,33 @@ proc onReadBitmap(response: Response) = except ErrorAgain: break +proc onFinishBitmap(response: Response; success: bool) = + let opaque = BitmapOpaque(response.opaque) + opaque.bodyRead.resolve() + proc saveToBitmap*(response: Response; bmp: Bitmap): EmptyPromise = assert not response.bodyUsed - let opaque = BitmapOpaque(bmp: bmp, idx: 0) + let opaque = BitmapOpaque(bmp: bmp, idx: 0, bodyRead: EmptyPromise()) let size = bmp.width * bmp.height bmp.px = cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](size)) response.opaque = opaque if size > 0: response.onRead = onReadBitmap + response.onFinish = onFinishBitmap else: response.unregisterFun() response.body.sclose() + opaque.bodyRead.resolve() response.bodyUsed = true response.resume() - return response.bodyRead + return opaque.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), csize_t(s.len), cstring"<input>")) +proc json(ctx: JSContext; this: Response): Promise[JSValue] {.jsfunc.} = + return this.text().then(proc(s: JSResult[string]): JSValue = + if s.isNone: + return ctx.toJS(s.error) + return JS_ParseJSON(ctx, cstring(s.get), csize_t(s.get.len), + cstring"<input>") ) proc addResponseModule*(ctx: JSContext) = |