diff options
-rw-r--r-- | bonus/config.toml | 15 | ||||
-rw-r--r-- | src/buffer/buffer.nim | 8 | ||||
-rw-r--r-- | src/buffer/container.nim | 25 | ||||
-rw-r--r-- | src/config/config.nim | 115 | ||||
-rw-r--r-- | src/config/toml.nim | 1 | ||||
-rw-r--r-- | src/display/client.nim | 8 | ||||
-rw-r--r-- | src/display/pager.nim | 59 | ||||
-rw-r--r-- | src/display/term.nim | 35 | ||||
-rw-r--r-- | src/io/loader.nim | 7 | ||||
-rw-r--r-- | src/io/request.nim | 43 | ||||
-rw-r--r-- | src/ips/forkserver.nim | 14 | ||||
-rw-r--r-- | src/ips/serialize.nim | 20 | ||||
-rw-r--r-- | src/types/cookie.nim | 48 |
13 files changed, 296 insertions, 102 deletions
diff --git a/bonus/config.toml b/bonus/config.toml new file mode 100644 index 00000000..39e6dc52 --- /dev/null +++ b/bonus/config.toml @@ -0,0 +1,15 @@ +[[omnirule]] +match = '^ddg:' +substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)' + +[[omnirule]] +match = '^wk:' +substitute-url = '(x) => "https://en.wikipedia.org/wiki/Special:Search?search=" + x.substring(4)' + +[[siteconf]] +url = '/^https?:\/\/(www\.)?npr\.org/' +rewrite-url = '(x) => x.pathname = x.pathname.replace(/(.*)\/.*/, "$1").replace(/.*\//, "")' + +[[siteconf]] +url = "^https://news.ycombinator.com.*" +cookie = true diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 9f699ca3..b692e253 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -36,6 +36,7 @@ import render/renderdocument import render/rendertext import types/buffersource import types/color +import types/cookie import types/url import utils/twtstr @@ -528,7 +529,7 @@ proc loadResources(buffer: Buffer, document: Document) = for child in elem.children_rev: stack.add(child) -type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string] +type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string, cookies: seq[Cookie]] proc setupSource(buffer: Buffer): ConnectResult = if buffer.loaded: @@ -564,6 +565,11 @@ proc setupSource(buffer: Buffer): ConnectResult = SocketStream(buffer.istream).source.getFd().setBlocking(false) result.needsAuth = response.status == 401 # Unauthorized result.redirect = response.redirect + if "Set-Cookie" in response.headers.table: + for s in response.headers.table["Set-Cookie"]: + let cookie = newCookie(s) + if cookie != nil: + result.cookies.add(cookie) buffer.istream = newTeeStream(buffer.istream, buffer.sstream, closedest = false) if setct: result.contentType = buffer.contenttype diff --git a/src/buffer/container.nim b/src/buffer/container.nim index 83bb6c8c..a148e0e1 100644 --- a/src/buffer/container.nim +++ b/src/buffer/container.nim @@ -1,3 +1,4 @@ +import deques import options import streams import strformat @@ -17,6 +18,7 @@ import ips/socketstream import js/javascript import js/regex import types/buffersource +import types/cookie import types/dispatcher import types/url import utils/twtstr @@ -32,7 +34,8 @@ type ContainerEventType* = enum NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE, - READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED + READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED, + SET_COOKIE ContainerEvent* = object case t*: ContainerEventType @@ -52,6 +55,8 @@ type msg*: string of UPDATE: force*: bool + of SET_COOKIE: + cookies*: seq[Cookie] else: discard Highlight* = ref object @@ -61,8 +66,9 @@ type clear*: bool Container* = ref object + config: BufferConfig iface*: BufferInterface - attrs*: WindowAttributes + attrs: WindowAttributes width*: int height*: int contenttype*: Option[string] @@ -88,24 +94,25 @@ type redraw*: bool needslines*: bool canceled: bool - events*: seq[ContainerEvent] + events*: Deque[ContainerEvent] startpos: Option[CursorPosition] hasstart: bool -proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container = +proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, cookiejar: CookieJar, title = ""): Container = let attrs = getWindowAttributes(stdout) + let config = config.getBufferConfig(source.location, cookiejar) let ostream = dispatcher.forkserver.ostream let istream = dispatcher.forkserver.istream ostream.swrite(FORK_BUFFER) ostream.swrite(source) - ostream.swrite(config.getBufferConfig(source.location)) + ostream.swrite(config) ostream.swrite(attrs) ostream.swrite(dispatcher.mainproc) ostream.flush() result = Container( source: source, attrs: attrs, width: attrs.width, height: attrs.height - 1, contenttype: source.contenttype, - title: title + title: title, config: config ) istream.sread(result.process) result.pos.setx = -1 @@ -250,7 +257,7 @@ func findHighlights*(container: Container, y: int): seq[Highlight] = result.add(hl) proc triggerEvent(container: Container, event: ContainerEvent) = - container.events.add(event) + container.events.addLast(event) proc triggerEvent(container: Container, t: ContainerEventType) = container.triggerEvent(ContainerEvent(t: t)) @@ -638,6 +645,8 @@ proc load*(container: Container) = container.code = res.code if res.code == 0: container.triggerEvent(SUCCESS) + if res.cookies.len > 0 and container.config.cookiejar != nil: # accept cookies + container.triggerEvent(ContainerEvent(t: SET_COOKIE, cookies: res.cookies)) container.setLoadInfo("Connected to " & $container.source.location & ". Downloading...") if res.needsAuth: container.triggerEvent(NEEDS_AUTH) @@ -690,7 +699,7 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l contenttype: if contenttype.isSome: contenttype else: container.contenttype, clonepid: container.process, ) - container.pipeto = dispatcher.newBuffer(config, source, container.title) + container.pipeto = dispatcher.newBuffer(config, source, container.config.cookiejar, container.title) container.iface.getSource().then(proc() = if container.pipeto != nil: container.pipeto.load() diff --git a/src/config/config.nim b/src/config/config.nim index cb42d6af..73072763 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -5,10 +5,12 @@ import streams import buffer/cell import config/toml +import io/request import io/urlfilter import js/javascript import js/regex import types/color +import types/cookie import types/url import utils/twtstr @@ -21,15 +23,28 @@ type ActionMap = Table[string, string] StaticSiteConfig = object - url: string + url: Option[string] + host: Option[string] subst: Option[string] + cookie: bool + + StaticOmniRule = object + match: string + subst: string SiteConfig* = object - url*: Regex + url*: Option[Regex] + host*: Option[Regex] + subst*: (proc(s: URL): Option[URL]) + cookie*: bool + + OmniRule* = object + match*: Regex subst*: (proc(s: string): Option[string]) Config* = ref ConfigObj ConfigObj* = object + termreload*: bool nmap*: ActionMap lemap*: ActionMap stylesheet*: string @@ -45,6 +60,7 @@ type editor*: string tmpdir*: string siteconf: seq[StaticSiteConfig] + omnirules: seq[StaticOmniRule] forceclear*: bool emulateoverline*: bool visualhome*: string @@ -52,6 +68,8 @@ type BufferConfig* = object userstyle*: string filter*: URLFilter + cookiejar*: CookieJar + headers*: HeaderList ForkServerConfig* = object tmpdir*: string @@ -63,20 +81,36 @@ func getForkServerConfig*(config: Config): ForkServerConfig = ambiguous_double: config.ambiguous_double ) -func getBufferConfig*(config: Config, location: URL): BufferConfig = +func getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar): BufferConfig = result.userstyle = config.stylesheet result.filter = newURLFilter(scheme = some(location.scheme)) + result.cookiejar = cookiejar proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] = for sc in config.siteconf: + var conf = SiteConfig( + cookie: sc.cookie, + ) + if sc.url.isSome: + conf.url = compileRegex(sc.url.get, 0) + elif sc.host.isSome: + conf.host = compileRegex(sc.host.get, 0) if sc.subst.isSome: - let re = compileRegex(sc.url, 0) let fun = jsctx.eval(sc.subst.get, "<siteconf>", JS_EVAL_TYPE_GLOBAL) - let f = getJSFunction[string, string](jsctx, fun.val) - result.add(SiteConfig( - url: re.get, - subst: f.get - )) + let f = getJSFunction[URL, URL](jsctx, fun.val) + conf.subst = f.get + result.add(conf) + +proc getOmniRules*(config: Config, jsctx: JSContext): seq[OmniRule] = + for rule in config.omnirules: + let re = compileRegex(rule.match, 0) + var conf = OmniRule( + match: re.get + ) + let fun = jsctx.eval(rule.subst, "<siteconf>", JS_EVAL_TYPE_GLOBAL) + let f = getJSFunction[string, string](jsctx, fun.val) + conf.subst = f.get + result.add(conf) func getRealKey(key: string): string = var realk: string @@ -150,9 +184,50 @@ proc readUserStylesheet(dir, file: string): string = result = f.readAll() f.close() +proc parseConfig(config: Config, dir: string, stream: Stream) +proc parseConfig*(config: Config, dir: string, s: string) + +proc loadConfig*(config: Config, s: string) {.jsfunc.} = + let s = if s.len > 0 and s[0] == '/': + s + else: + getCurrentDir() / s + if not fileExists(s): return + config.parseConfig(parentDir(s), newFileStream(s)) + +proc bindPagerKey*(config: Config, key, action: string) {.jsfunc.} = + let k = getRealKey(key) + config.nmap[k] = action + var teststr = "" + for c in k: + teststr &= c + if teststr notin config.nmap: + config.nmap[teststr] = "client.feedNext()" + +proc bindLineKey*(config: Config, key, action: string) {.jsfunc.} = + let k = getRealKey(key) + config.lemap[k] = action + var teststr = "" + for c in k: + teststr &= c + if teststr notin config.nmap: + config.lemap[teststr] = "client.feedNext()" + proc parseConfig(config: Config, dir: string, t: TomlValue) = for k, v in t: case k + of "include": + if v.vt == VALUE_STRING: + when nimvm: + config.loadConfig(v.s) + else: + config.loadConfig(v.s) + elif t.vt == VALUE_ARRAY: + for v in t.a: + when nimvm: + config.parseConfig(parentDir(v.s), staticRead(v.s)) + else: + config.parseConfig(parentDir(v.s), newFileStream(v.s)) of "start": for k, v in v: case k @@ -233,10 +308,21 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) = var conf = StaticSiteConfig() for k, v in v: case k - of "url": conf.url = v.s - of "substitute_url": conf.subst = some(v.s) - if conf.url != "": - config.siteconf.add(conf) + of "url": conf.url = some(v.s) + of "host": conf.host = some(v.s) + of "rewrite-url": conf.subst = some(v.s) + of "cookie": conf.cookie = v.b + assert conf.url.isSome != conf.host.isSome + config.siteconf.add(conf) + of "omnirule": + for v in v: + var rule = StaticOmniRule() + for k, v in v: + case k + of "match": rule.match = v.s + of "substitute-url": rule.subst = v.s + if rule.match != "": + config.omnirules.add(rule) proc parseConfig(config: Config, dir: string, stream: Stream) = config.parseConfig(dir, parseToml(stream)) @@ -272,3 +358,6 @@ proc readConfig*(): Config = when defined(debug): result.readConfig(getCurrentDir() / "res") result.readConfig(getConfigDir() / "chawan") + +proc addConfigModule*(ctx: JSContext) = + ctx.registerType(Config) diff --git a/src/config/toml.nim b/src/config/toml.nim index c5a93b8a..2c4550b5 100644 --- a/src/config/toml.nim +++ b/src/config/toml.nim @@ -447,5 +447,6 @@ proc parseToml*(inputStream: Stream): TomlValue = state.consumeComment() of '\t', ' ': discard else: state.syntaxError(fmt"invalid character after value: {c}") + inputStream.close() return TomlValue(vt: VALUE_TABLE, t: state.root) diff --git a/src/display/client.nim b/src/display/client.nim index 7bf9a278..90945968 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -48,7 +48,7 @@ type pager {.jsget.}: Pager line {.jsget.}: LineEdit sevent: seq[Container] - config: Config + config {.jsget.}: Config jsrt: JSRuntime jsctx: JSContext timeoutid: int @@ -466,7 +466,9 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client = result.jsrt.setInterruptHandler(interruptHandler, cast[pointer](result)) let ctx = result.jsrt.newJSContext() result.jsctx = ctx - result.pager = newPager(config, result.attrs, dispatcher, result.config.getSiteConfig(ctx)) + result.pager = newPager(config, result.attrs, dispatcher, + result.config.getSiteConfig(ctx), + result.config.getOmniRules(ctx)) var global = ctx.getGlobalObject() ctx.registerType(Client, asglobal = true) global.setOpaque(result) @@ -481,5 +483,7 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client = ctx.addHTMLModule() ctx.addRequestModule() ctx.addLineEditModule() + ctx.addConfigModule() ctx.addPagerModule() ctx.addContainerModule() + ctx.addConfigModule() diff --git a/src/display/pager.nim b/src/display/pager.nim index 765e1e73..06b28346 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -1,3 +1,4 @@ +import deques import net import options import os @@ -22,6 +23,7 @@ import js/javascript import js/regex import types/buffersource import types/color +import types/cookie import types/dispatcher import types/url import utils/twtstr @@ -55,6 +57,8 @@ type term*: Terminal linehist: array[LineMode, LineHistory] siteconf: seq[SiteConfig] + omnirules: seq[OmniRule] + cookiejars: Table[string, CookieJar] func attrs(pager: Pager): WindowAttributes = pager.term.attrs @@ -147,7 +151,7 @@ proc isearchBackward(pager: Pager) {.jsfunc.} = pager.container.pushCursorPos() pager.setLineEdit("?", ISEARCH_B) -proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, siteconf: seq[SiteConfig]): Pager = +proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, siteconf: seq[SiteConfig], omnirules: seq[OmniRule]): Pager = let pager = Pager( dispatcher: dispatcher, config: config, @@ -156,9 +160,13 @@ proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, term: newTerminal(stdout, config, attrs), ) for sc in siteconf: - # not sure why but normal copies don't seem to work here... + # not sure why but normal copies don't seem to work here... probably + # something weird going on with lambdas pager.siteconf.add(sc) pager.siteconf[^1].subst = sc.subst + for rule in omnirules: + pager.omnirules.add(rule) + pager.omnirules[^1].subst = rule.subst return pager proc launchPager*(pager: Pager, tty: File) = @@ -304,7 +312,7 @@ proc draw*(pager: Pager) = pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) pager.term.outputGrid() if pager.lineedit.isSome: - pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.container.attrs.height - 1) + pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1) else: pager.term.setCursor(pager.container.acursorx, pager.container.acursory) pager.term.showCursor() @@ -459,21 +467,24 @@ proc windowChange*(pager: Pager, attrs: WindowAttributes) = for container in pager.containers: container.windowChange(attrs) -# ugh... -proc substituteUrl(pager: Pager, request: Request) = - let surl = $request.url +proc applySiteconf(pager: Pager, request: Request) = + let url = $request.url + let host = $request.url.host for sc in pager.siteconf: - if sc.url.exec(surl).success: - let s = sc.subst(surl) - if s.isSome: - let nurl = parseURL(s.get) - if nurl.isSome: - request.url = nurl.get - break + if sc.url.isSome and not sc.url.get.exec(url).success: + continue + elif sc.host.isSome and not sc.host.get.exec(host).success: + continue + if sc.subst != nil: + let s = sc.subst(request.url) + if s.isSome and s.get != nil: + request.url = s.get + if sc.cookie and request.url.host notin pager.cookiejars: + pager.cookiejars[request.url.host] = newCookieJar(request.url) # Load request in a new buffer. proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) = - pager.substituteUrl(request) + pager.applySiteconf(request) if prevurl.isnone or not prevurl.get.equals(request.url, true) or request.url.hash == "" or request.httpmethod != HTTP_GET: # Basically, we want to reload the page *only* when @@ -488,7 +499,8 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none( contenttype: ctype, location: request.url ) - let container = pager.dispatcher.newBuffer(pager.config, source) + let cookiejar = pager.cookiejars.getOrDefault(request.url.host) + let container = pager.dispatcher.newBuffer(pager.config, source, cookiejar) if replace != nil: container.replace = replace container.copyCursorPos(container.replace) @@ -497,6 +509,12 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none( else: pager.container.findAnchor(request.url.anchor) +proc omniRewrite(pager: Pager, s: string): string = + for rule in pager.omnirules: + if rule.match.exec(s).success: + return rule.subst(s).get + return s + # When the user has passed a partial URL as an argument, they might've meant # either: # * file://$PWD/<file> @@ -504,6 +522,8 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none( # So we attempt to load both, and see what works. # (TODO: make this optional) proc loadURL*(pager: Pager, url: string, ctype = none(string)) = + let url0 = pager.omniRewrite(url) + let url = if url[0] == '~': expandPath(url0) else: url0 let firstparse = parseURL(url) if firstparse.issome: let prev = if pager.container != nil: @@ -518,7 +538,6 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string)) = if pageurl.isSome: # attempt to load remote page urls.add(pageurl.get) let cdir = parseURL("file://" & getCurrentDir() & DirSep) - let url = if url[0] == '~': expandPath(url) else: url let purl = percentEncode(url, LocalPathPercentEncodeSet) if purl != url: let newurl = parseURL(purl, cdir) @@ -542,7 +561,7 @@ proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: O contenttype: some(ctype.get("text/plain")), location: location.get(newURL("file://-")) ) - let container = pager.dispatcher.newBuffer(pager.config, source, title) + let container = pager.dispatcher.newBuffer(pager.config, source, nil, title) return container proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) = @@ -726,12 +745,16 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo if pager.container == container: pager.alert(event.msg) pager.refreshStatusMsg() + of SET_COOKIE: + let host = container.source.location.host + if host in pager.cookiejars: + pager.cookiejars[host].cookies.add(event.cookies) of NO_EVENT: discard return true proc handleEvents*(pager: Pager, container: Container): bool = while container.events.len > 0: - let event = container.events.pop() + let event = container.events.popFirst() if not pager.handleEvent0(container, event): return false return true diff --git a/src/display/term.nim b/src/display/term.nim index 073b5dfc..247f356d 100644 --- a/src/display/term.nim +++ b/src/display/term.nim @@ -386,7 +386,26 @@ proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) = cell.format.fgcolor = grid[(ly - y) * grid.width + (lx - x)].format.fgcolor j += cell[].width() +proc applyConfig(term: Terminal) = + if term.config.colormode.isSome: + term.colormode = term.config.colormode.get + elif term.isatty(): + term.colormode = ANSI + let colorterm = getEnv("COLORTERM") + case colorterm + of "24bit", "truecolor": term.colormode = TRUE_COLOR + if term.config.formatmode.isSome: + term.formatmode = term.config.formatmode.get + for fm in FormatFlags: + if fm in term.config.noformatmode: + term.formatmode.excl(fm) + if term.isatty() and term.config.altscreen.isSome: + term.smcup = term.config.altscreen.get + term.mincontrast = term.config.mincontrast + proc outputGrid*(term: Terminal) = + if term.config.termreload: + term.applyConfig() term.outfile.write(term.resetFormat()) if term.config.forceclear or not term.cleared: term.outfile.write(term.generateFullOutput(term.canvas)) @@ -488,21 +507,7 @@ proc detectTermAttributes(term: Terminal) = if term.isatty(): term.smcup = true term.formatmode = {low(FormatFlags)..high(FormatFlags)} - if term.config.colormode.isSome: - term.colormode = term.config.colormode.get - elif term.isatty(): - term.colormode = ANSI - let colorterm = getEnv("COLORTERM") - case colorterm - of "24bit", "truecolor": term.colormode = TRUE_COLOR - if term.config.formatmode.isSome: - term.formatmode = term.config.formatmode.get - for fm in FormatFlags: - if fm in term.config.noformatmode: - term.formatmode.excl(fm) - if term.isatty() and term.config.altscreen.isSome: - term.smcup = term.config.altscreen.get - term.mincontrast = term.config.mincontrast + term.applyConfig() proc start*(term: Terminal, infile: File) = term.infile = infile diff --git a/src/io/loader.nim b/src/io/loader.nim index dbca256b..671b9ca3 100644 --- a/src/io/loader.nim +++ b/src/io/loader.nim @@ -26,6 +26,7 @@ import io/urlfilter import ips/serialize import ips/serversocket import ips/socketstream +import types/cookie import types/mime import types/url import utils/twtstr @@ -84,7 +85,7 @@ proc loadResource(request: Request, ostream: Stream) = ostream.flush() var ssock: ServerSocket -proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter) = +proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter, cookiejar: CookieJar) = if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: raise newException(Defect, "Failed to initialize libcurl.") ssock = initServerSocket() @@ -116,6 +117,10 @@ proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter) = for k, v in defaultHeaders.table: if k notin request.headers.table: request.headers.table[k] = v + if cookiejar != nil and cookiejar.cookies.len > 0: + if request.url.host == cookiejar.location.host: + if "Cookie" notin request.headers.table: + request.headers["Cookie"] = $cookiejar loadResource(request, stream) stream.close() of QUIT: diff --git a/src/io/request.nim b/src/io/request.nim index 9332f9ff..7587b2e0 100644 --- a/src/io/request.nim +++ b/src/io/request.nim @@ -128,30 +128,29 @@ func newHeaderList*(table: Table[string, string]): HeaderList = else: result.table[k] = @[v] -func newRequest*(url: Url, - httpmethod = HTTP_GET, - headers: seq[(string, string)] = @[], - body = none(string), +func newRequest*(url: Url, httpmethod: HttpMethod, headers: HeaderList, + body = none(string), multipart = none(MimeData)): Request = + return Request( + url: url, + httpmethod: httpmethod, + headers: headers, + body: body, + multipart: multipart + ) + +func newRequest*(url: Url, httpmethod = HTTP_GET, + headers: seq[(string, string)] = @[], body = none(string), multipart = none(MimeData)): Request {.jsctor.} = - new(result) - result.httpmethod = httpmethod - result.url = url - result.headers = newHeaderList() - for it in headers: - if it[1] != "": #TODO not sure if this is a good idea, options would probably work better - result.headers.table[it[0]] = @[it[1]] - result.body = body - result.multipart = multipart - -func newRequest*(url: Url, - httpmethod: HttpMethod, - headers: openarray[(string, string)], - body = none(string), + let hl = newHeaderList() + for pair in headers: + let (k, v) = pair + hl.table[k] = @[v] + return newRequest(url, httpmethod, hl, body, multipart) + +func newRequest*(url: Url, httpmethod: HttpMethod, + headers: openarray[(string, string)], body = none(string), multipart = none(MimeData)): Request = - var s: seq[(string, string)] - for it in headers: - s.add(it) - return newRequest(url, httpmethod, s, body, multipart) + return newRequest(url, httpmethod, @headers, body, multipart) proc `[]=`*(multipart: var MimeData, k, v: string) = multipart.content.add(MimePart(name: k, content: v)) diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim index eb850ea0..e8904354 100644 --- a/src/ips/forkserver.nim +++ b/src/ips/forkserver.nim @@ -12,6 +12,7 @@ import io/window import ips/serialize import ips/serversocket import types/buffersource +import types/cookie import utils/twtstr type @@ -28,10 +29,11 @@ type ostream: Stream children: seq[(Pid, Pid)] -proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = DefaultHeaders, filter = newURLFilter()): FileLoader = +proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = DefaultHeaders, filter = newURLFilter(), cookiejar: CookieJar = nil): FileLoader = forkserver.ostream.swrite(FORK_LOADER) forkserver.ostream.swrite(defaultHeaders) forkserver.ostream.swrite(filter) + forkserver.ostream.swrite(cookiejar) forkserver.ostream.flush() forkserver.istream.sread(result) @@ -45,7 +47,7 @@ proc removeChild*(forkserver: Forkserver, pid: Pid) = forkserver.ostream.swrite(pid) forkserver.ostream.flush() -proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: URLFilter): FileLoader = +proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: URLFilter, cookiejar: CookieJar): FileLoader = var pipefd: array[2, cint] if pipe(pipefd) == -1: raise newException(Defect, "Failed to open pipe.") @@ -56,7 +58,7 @@ proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: ctx.children.setLen(0) zeroMem(addr ctx, sizeof(ctx)) discard close(pipefd[0]) # close read - runFileLoader(pipefd[1], defaultHeaders, filter) + runFileLoader(pipefd[1], defaultHeaders, filter, cookiejar) assert false let readfd = pipefd[0] # get read discard close(pipefd[1]) # close write @@ -77,7 +79,7 @@ proc forkBuffer(ctx: var ForkServerContext): Pid = ctx.istream.sread(config) ctx.istream.sread(attrs) ctx.istream.sread(mainproc) - let loader = ctx.forkLoader(DefaultHeaders, config.filter) #TODO make this configurable + let loader = ctx.forkLoader(DefaultHeaders, config.filter, config.cookiejar) #TODO make this configurable let pid = fork() if pid == 0: for i in 0 ..< ctx.children.len: ctx.children[i] = (Pid(0), Pid(0)) @@ -109,9 +111,11 @@ proc runForkServer() = of FORK_LOADER: var defaultHeaders: HeaderList var filter: URLFilter + var cookiejar: CookieJar ctx.istream.sread(defaultHeaders) ctx.istream.sread(filter) - let loader = ctx.forkLoader(defaultHeaders, filter) + ctx.istream.sread(cookiejar) + let loader = ctx.forkLoader(defaultHeaders, filter, cookiejar) ctx.ostream.swrite(loader) ctx.children.add((loader.process, Pid(-1))) of LOAD_CONFIG: diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim index 73320b27..ed35371a 100644 --- a/src/ips/serialize.nim +++ b/src/ips/serialize.nim @@ -139,7 +139,10 @@ func slen*(b: bool): int = return sizeof(uint8) proc swrite*(stream: Stream, url: Url) = - stream.swrite(url.serialize()) + if url != nil: + stream.swrite(url.serialize()) + else: + stream.swrite("") proc sread*(stream: Stream, url: var Url) = var s: string @@ -215,14 +218,21 @@ func slen*(obj: object): int = result += slen(f) proc swrite*(stream: Stream, obj: ref object) = - stream.swrite(obj[]) + stream.swrite(obj != nil) + if obj != nil: + stream.swrite(obj[]) proc sread*(stream: Stream, obj: var ref object) = - new(obj) - stream.sread(obj[]) + var n: bool + stream.sread(n) + if n: + new(obj) + stream.sread(obj[]) func slen*(obj: ref object): int = - slen(obj[]) + result = slen(obj != nil) + if obj != nil: + result += slen(obj[]) proc swrite*(stream: Stream, part: MimePart) = stream.swrite(part.isFile) diff --git a/src/types/cookie.nim b/src/types/cookie.nim index b0578e44..807c494f 100644 --- a/src/types/cookie.nim +++ b/src/types/cookie.nim @@ -3,18 +3,24 @@ import strutils import times import js/javascript +import types/url import utils/twtstr -type Cookie = ref object of RootObj - name {.jsget.}: string - value {.jsget.}: string - expires {.jsget.}: int64 # unix time - maxAge {.jsget.}: int64 - secure {.jsget.}: bool - httponly {.jsget.}: bool - samesite {.jsget.}: bool - domain {.jsget.}: string - path {.jsget.}: string +type + Cookie* = ref object + name {.jsget.}: string + value {.jsget.}: string + expires {.jsget.}: int64 # unix time + maxAge {.jsget.}: int64 + secure {.jsget.}: bool + httponly {.jsget.}: bool + samesite {.jsget.}: bool + domain {.jsget.}: string + path {.jsget.}: string + + CookieJar* = ref object + location*: URL + cookies*: seq[Cookie] proc parseCookieDate(val: string): Option[DateTime] = # cookie-date @@ -101,7 +107,19 @@ proc parseCookieDate(val: string): Option[DateTime] = var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) return some(dateTime) -proc newCookie(str: string): Cookie {.jsctor.} = +proc `$`*(cookiejar: CookieJar): string = + let t = now().toTime().toUnix() + for i in countdown(cookiejar.cookies.high, 0): + let cookie = cookiejar.cookies[i] + if cookie.expires <= t: + cookiejar.cookies.delete(i) + else: + result.percentEncode(cookie.name, UserInfoPercentEncodeSet) + result &= "=" + result.percentEncode(cookie.value, UserInfoPercentEncodeSet) + result &= ";" + +proc newCookie*(str: string): Cookie {.jsctor.} = let cookie = new(Cookie) var first = true for part in str.split(';'): @@ -125,7 +143,8 @@ proc newCookie(str: string): Cookie {.jsctor.} = let date = parseCookieDate(val) if date.issome: cookie.expires = date.get.toTime().toUnix() - of "max-age": cookie.maxAge = parseInt64(val) + of "max-age": + cookie.expires = now().toTime().toUnix() + parseInt64(val) of "secure": cookie.secure = true of "httponly": cookie.httponly = true of "samesite": cookie.samesite = true @@ -133,5 +152,10 @@ proc newCookie(str: string): Cookie {.jsctor.} = of "domain": cookie.domain = val return cookie +proc newCookieJar*(location: URL): CookieJar = + return CookieJar( + location: location + ) + proc addCookieModule*(ctx: JSContext) = ctx.registerType(Cookie) |