diff options
author | bptato <nincsnevem662@gmail.com> | 2024-05-20 18:12:09 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-05-20 18:12:09 +0200 |
commit | 7c4bb940410c8f5ad59e1d21d5565364a9a0cd71 (patch) | |
tree | d46d682ce9d4f308232c961985d8411c2a70197c | |
parent | 723613b0a02605dbf715d74c70b9ec29f1092c76 (diff) | |
download | chawan-7c4bb940410c8f5ad59e1d21d5565364a9a0cd71.tar.gz |
html: improve Request, derive Client from Window
* make Client an instance of Window (for less special casing) * misc work on Request & fetch * improve origin comparison (opaque origins of same URLs are now considered the same)
-rw-r--r-- | src/html/chadombuilder.nim | 10 | ||||
-rw-r--r-- | src/html/dom.nim | 54 | ||||
-rw-r--r-- | src/html/env.nim | 48 | ||||
-rw-r--r-- | src/html/script.nim | 34 | ||||
-rw-r--r-- | src/html/xmlhttprequest.nim | 14 | ||||
-rw-r--r-- | src/js/javascript.nim | 8 | ||||
-rw-r--r-- | src/loader/cgi.nim | 2 | ||||
-rw-r--r-- | src/loader/loader.nim | 22 | ||||
-rw-r--r-- | src/loader/request.nim | 261 | ||||
-rw-r--r-- | src/loader/response.nim | 12 | ||||
-rw-r--r-- | src/local/client.nim | 37 | ||||
-rw-r--r-- | src/local/container.nim | 4 | ||||
-rw-r--r-- | src/local/pager.nim | 2 | ||||
-rw-r--r-- | src/server/buffer.nim | 11 | ||||
-rw-r--r-- | src/types/referrer.nim | 65 | ||||
-rw-r--r-- | src/types/url.nim | 77 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 16 |
17 files changed, 362 insertions, 315 deletions
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim index 92effe3a..3c5b2c12 100644 --- a/src/html/chadombuilder.nim +++ b/src/html/chadombuilder.nim @@ -352,18 +352,14 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string): case t of "text/html": let global = JS_GetGlobalObject(ctx) - let window = if ctx.hasClass(Window): - fromJS[Window](ctx, global).get(nil) - else: - Window(nil) + let window = fromJS[Window](ctx, global).get JS_FreeValue(ctx, global) - let url = if window != nil and window.document != nil: + let url = if window.document != nil: window.document.url else: newURL("about:blank").get #TODO this is probably broken in client (or at least sub-optimal) - let factory = if window != nil: window.factory else: newCAtomFactory() - let builder = newChaDOMBuilder(url, window, factory, ccIrrelevant) + let builder = newChaDOMBuilder(url, window, window.factory, ccIrrelevant) var parser = initHTML5Parser(builder, HTML5ParserOpts[Node, CAtom]()) let res = parser.parseChunk(str) assert res == PRES_CONTINUE diff --git a/src/html/dom.nim b/src/html/dom.nim index 5833142b..14d46037 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -71,11 +71,11 @@ type Window* = ref object of EventTarget attrs*: WindowAttributes - console* {.jsget.}: Console + internalConsole*: Console navigator* {.jsget.}: Navigator screen* {.jsget.}: Screen settings*: EnvironmentSettings - loader*: Option[FileLoader] + internalLoader*: Option[FileLoader] location* {.jsget.}: Location jsrt*: JSRuntime jsctx*: JSContext @@ -1512,7 +1512,7 @@ proc reload(location: Location) {.jsuffunc.} = location.document.window.navigate(location.url) func origin(location: Location): string {.jsuffget.} = - return location.url.origin + return location.url.jsOrigin func protocol(location: Location): string {.jsuffget.} = return location.url.protocol @@ -2185,14 +2185,14 @@ func outerHTML(element: Element): string {.jsfget.} = func crossOrigin0(element: HTMLElement): CORSAttribute = if not element.attrb(satCrossorigin): - return NO_CORS + return caNoCors case element.attr(satCrossorigin) of "anonymous", "": - return ANONYMOUS + return caAnonymous of "use-credentials": - return USE_CREDENTIALS + return caUseCredentials else: - return ANONYMOUS + return caAnonymous func crossOrigin(element: HTMLScriptElement): CORSAttribute {.jsfget.} = return element.crossOrigin0 @@ -2201,7 +2201,7 @@ func crossOrigin(element: HTMLImageElement): CORSAttribute {.jsfget.} = return element.crossOrigin0 func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] = - getReferrerPolicy(element.attr(satReferrerpolicy)) + return strictParseEnum[ReferrerPolicy](element.attr(satReferrerpolicy)) proc sheets*(document: Document): seq[CSSStylesheet] = if document.cachedSheetsInvalid: @@ -2613,14 +2613,9 @@ func newDocument*(factory: CAtomFactory): Document = func newDocument(ctx: JSContext): Document {.jsctor.} = let global = JS_GetGlobalObject(ctx) - let window = if ctx.hasClass(Window): - fromJS[Window](ctx, global).get(nil) - else: - Window(nil) + let window = fromJS[Window](ctx, global).get JS_FreeValue(ctx, global) - #TODO this is probably broken in client (or at least sub-optimal) - let factory = if window != nil: window.factory else: newCAtomFactory() - return newDocument(factory) + return newDocument(window.factory) func newDocumentType*(document: Document; name, publicId, systemId: string): DocumentType = @@ -2815,6 +2810,12 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} = var appliesFwdDecl*: proc(mqlist: MediaQueryList; window: Window): bool {.nimcall, noSideEffect.} +func loader*(window: Window): Option[FileLoader] {.inline.} = + return window.internalLoader + +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) = @@ -3565,19 +3566,20 @@ proc createClassicScript(ctx: JSContext; source: string; baseURL: URL; type OnCompleteProc = proc(element: HTMLScriptElement, res: ScriptResult) proc fetchClassicScript(element: HTMLScriptElement; url: URL; - options: ScriptOptions; cors: CORSAttribute; cs: Charset, + options: ScriptOptions; cors: CORSAttribute; cs: Charset; onComplete: OnCompleteProc) = let window = element.document.window if not element.scriptingEnabled or window.loader.isNone: element.onComplete(ScriptResult(t: RESULT_NULL)) return let loader = window.loader.get - let request = createPotentialCORSRequest(url, RequestDestination.SCRIPT, cors) - let response = loader.doRequest(request) + let request = createPotentialCORSRequest(url, rdScript, cors) + request.client = some(window.settings) + #TODO make this non-blocking somehow + let response = loader.doRequest(request.request) if response.res != 0: element.onComplete(ScriptResult(t: RESULT_NULL)) return - #TODO make this non-blocking somehow let s = response.body.recvAll() let source = if cs in {CHARSET_UNKNOWN, CHARSET_UTF_8}: s.toValidUTF8() @@ -3603,7 +3605,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL; window.importMapsAllowed = false element.fetchSingleModule( url, - RequestDestination.SCRIPT, + rdScript, options, parseURL("about:client").get, isTopLevel = true, @@ -3611,8 +3613,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL; if res.t == RESULT_NULL: element.onComplete(res) else: - element.fetchDescendantsAndLink(res.script, RequestDestination.SCRIPT, - onComplete) + element.fetchDescendantsAndLink(res.script, rdScript, onComplete) ) proc fetchDescendantsAndLink(element: HTMLScriptElement; script: Script; @@ -3623,6 +3624,8 @@ proc fetchDescendantsAndLink(element: HTMLScriptElement; script: Script; proc fetchSingleModule(element: HTMLScriptElement; url: URL; destination: RequestDestination; options: ScriptOptions, referrer: URL; isTopLevel: bool; onComplete: OnCompleteProc) = + discard #TODO implement + #[ let moduleType = "javascript" #TODO moduleRequest let settings = element.document.window.settings @@ -3634,10 +3637,10 @@ proc fetchSingleModule(element: HTMLScriptElement; url: URL; element.onComplete(settings.moduleMap[i].value) return let destination = fetchDestinationFromModuleType(destination, moduleType) - let mode = if destination in {WORKER, SHAREDWORKER, SERVICEWORKER}: - RequestMode.SAME_ORIGIN + let mode = if destination in {rdWorker, rdSharedworker, rdServiceworker}: + rmSameOrigin else: - RequestMode.CORS + rmCors #TODO client #TODO initiator type let request = newRequest( @@ -3647,6 +3650,7 @@ proc fetchSingleModule(element: HTMLScriptElement; url: URL; destination = destination ) discard request #TODO + ]# proc execute*(element: HTMLScriptElement) = let document = element.document diff --git a/src/html/env.nim b/src/html/env.nim index dc63f790..76aa2007 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -91,17 +91,24 @@ proc height(screen: var Screen): int64 {.jsfget.} = proc colorDepth(screen: var Screen): int64 {.jsfget.} = 24 proc pixelDepth(screen: var Screen): int64 {.jsfget.} = screen.colorDepth -proc addNavigatorModule(ctx: JSContext) = +proc addNavigatorModule*(ctx: JSContext) = ctx.registerType(Navigator) ctx.registerType(PluginArray) ctx.registerType(MimeTypeArray) ctx.registerType(Screen) -proc fetch[T: Request|string](window: Window; req: T; init = none(RequestInit)): - JSResult[FetchPromise] {.jsfunc.} = +proc fetch[T: JSRequest|string](window: Window; input: T; + init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} = if window.loader.isSome: - let req = ?newRequest(window.jsctx, req, init) - return ok(window.loader.get.fetch(req)) + let input = ?newRequest(window.jsctx, input, init) + #TODO cors requests? + if not window.settings.origin.isSameOrigin(input.request.url.origin): + let promise = FetchPromise() + let err = newTypeError("NetworkError when attempting to fetch resource") + promise.resolve(JSResult[Response].err(err)) + return ok(promise) + return ok(window.loader.get.fetch(input.request)) + return ok(nil) proc setTimeout[T: JSValue|string](window: Window; handler: T; timeout = 0i32): int32 {.jsfunc.} = @@ -117,6 +124,9 @@ proc clearTimeout(window: Window; id: int32) {.jsfunc.} = proc clearInterval(window: Window; id: int32) {.jsfunc.} = window.timeouts.clearInterval(id) +func console(window: Window): Console {.jsfget.} = + return window.internalConsole + proc screenX(window: Window): int64 {.jsfget.} = 0 proc screenY(window: Window): int64 {.jsfget.} = 0 proc screenLeft(window: Window): int64 {.jsfget.} = 0 @@ -170,6 +180,17 @@ proc setOnLoad(ctx: JSContext; window: Window; val: JSValue) doAssert ctx.addEventListener(window, "load", val).isSome JS_FreeValue(ctx, this) +proc addWindowModule*(ctx: JSContext) = + ctx.addEventModule() + let eventTargetCID = ctx.getClass("EventTarget") + ctx.registerType(Window, parent = eventTargetCID, asglobal = true) + +proc addWindowModule2*(ctx: JSContext) = + ctx.addEventModule() + let eventTargetCID = ctx.getClass("EventTarget") + ctx.registerType(Window, parent = eventTargetCID, asglobal = true, + globalparent = true) + proc addScripting*(window: Window; selector: Selector[int]) = let rt = newJSRuntime() let ctx = rt.newJSContext() @@ -189,12 +210,8 @@ proc addScripting*(window: Window; selector: Selector[int]) = JS_FreeValue(ctx, ret) ) ) - var global = JS_GetGlobalObject(ctx) - ctx.addEventModule() - let eventTargetCID = ctx.getClass("EventTarget") - ctx.registerType(Window, asglobal = true, parent = eventTargetCID) + ctx.addWindowModule() ctx.setGlobal(window) - JS_FreeValue(ctx, global) ctx.addDOMExceptionModule() ctx.addConsoleModule() ctx.addNavigatorModule() @@ -219,17 +236,18 @@ proc runJSJobs*(window: Window) = ctx.writeException(window.console.err) proc newWindow*(scripting, images: bool; selector: Selector[int]; - attrs: WindowAttributes; factory: CAtomFactory; - navigate: proc(url: URL) = nil, loader = none(FileLoader)): Window = + attrs: WindowAttributes; factory: CAtomFactory; navigate: proc(url: URL); + loader: FileLoader; url: URL): Window = let err = newDynFileStream(stderr) let window = Window( attrs: attrs, - console: newConsole(err), + internalConsole: newConsole(err), navigator: Navigator(), - loader: loader, + internalLoader: some(loader), images: images, settings: EnvironmentSettings( - scripting: scripting + scripting: scripting, + origin: url.origin ), navigate: navigate, factory: factory diff --git a/src/html/script.nim b/src/html/script.nim index 61066abe..c0962b13 100644 --- a/src/html/script.nim +++ b/src/html/script.nim @@ -1,5 +1,4 @@ import js/javascript -import loader/request import types/referrer import types/url @@ -13,10 +12,39 @@ type ScriptResultType* = enum RESULT_NULL, RESULT_SCRIPT, RESULT_IMPORT_MAP_PARSE, RESULT_FETCHING + RequestDestination* = enum + rdNone = "" + rdAudio = "audio" + rdAudioworklet = "audioworklet" + rdDocument = "document" + rdEmbed = "embed" + rdFont = "font" + rdFrame = "frame" + rdIframe = "iframe" + rdImage = "image" + rdJson = "json" + rdManifest = "manifest" + rdObject = "object" + rdPaintworklet = "paintworklet" + rdReport = "report" + rdScript = "script" + rdServiceworker = "serviceworker" + rdSharedworker = "sharedworker" + rdStyle = "style" + rdTrack = "track" + rdWorker = "worker" + rdXslt = "xslt" + + CredentialsMode* = enum + cmSameOrigin = "same-origin" + cmOmit = "omit" + cmInclude = "include" + type EnvironmentSettings* = ref object scripting*: bool moduleMap*: ModuleMap + origin*: Origin Script* = object #TODO setings @@ -61,7 +89,7 @@ proc find*(moduleMap: ModuleMap; url: URL; moduleType: string): int = func fetchDestinationFromModuleType*(default: RequestDestination; moduleType: string): RequestDestination = if moduleType == "json": - return RequestDestination.JSON + return rdJson if moduleType == "css": - return RequestDestination.STYLE + return rdStyle return default diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim index 69fe157d..33f1e61c 100644 --- a/src/html/xmlhttprequest.nim +++ b/src/html/xmlhttprequest.nim @@ -69,13 +69,13 @@ func readyState(this: XMLHttpRequest): uint16 {.jsfget.} = proc parseMethod(s: string): DOMResult[HttpMethod] = return case s.toLowerAscii() - of "get": ok(HTTP_GET) - of "delete": ok(HTTP_DELETE) - of "head": ok(HTTP_HEAD) - of "options": ok(HTTP_OPTIONS) - of "patch": ok(HTTP_PATCH) - of "post": ok(HTTP_POST) - of "put": ok(HTTP_PUT) + of "get": ok(hmGet) + of "delete": ok(hmDelete) + of "head": ok(hmHead) + of "options": ok(hmOptions) + of "patch": ok(hmPatch) + of "post": ok(hmPost) + of "put": ok(hmPut) of "connect", "trace", "track": errDOMException("Forbidden method", "SecurityError") else: diff --git a/src/js/javascript.nim b/src/js/javascript.nim index bfe45aae..a697bb2f 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -1601,8 +1601,9 @@ proc bindEndStmts(endstmts: NimNode; info: RegistryInfo) = let `classDef` = JSClassDefConst(addr cd)) macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0; - asglobal: static bool = false; nointerface = false; - name: static string = ""; has_extra_getset: static bool = false; + asglobal: static bool = false; globalparent: static bool = false; + nointerface = false; name: static string = ""; + has_extra_getset: static bool = false; extra_getset: static openArray[TabGetSet] = []; namespace = JS_NULL; errid = opt(JSErrorEnum); ishtmldda = false): JSClassID = var stmts = newStmtList() @@ -1636,9 +1637,10 @@ macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0; let tname = info.tname let unforgeable = info.tabUnforgeable let staticfuns = info.tabStatic + let global = asglobal and not globalparent endstmts.add(quote do: `ctx`.newJSClass(`classDef`, `tname`, getTypePtr(`t`), `sctr`, `tabList`, - `parent`, bool(`asglobal`), `nointerface`, `finName`, `namespace`, + `parent`, bool(`global`), `nointerface`, `finName`, `namespace`, `errid`, `unforgeable`, `staticfuns`, `ishtmldda`) ) stmts.add(newBlockStmt(endstmts)) diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim index 347855ac..f3c5b1e3 100644 --- a/src/loader/cgi.nim +++ b/src/loader/cgi.nim @@ -41,7 +41,7 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI, myDir: string; putEnv("PATH_INFO", pathInfo) if url.query.isSome: putEnv("QUERY_STRING", url.query.get) - if request.httpMethod == HTTP_POST: + if request.httpMethod == hmPost: if request.multipart.isSome: putEnv("CONTENT_TYPE", request.multipart.get.getContentType()) else: diff --git a/src/loader/loader.nim b/src/loader/loader.nim index c378523b..8bc23c16 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -854,18 +854,18 @@ proc getRedirect*(response: Response; request: Request): Request = let location = response.headers.table["Location"][0] let url = parseURL(location, option(request.url)) if url.isSome: - if (response.status == 303 and - request.httpMethod notin {HTTP_GET, HTTP_HEAD}) or - (response.status == 301 or response.status == 302 and - request.httpMethod == HTTP_POST): - return newRequest(url.get, HTTP_GET, - mode = request.mode, credentialsMode = request.credentialsMode, - destination = request.destination) + let status = response.status + if status == 303 and request.httpMethod notin {hmGet, hmHead} or + status == 301 or + status == 302 and request.httpMethod == hmPost: + return newRequest(url.get, hmGet) else: - return newRequest(url.get, request.httpMethod, - body = request.body, multipart = request.multipart, - mode = request.mode, credentialsMode = request.credentialsMode, - destination = request.destination) + return newRequest( + url.get, + request.httpMethod, + body = request.body, + multipart = request.multipart + ) return nil template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader; diff --git a/src/loader/request.nim b/src/loader/request.nim index 8eeb17b8..1398d25f 100644 --- a/src/loader/request.nim +++ b/src/loader/request.nim @@ -3,9 +3,10 @@ import std/strutils import std/tables import bindings/quickjs -import js/jserror +import html/script import js/fromjs import js/javascript +import js/jserror import js/jstypes import loader/headers import types/blob @@ -15,82 +16,83 @@ import types/url type HttpMethod* = enum - HTTP_GET = "GET" - HTTP_CONNECT = "CONNECT" - HTTP_DELETE = "DELETE" - HTTP_HEAD = "HEAD" - HTTP_OPTIONS = "OPTIONS" - HTTP_PATCH = "PATCH" - HTTP_POST = "POST" - HTTP_PUT = "PUT" - HTTP_TRACE = "TRACE" + hmGet = "GET" + hmConnect = "CONNECT" + hmDelete = "DELETE" + hmHead = "HEAD" + hmOptions = "OPTIONS" + hmPatch = "PATCH" + hmPost = "POST" + hmPut = "PUT" + hmTrace = "TRACE" RequestMode* = enum - NO_CORS = "no-cors" - SAME_ORIGIN = "same-origin" - CORS = "cors" - NAVIGATE = "navigate" - WEBSOCKET = "websocket" - - RequestDestination* = enum - NO_DESTINATION = "" - AUDIO = "audio" - AUDIOWORKLET = "audioworklet" - DOCUMENT = "document" - EMBED = "embed" - FONT = "font" - FRAME = "frame" - IFRAME = "iframe" - IMAGE = "image" - JSON = "json" - MANIFEST = "manifest" - OBJECT = "object" - PAINTWORKLET = "paintworklet" - REPORT = "report" - SCRIPT = "script" - SERVICEWORKER = "serviceworker" - SHAREDWORKER = "sharedworker" - STYLE = "style" - TRACK = "track" - WORKER = "worker" - XSLT = "xslt" - - CredentialsMode* = enum - SAME_ORIGIN = "same-origin" - OMIT = "omit" - INCLUDE = "include" + rmNoCors = "no-cors" + rmSameOrigin = "same-origin" + rmCors = "cors" + rmNavigate = "navigate" + rmWebsocket = "websocket" CORSAttribute* = enum - NO_CORS = "no-cors" - ANONYMOUS = "anonymous" - USE_CREDENTIALS = "use-credentials" + caNoCors = "no-cors" + caAnonymous = "anonymous" + caUseCredentials = "use-credentials" type - Request* = ref RequestObj - RequestObj* = object + RequestOriginType* = enum + rotClient, rotOrigin + + RequestOrigin* = object + case t*: RequestOriginType + of rotClient: discard + of rotOrigin: + origin*: Origin + + RequestWindowType* = enum + rwtClient, rwtNoWindow, rwtWindow + + RequestWindow* = object + case t*: RequestWindowType + of rwtClient, rwtNoWindow: discard + of rwtWindow: + window*: EnvironmentSettings + + Request* = ref object httpMethod*: HttpMethod url*: URL - headers* {.jsget.}: Headers + headers*: Headers body*: Option[string] multipart*: Option[FormData] referrer*: URL - mode* {.jsget.}: RequestMode - destination* {.jsget.}: RequestDestination - credentialsMode* {.jsget.}: CredentialsMode 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 -jsDestructor(Request) + JSRequest* = ref object + request*: Request + mode* {.jsget.}: RequestMode + destination* {.jsget.}: RequestDestination + credentialsMode* {.jsget.}: CredentialsMode + origin*: RequestOrigin + window*: RequestWindow + client*: Option[EnvironmentSettings] + +jsDestructor(JSRequest) -proc js_url(this: Request): string {.jsfget: "url".} = +func headers(this: JSRequest): Headers {.jsfget.} = + return this.request.headers + +func url(this: JSRequest): URL = + return this.request.url + +proc jsUrl(this: JSRequest): string {.jsfget: "url".} = return $this.url #TODO pretty sure this is incorrect -proc js_referrer(this: Request): string {.jsfget: "referrer".} = - if this.referrer != nil: - return $this.referrer +proc jsReferrer(this: JSRequest): string {.jsfget: "referrer".} = + if this.request.referrer != nil: + return $this.request.referrer return "" iterator pairs*(headers: Headers): (string, string) = @@ -98,10 +100,8 @@ iterator pairs*(headers: Headers): (string, string) = for v in vs: yield (k, v) -func newRequest*(url: URL; httpMethod = HTTP_GET; headers = newHeaders(); - body = none(string); multipart = none(FormData); mode = RequestMode.NO_CORS; - credentialsMode = CredentialsMode.SAME_ORIGIN; - destination = RequestDestination.NO_DESTINATION; proxy: URL = nil; +func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders(); + body = none(string); multipart = none(FormData); proxy: URL = nil; referrer: URL = nil; suspended = false): Request = return Request( url: url, @@ -109,18 +109,14 @@ func newRequest*(url: URL; httpMethod = HTTP_GET; headers = newHeaders(); headers: headers, body: body, multipart: multipart, - mode: mode, - credentialsMode: credentialsMode, - destination: destination, referrer: referrer, proxy: proxy, suspended: suspended ) -func newRequest*(url: URL; httpMethod = HTTP_GET; +func newRequest*(url: URL; httpMethod = hmGet; headers: seq[(string, string)] = @[]; body = none(string); - multipart = none(FormData); mode = RequestMode.NO_CORS; proxy: URL = nil): - Request = + multipart = none(FormData); proxy: URL = nil): Request = let hl = newHeaders() for pair in headers: let (k, v) = pair @@ -131,43 +127,39 @@ func newRequest*(url: URL; httpMethod = HTTP_GET; hl, body, multipart, - mode, proxy = proxy ) func createPotentialCORSRequest*(url: URL; destination: RequestDestination; - cors: CORSAttribute; fallbackFlag = false): Request = - var mode = if cors == NO_CORS: - RequestMode.NO_CORS + cors: CORSAttribute; fallbackFlag = false): JSRequest = + var mode = if cors == caNoCors: + rmNoCors else: - RequestMode.CORS - if fallbackFlag and mode == NO_CORS: - mode = SAME_ORIGIN - let credentialsMode = if cors == ANONYMOUS: - CredentialsMode.SAME_ORIGIN - else: CredentialsMode.INCLUDE - return newRequest( - url, - destination = destination, - mode = mode, - credentialsMode = credentialsMode + rmCors + if fallbackFlag and mode == rmNoCors: + mode = rmSameOrigin + let credentialsMode = if cors == caAnonymous: cmSameOrigin else: cmInclude + return JSRequest( + request: newRequest(url), + destination: destination, + mode: mode, + credentialsMode: credentialsMode ) type BodyInitType = enum - BODY_INIT_BLOB, BODY_INIT_FORM_DATA, BODY_INIT_URL_SEARCH_PARAMS, - BODY_INIT_STRING + bitBlob, bitFormData, bitUrlSearchParams, bitString BodyInit = object #TODO ReadableStream, BufferSource case t: BodyInitType - of BODY_INIT_BLOB: + of bitBlob: blob: Blob - of BODY_INIT_FORM_DATA: + of bitFormData: formData: FormData - of BODY_INIT_URL_SEARCH_PARAMS: + of bitUrlSearchParams: searchParams: URLSearchParams - of BODY_INIT_STRING: + of bitString: str: string RequestInit* = object of JSDict @@ -180,6 +172,7 @@ type credentials: Option[CredentialsMode] proxyUrl: URL mode: Option[RequestMode] + window: Option[JSValue] proc fromJSBodyInit(ctx: JSContext; val: JSValue): JSResult[BodyInit] = if JS_IsUndefined(val) or JS_IsNull(val): @@ -187,67 +180,72 @@ proc fromJSBodyInit(ctx: JSContext; val: JSValue): JSResult[BodyInit] = block formData: let x = fromJS[FormData](ctx, val) if x.isSome: - return ok(BodyInit(t: BODY_INIT_FORM_DATA, formData: x.get)) + return ok(BodyInit(t: bitFormData, formData: x.get)) block blob: let x = fromJS[Blob](ctx, val) if x.isSome: - return ok(BodyInit(t: BODY_INIT_BLOB, blob: x.get)) + return ok(BodyInit(t: bitBlob, blob: x.get)) block searchParams: let x = fromJS[URLSearchParams](ctx, val) if x.isSome: - return ok(BodyInit(t: BODY_INIT_URL_SEARCH_PARAMS, searchParams: x.get)) + return ok(BodyInit(t: bitUrlSearchParams, searchParams: x.get)) block str: let x = fromJS[string](ctx, val) if x.isSome: - return ok(BodyInit(t: BODY_INIT_STRING, str: x.get)) + return ok(BodyInit(t: bitString, str: x.get)) return err(newTypeError("Invalid body init type")) -func newRequest*[T: string|Request](ctx: JSContext; resource: T; - init = none(RequestInit)): JSResult[Request] {.jsctor.} = +func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T; + init = none(RequestInit)): JSResult[JSRequest] {.jsctor.} = + defer: + if init.isSome and init.get.window.isSome: + JS_FreeValue(ctx, init.get.window.get) when T is string: let url = ?newURL(resource) if url.username != "" or url.password != "": return err(newTypeError("Input URL contains a username or password")) - var httpMethod = HTTP_GET + var httpMethod = hmGet var headers = newHeaders() let referrer: URL = nil - var credentials = CredentialsMode.SAME_ORIGIN + var credentials = cmSameOrigin var body: Option[string] var multipart: Option[FormData] var proxyUrl: URL #TODO? - let fallbackMode = opt(RequestMode.CORS) + let fallbackMode = opt(rmCors) + var window = RequestWindow(t: rwtClient) else: let url = resource.url - var httpMethod = resource.httpMethod + var httpMethod = resource.request.httpMethod var headers = resource.headers.clone() - let referrer = resource.referrer + let referrer = resource.request.referrer var credentials = resource.credentialsMode - var body = resource.body - var multipart = resource.multipart - var proxyUrl = resource.proxy #TODO? + var body = resource.request.body + var multipart = resource.request.multipart + var proxyUrl = resource.request.proxy #TODO? let fallbackMode = none(RequestMode) - #TODO window - var mode = fallbackMode.get(RequestMode.NO_CORS) - let destination = NO_DESTINATION + var window = resource.window + var mode = fallbackMode.get(rmNoCors) + let destination = rdNone #TODO origin, window - if init.isSome: - if mode == RequestMode.NAVIGATE: - mode = RequestMode.SAME_ORIGIN + if init.isSome: #TODO spec wants us to check if it's "not empty"... + let init = init.get + if init.window.isSome: + if not JS_IsNull(init.window.get): + return errTypeError("Expected window to be null") + window = RequestWindow(t: rwtNoWindow) + if mode == rmNavigate: + mode = rmSameOrigin #TODO flags? #TODO referrer - let init = init.get httpMethod = init.`method` if init.body.isSome: let ibody = init.body.get case ibody.t - of BODY_INIT_FORM_DATA: - multipart = some(ibody.formData) - of BODY_INIT_STRING: - body = some(ibody.str) - else: - discard #TODO - if httpMethod in {HTTP_GET, HTTP_HEAD}: - return err(newTypeError("HEAD or GET Request cannot have a body.")) + of bitFormData: multipart = some(ibody.formData) + of bitString: body = some(ibody.str) + else: discard #TODO + if httpMethod in {hmGet, hmHead}: + return errTypeError("HEAD or GET Request cannot have a body.") if init.headers.isSome: headers.fill(init.headers.get) if init.credentials.isSome: @@ -256,25 +254,28 @@ func newRequest*[T: string|Request](ctx: JSContext; resource: T; mode = init.mode.get #TODO find a standard compatible way to implement this proxyUrl = init.proxyUrl - return ok(Request( - url: url, - httpMethod: httpMethod, - headers: headers, - body: body, - multipart: multipart, + return ok(JSRequest( + request: newRequest( + url, + httpMethod, + headers, + body, + multipart, + proxy = proxyUrl, + referrer = referrer + ), mode: mode, credentialsMode: credentials, destination: destination, - proxy: proxyUrl, - referrer: referrer + window: window )) func credentialsMode*(attribute: CORSAttribute): CredentialsMode = case attribute - of NO_CORS, ANONYMOUS: - return SAME_ORIGIN - of USE_CREDENTIALS: - return INCLUDE + of caNoCors, caAnonymous: + return cmSameOrigin + of caUseCredentials: + return cmInclude proc addRequestModule*(ctx: JSContext) = - ctx.registerType(Request) + ctx.registerType(JSRequest, name = "Request") diff --git a/src/loader/response.nim b/src/loader/response.nim index 63b07cf1..56e955a3 100644 --- a/src/loader/response.nim +++ b/src/loader/response.nim @@ -28,11 +28,11 @@ type #TODO fully implement headers guards HeadersGuard* = enum - GUARD_IMMUTABLE = "immutable" - GUARD_REQUEST = "request" - GUARD_REQUEST_NO_CORS = "request-no-cors" - GUARD_RESPONSE = "response" - GUARD_NONE = "none" + hgImmutable = "immutable" + hgRequest = "request" + hgRequestNoCors = "request-no-cors" + hgResponse = "response" + hgNone = "none" Response* = ref object responseType* {.jsget: "type".}: ResponseType @@ -67,7 +67,7 @@ func makeNetworkError*(): Response {.jsstfunc: "Response:error".} = responseType: TYPE_ERROR, status: 0, headers: newHeaders(), - headersGuard: GUARD_IMMUTABLE + headersGuard: hgImmutable ) func sok(response: Response): bool {.jsfget: "ok".} = diff --git a/src/local/client.nim b/src/local/client.nim index 07a7c523..19c31263 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -12,9 +12,10 @@ import std/unicode import bindings/constcharp import bindings/quickjs import config/config +import html/catom import html/chadombuilder import html/dom -import html/event +import html/env import html/formdata import html/xmlhttprequest import io/bufstream @@ -29,14 +30,14 @@ import js/base64 import js/console import js/domexception import js/encoding -import js/jserror import js/fromjs import js/intl import js/javascript -import js/jstypes -import js/jsutils +import js/jserror import js/jsmodule import js/jsopaque +import js/jstypes +import js/jsutils import js/timeout import js/tojs import loader/headers @@ -57,16 +58,13 @@ import utils/twtstr import chagashi/charset type - Client* = ref object + Client* = ref object of Window alive: bool config {.jsget.}: Config consoleWrapper: ConsoleWrapper fdmap: Table[int, Container] feednext: bool - jsctx: JSContext - jsrt: JSRuntime pager {.jsget.}: Pager - timeouts: TimeoutState pressed: tuple[col: int; row: int] exitCode: int inEval: bool @@ -91,11 +89,6 @@ template forkserver(client: Client): ForkServer = template readChar(client: Client): char = client.pager.term.readChar() -proc fetch[T: Request|string](client: Client; req: T; - init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} = - let req = ?newRequest(client.jsctx, req, init) - return ok(client.loader.fetch(req)) - proc interruptHandler(rt: JSRuntime; opaque: pointer): cint {.cdecl.} = let client = cast[Client](opaque) if client.console == nil or client.pager.term.istream == nil: @@ -805,12 +798,12 @@ func line(client: Client): LineEdit {.jsfget.} = return client.pager.lineedit.get(nil) proc addJSModules(client: Client; ctx: JSContext) = + ctx.addWindowModule2() ctx.addDOMExceptionModule() ctx.addConsoleModule() - ctx.addCookieModule() - ctx.addURLModule() - ctx.addEventModule() + ctx.addNavigatorModule() ctx.addDOMModule() + ctx.addURLModule() ctx.addHTMLModule() ctx.addIntlModule() ctx.addBlobModule() @@ -819,11 +812,12 @@ proc addJSModules(client: Client; ctx: JSContext) = ctx.addHeadersModule() ctx.addRequestModule() ctx.addResponseModule() + ctx.addEncodingModule() ctx.addLineEditModule() ctx.addConfigModule() ctx.addPagerModule() ctx.addContainerModule() - ctx.addEncodingModule() + ctx.addCookieModule() func getClient(client: Client): Client {.jsfget: "client".} = return client @@ -850,14 +844,17 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext; jsctx: jsctx, pager: pager, exitCode: -1, - alive: true + alive: true, + factory: newCAtomFactory(), + internalLoader: some(loader) ) jsrt.setInterruptHandler(interruptHandler, cast[pointer](client)) - jsctx.registerType(Client, asglobal = true) - jsctx.setGlobal(client) let global = JS_GetGlobalObject(jsctx) + jsctx.setGlobal(client) jsctx.definePropertyE(global, "cmd", config.cmd.jsObj) JS_FreeValue(jsctx, global) config.cmd.jsObj = JS_NULL client.addJSModules(jsctx) + let windowCID = jsctx.getClass("Window") + jsctx.registerType(Client, asglobal = true, parent = windowCID) return client diff --git a/src/local/container.nim b/src/local/container.nim index 75eba41b..00553190 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -1405,7 +1405,7 @@ proc extractCookies(response: Response): seq[Cookie] = proc extractReferrerPolicy(response: Response): Option[ReferrerPolicy] = if "Referrer-Policy" in response.headers: - return getReferrerPolicy(response.headers["Referrer-Policy"]) + return strictParseEnum[ReferrerPolicy](response.headers["Referrer-Policy"]) return none(ReferrerPolicy) # Apply data received in response. @@ -1423,7 +1423,7 @@ proc applyResponse*(container: Container; response: Response; if referrerPolicy.isSome: container.loaderConfig.referrerPolicy = referrerPolicy.get else: - container.loaderConfig.referrerPolicy = NO_REFERRER + container.loaderConfig.referrerPolicy = rpNoReferrer # setup content type; note that isSome means an override so we skip it if container.contentType.isNone: var contentType = response.getContentType() diff --git a/src/local/pager.nim b/src/local/pager.nim index b2cd779c..3a30a8f8 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -962,7 +962,7 @@ proc gotoURL(pager: Pager; request: Request; prevurl = none(URL); var loaderConfig: LoaderClientConfig var bufferConfig = pager.applySiteconf(request.url, cs, loaderConfig) if prevurl.isNone or not prevurl.get.equals(request.url, true) or - request.url.hash == "" or request.httpMethod != HTTP_GET: + request.url.hash == "" or request.httpMethod != hmGet: # Basically, we want to reload the page *only* when # a) we force a reload (by setting prevurl to none) # b) or the new URL isn't just the old URL + an anchor diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 54c32de3..178dd94b 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -887,7 +887,8 @@ proc setHTML(buffer: Buffer) = buffer.attrs, factory, navigate, - some(buffer.loader) + buffer.loader, + buffer.url ) let confidence = if buffer.config.charsetOverride == CHARSET_UNKNOWN: ccTentative @@ -1317,10 +1318,10 @@ proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request] = #TODO return none(Request) let httpmethod = if formmethod == fmGet: - HTTP_GET + hmGet else: assert formmethod == fmPost - HTTP_POST + hmPost #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): # submitter.attr("formtarget") @@ -1549,7 +1550,7 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult = repaint = true if s.isSome: let url = newURL("data:text/html," & s.get).get - let req = newRequest(url, HTTP_GET) + let req = newRequest(url, hmGet) return ClickResult( repaint: repaint, open: some(req) @@ -1559,7 +1560,7 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult = ) return ClickResult( repaint: repaint, - open: some(newRequest(url, HTTP_GET)) + open: some(newRequest(url, hmGet)) ) return ClickResult(repaint: repaint) diff --git a/src/types/referrer.nim b/src/types/referrer.nim index ec9acc76..7c7b6ffe 100644 --- a/src/types/referrer.nim +++ b/src/types/referrer.nim @@ -1,72 +1,51 @@ -import std/options - import types/url type ReferrerPolicy* = enum - STRICT_ORIGIN_WHEN_CROSS_ORIGIN - NO_REFERRER - NO_REFERRER_WHEN_DOWNGRADE - STRICT_ORIGIN - ORIGIN - SAME_ORIGIN - ORIGIN_WHEN_CROSS_ORIGIN - UNSAFE_URL - -const DefaultPolicy* = STRICT_ORIGIN_WHEN_CROSS_ORIGIN + rpStrictOriginWhenCrossOrigin = "strict-origin-when-cross-origin" + rpNoReferrer = "no-referrer" + rpNoReferrerWhenDowngrade = "no-referrer-when-downgrade" + rpStrictOrigin = "strict-origin" + rpOrigin = "origin" + rpSameOrigin = "same-origin" + rpOriginWhenCrossOrigin = "origin-when-cross-origin" + rpUnsafeURL = "unsafe-url" -proc getReferrerPolicy*(s: string): Option[ReferrerPolicy] = - case s - of "no-referrer": - return some(NO_REFERRER) - of "no-referrer-when-downgrade": - return some(NO_REFERRER_WHEN_DOWNGRADE) - of "origin": - return some(ORIGIN) - of "origin-when-cross-origin": - return some(ORIGIN_WHEN_CROSS_ORIGIN) - of "same-origin": - return some(SAME_ORIGIN) - of "strict-origin": - return some(STRICT_ORIGIN) - of "strict-origin-when-cross-origin": - return some(STRICT_ORIGIN_WHEN_CROSS_ORIGIN) - of "unsafe-url": - return some(UNSAFE_URL) +const DefaultPolicy* = rpStrictOriginWhenCrossOrigin proc getReferrer*(prev, target: URL; policy: ReferrerPolicy): string = - let origin = prev.origin0 - if origin.isNone: + let origin = prev.origin + if origin.t == otOpaque: return "" if prev.scheme != "http" and prev.scheme != "https": return "" if target.scheme != "http" and target.scheme != "https": return "" case policy - of NO_REFERRER: + of rpNoReferrer: return "" - of NO_REFERRER_WHEN_DOWNGRADE: + of rpNoReferrerWhenDowngrade: if prev.scheme == "https" and target.scheme == "http": return "" return $origin & prev.pathname & prev.search - of SAME_ORIGIN: - if origin == target.origin0: + of rpSameOrigin: + if origin.isSameOrigin(target.origin): return $origin return "" - of ORIGIN: + of rpOrigin: return $origin - of STRICT_ORIGIN: + of rpStrictOrigin: if prev.scheme == "https" and target.scheme == "http": return "" return $origin - of ORIGIN_WHEN_CROSS_ORIGIN: - if origin != target.origin0: + of rpOriginWhenCrossOrigin: + if not origin.isSameOrigin(target.origin): return $origin return $origin & prev.pathname & prev.search - of STRICT_ORIGIN_WHEN_CROSS_ORIGIN: + of rpStrictOriginWhenCrossOrigin: if prev.scheme == "https" and target.scheme == "http": return $origin - if origin != target.origin0: + if not origin.isSameOrigin(target.origin): return $origin return $origin & prev.pathname & prev.search - of UNSAFE_URL: + of rpUnsafeURL: return $origin & prev.pathname & prev.search diff --git a/src/types/url.nim b/src/types/url.nim index abb76f56..585bb64b 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -58,12 +58,21 @@ type blob: Option[BlobURLEntry] searchParams* {.jsget.}: URLSearchParams - Origin* = Option[tuple[ - scheme: string, - host: Host, - port: Option[uint16], + OriginType* = enum + otOpaque, otTuple + + TupleOrigin* = tuple + scheme: string + host: Host + port: Option[uint16] domain: Option[string] - ]] + + Origin* = ref object + case t*: OriginType + of otOpaque: + s: string + of otTuple: + tup: TupleOrigin jsDestructor(URL) jsDestructor(URLSearchParams) @@ -876,9 +885,11 @@ func serialize(host: Host): string = func serialize*(path: URLPath): string {.inline.} = if path.opaque: return path.s + var buf = "" for s in path.ss: - result &= '/' - result &= s + buf &= '/' + buf &= s + return buf when defined(windows) or defined(OS2) or defined(DOS): func serialize_unicode_dos(path: URLPath): string = @@ -1081,7 +1092,7 @@ proc newURL*(s: string; base: Option[string] = none(string)): url.searchParams.initURLSearchParams(url.query.get("")) return ok(url) -proc origin0*(url: URL): Origin = +proc origin*(url: URL): Origin = case url.scheme of "blob": if url.blob.isSome: @@ -1089,33 +1100,43 @@ proc origin0*(url: URL): Origin = discard let pathURL = parseURL($url.path) if pathURL.isNone: - return # opaque - return pathURL.get.origin0 + return Origin(t: otOpaque, s: $url) + return pathURL.get.origin of "ftp", "http", "https", "ws", "wss": - return some((url.scheme, url.host.get, url.port, none(string))) + return Origin( + t: otTuple, + tup: (url.scheme, url.host.get, url.port, none(string)) + ) of "file": - #??? - return # opaque + return Origin(t: otOpaque, s: $url) else: - return # opaque + return Origin(t: otOpaque, s: $url) -proc `==`*(a, b: Origin): bool = - if a.isNone or b.isNone: return false - return a.get == b.get +proc `==`*(a, b: Origin): bool {.error.} = + discard + +proc isSameOrigin*(a, b: Origin): bool = + if a.t != b.t: + return false + case a.t + of otOpaque: + return a.s == b.s + of otTuple: + return a.tup == b.tup proc `$`*(origin: Origin): string = - if origin.isNone: + if origin.t == otOpaque: return "null" - let origin = origin.get - result = origin.scheme - result &= "://" - result &= origin.host.serialize() - if origin.port.isSome: - result &= ':' - result &= $origin.port.get - -proc origin*(url: URL): string {.jsfget.} = - return $url.origin0 + var s = origin.tup.scheme + s &= "://" + s &= origin.tup.host.serialize() + if origin.tup.port.isSome: + s &= ':' + s &= $origin.tup.port.get + return s + +proc jsOrigin*(url: URL): string {.jsfget: "origin".} = + return $url.origin proc protocol*(url: URL): string {.jsfget.} = return url.scheme & ':' diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index c657d15b..3cbcfcd3 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -552,12 +552,12 @@ proc makeCRLF*(s: string): string = else: result &= s[i] -func strictParseEnum*[T: enum](s: string): Opt[T] = +func strictParseEnum*[T: enum](s: string): Option[T] = # cmp when len is small enough, otherwise hashmap when {T.low..T.high}.len <= 4: for e in T.low .. T.high: if $e == s: - return ok(e) + return some(e) else: const tab = (func(): Table[string, T] = result = initTable[string, T]() @@ -565,15 +565,15 @@ func strictParseEnum*[T: enum](s: string): Opt[T] = result[$e] = e )() if s in tab: - return ok(tab[s]) - return err() + return some(tab[s]) + return none(T) -func parseEnumNoCase*[T: enum](s: string): Opt[T] = +func parseEnumNoCase*[T: enum](s: string): Option[T] = # cmp when len is small enough, otherwise hashmap when {T.low..T.high}.len <= 4: for e in T.low .. T.high: if ($e).equalsIgnoreCase(s): - return ok(e) + return some(e) else: const tab = (func(): Table[string, T] = result = initTable[string, T]() @@ -581,8 +581,8 @@ func parseEnumNoCase*[T: enum](s: string): Opt[T] = result[$e] = e )() if s in tab: - return ok(tab[s]) - return err() + return some(tab[s]) + return none(T) proc getContentTypeAttr*(contentType, attrname: string): string = var i = contentType.find(';') |