From dee468d502a2d2e0aa6ae2f8365c098f72acc0c9 Mon Sep 17 00:00:00 2001 From: bptato Date: Sat, 8 Jun 2024 21:30:32 +0200 Subject: pager, buffer: improve forms, protocol config * refactor form submission * add options to specify form handling per protocol * block cross-protocol POST requests --- doc/config.md | 25 +++++ res/config.toml | 9 ++ src/config/config.nim | 21 +++- src/loader/request.nim | 16 --- src/local/container.nim | 8 +- src/local/pager.nim | 32 +++--- src/server/buffer.nim | 254 ++++++++++++++++++++++-------------------------- 7 files changed, 192 insertions(+), 173 deletions(-) diff --git a/doc/config.md b/doc/config.md index d7b92723..d3abdb22 100644 --- a/doc/config.md +++ b/doc/config.md @@ -34,6 +34,7 @@ examples. * [External](#external) * [Network](#network) * [Display](#display) +* [Protocol](#protocol) * [Omnirule](#omnirule) * [Siteconf](#siteconf) * [Stylesheets](#stylesheets) @@ -476,6 +477,30 @@ fallback values provided above. +## Protocol + +Protocol-related rules are to be placed in objects keyed as +`[protocol.{protocol-name}]`. e.g. FTP related rules are placed in in +`[protocol.ftp]`. + + + + + + + + + + + + + + + +
NameValueFunction
form-requesthttp, ftp, data, mailtoSpecify which protocol to imitate when submitting forms to this +protocol.
+Defaults to HTTP.
+ ## Omnirule The omni-bar (by default opened with C-l) can be used to perform searches using diff --git a/res/config.toml b/res/config.toml index 4a295099..48eb0e64 100644 --- a/res/config.toml +++ b/res/config.toml @@ -215,6 +215,15 @@ escape = '() => line.escape()' prevHist = '() => line.prevHist()' nextHist = '() => line.nextHist()' +[protocol.ftp] +form-request = "ftp" + +[protocol.mailto] +form-request = "mailto" + +[protocol.data] +form-request = "data" + [search] wrap = true ignore-case = true diff --git a/src/config/config.nim b/src/config/config.nim index 35ca1a17..50173057 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -43,7 +43,13 @@ type ActionMap = object t: Table[string, string] - SiteConfig* = object + FormRequestType* = enum + frtHttp = "http" + frtFtp = "ftp" + frtData = "data" + frtMailto = "mailto" + + SiteConfig* = ref object url*: Option[Regex] host*: Option[Regex] rewrite_url*: Option[JSValueFunction] @@ -59,7 +65,7 @@ type default_headers*: TableRef[string, string] insecure_ssl_no_verify*: Option[bool] - OmniRule* = object + OmniRule* = ref object match*: Regex substitute_url*: Option[JSValueFunction] @@ -131,6 +137,9 @@ type force_pixels_per_column* {.jsgetset.}: bool force_pixels_per_line* {.jsgetset.}: bool + ProtocolConfig* = ref object + form_request*: FormRequestType + Config* = ref object jsctx: JSContext jsvfns*: seq[JSValueFunction] @@ -145,6 +154,7 @@ type input* {.jsget.}: InputConfig display* {.jsget.}: DisplayConfig #TODO getset + protocol*: Table[string, ProtocolConfig] siteconf*: seq[SiteConfig] omnirule*: seq[OmniRule] cmd*: CommandConfig @@ -289,6 +299,8 @@ type ConfigParser = object proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue; k: string) +proc parseConfigValue(ctx: var ConfigParser; x: var ref object; v: TomlValue; + k: string) proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue; k: string) proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue; @@ -365,6 +377,11 @@ proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue; fk ctx.parseConfigValue(fv, v[kebabk], kkk) +proc parseConfigValue(ctx: var ConfigParser; x: var ref object; v: TomlValue; + k: string) = + new(x) + ctx.parseConfigValue(x[], v, k) + proc parseConfigValue[U, V](ctx: var ConfigParser; x: var Table[U, V]; v: TomlValue; k: string) = typeCheck(v, tvtTable, k) diff --git a/src/loader/request.nim b/src/loader/request.nim index fc6c43f5..277481f1 100644 --- a/src/loader/request.nim +++ b/src/loader/request.nim @@ -115,22 +115,6 @@ func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders(); suspended: suspended ) -func newRequest*(url: URL; httpMethod = hmGet; - headers: seq[(string, string)] = @[]; body = none(string); - multipart = none(FormData); proxy: URL = nil): Request = - let hl = newHeaders() - for pair in headers: - let (k, v) = pair - hl.table[k] = @[v] - return newRequest( - url, - httpMethod, - hl, - body, - multipart, - proxy = proxy - ) - func createPotentialCORSRequest*(url: URL; destination: RequestDestination; cors: CORSAttribute; fallbackFlag = false): JSRequest = var mode = if cors == caNoCors: diff --git a/src/local/container.nim b/src/local/container.nim index 8439b32a..90e00ebb 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -1489,8 +1489,8 @@ proc readSuccess*(container: Container; s: string; fd = -1) = p.then(proc(res: ReadSuccessResult) = if res.repaint: container.needslines = true - if res.open.isSome: - container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get)) + if res.open != nil: + container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open)) ) proc reshape(container: Container): EmptyPromise {.jsfunc.} = @@ -1513,10 +1513,10 @@ proc displaySelect(container: Container; selectResult: SelectResult) = proc onclick(container: Container; res: ClickResult; save: bool) = if res.repaint: container.needslines = true - if res.open.isSome: + if res.open != nil: container.triggerEvent(ContainerEvent( t: cetOpen, - request: res.open.get, + request: res.open, save: save )) if res.select.isSome and not save: diff --git a/src/local/pager.nim b/src/local/pager.nim index 8eb42b65..48856837 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -98,7 +98,6 @@ type LineDataAuth = ref object of LineData url: URL - username: string NavDirection = enum ndPrev = "prev" @@ -124,6 +123,7 @@ type devRandom: PosixStream display: FixedGrid forkserver*: ForkServer + formRequestMap*: Table[string, FormRequestType] hasload*: bool # has a page been successfully loaded since startup? inputBuffer*: string # currently uninterpreted characters iregex: Result[Regex, string] @@ -1026,6 +1026,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; images: images, isdump: pager.config.start.headless, charsetOverride: charsetOverride, + protocol: pager.config.protocol ) # Load request in a new buffer. @@ -1231,18 +1232,13 @@ proc updateReadLine*(pager: Pager) = case pager.linemode of lmLocation: pager.loadURL(lineedit.news) of lmUsername: - LineDataAuth(pager.lineData).username = lineedit.news + LineDataAuth(pager.lineData).url.username = lineedit.news pager.setLineEdit(lmPassword, hide = true) of lmPassword: - let data = LineDataAuth(pager.lineData) - let url = newURL(data.url) - url.username = data.username + let url = LineDataAuth(pager.lineData).url url.password = lineedit.news - pager.gotoURL( - newRequest(url), some(pager.container.url), - replace = pager.container, - referrer = pager.container - ) + pager.gotoURL(newRequest(url), some(pager.container.url), + replace = pager.container, referrer = pager.container) pager.lineData = nil of lmCommand: pager.scommand = lineedit.news @@ -1676,6 +1672,9 @@ proc redirect(pager: Pager; container: Container; response: Response; container.url.scheme == "http" and request.url.scheme == "https" or container.url.scheme == "https" and request.url.scheme == "http": pager.redirectTo(container, request) + #TODO perhaps make following behavior configurable? + elif request.url.scheme == "cgi-bin": + pager.alert("Blocked redirection attempt to " & $request.url) else: let url = request.url pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) = @@ -1708,7 +1707,7 @@ proc connected(pager: Pager; container: Container; response: Response) = container.applyResponse(response, pager.config.external.mime_types) if response.status == 401: # unauthorized pager.setLineEdit(lmUsername) - pager.lineData = LineDataAuth(url: container.url) + pager.lineData = LineDataAuth(url: newURL(container.url)) istream.sclose() return # This forces client to ask for confirmation before quitting. @@ -1863,8 +1862,15 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): pager.setLineEdit(lmBufferFile, "") of cetOpen: let url = event.request.url - if not event.save and (pager.container != container or - not container.isHoverURL(url)): + let sameScheme = container.url.scheme == url.scheme + if event.request.httpMethod != hmGet and (not sameScheme or + container.url.scheme in ["http", "https"] and + url.scheme in ["http", "https"]): + pager.alert("Blocked cross-scheme POST: " & $url) + return + #TODO this is horrible UX, async actions shouldn't block input + if pager.container != container or + not event.save and not container.isHoverURL(url): pager.ask("Open pop-up? " & $url).then(proc(x: bool) = if x: pager.gotoURL(event.request, some(container.url), diff --git a/src/server/buffer.nim b/src/server/buffer.nim index e471c1ae..c2ed2fb7 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -14,7 +14,6 @@ import config/config import css/cascade import css/cssparser import css/cssvalues -import css/mediaquery import css/sheet import css/stylednode import html/catom @@ -48,7 +47,6 @@ import monoucha/tojs import types/blob import types/cell import types/color -import types/cookie import types/formdata import types/opt import types/url @@ -146,6 +144,7 @@ type isdump*: bool charsets*: seq[Charset] charsetOverride*: Charset + protocol*: Table[string, ProtocolConfig] proc getFromOpaque[T](opaque: pointer; res: var T) = let opaque = cast[InterfaceOpaque](opaque) @@ -300,7 +299,7 @@ macro task(fun: typed) = pfun.istask = true fun -func getTitleAttr(node: StyledNode): string = +func getTitleAttr(buffer: Buffer; node: StyledNode): string = if node == nil: return "" if node.t == stElement and node.node != nil: @@ -336,7 +335,8 @@ func getClickable(styledNode: StyledNode): Element = return Element(styledNode.node) styledNode = styledNode.parent -proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request] +proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element): + Request func canSubmitOnClick(fae: FormAssociatedElement): bool = if fae.form == nil: @@ -350,7 +350,7 @@ func canSubmitOnClick(fae: FormAssociatedElement): bool = return true return false -proc getClickHover(styledNode: StyledNode): string = +proc getClickHover(buffer: Buffer; styledNode: StyledNode): string = let clickable = styledNode.getClickable() if clickable != nil: if clickable of HTMLAnchorElement: @@ -359,15 +359,15 @@ proc getClickHover(styledNode: StyledNode): string = #TODO this is inefficient and also quite stupid let fae = FormAssociatedElement(clickable) if fae.canSubmitOnClick(): - let req = fae.form.submitForm(fae) - if req.isSome: - return $req.get.url + let req = buffer.submitForm(fae.form, fae) + if req != nil: + return $req.url return "<" & $clickable.tagType & ">" elif clickable of HTMLOptionElement: return "