diff options
author | bptato <nincsnevem662@gmail.com> | 2024-06-08 21:30:32 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-06-08 21:47:19 +0200 |
commit | dee468d502a2d2e0aa6ae2f8365c098f72acc0c9 (patch) | |
tree | 8700f96705e1e9f12122539c81047b43d6992a8b | |
parent | bb0605182a3944418f74e1f5269916d4d0298190 (diff) | |
download | chawan-dee468d502a2d2e0aa6ae2f8365c098f72acc0c9.tar.gz |
pager, buffer: improve forms, protocol config
* refactor form submission * add options to specify form handling per protocol * block cross-protocol POST requests
-rw-r--r-- | doc/config.md | 25 | ||||
-rw-r--r-- | res/config.toml | 9 | ||||
-rw-r--r-- | src/config/config.nim | 21 | ||||
-rw-r--r-- | src/loader/request.nim | 16 | ||||
-rw-r--r-- | src/local/container.nim | 8 | ||||
-rw-r--r-- | src/local/pager.nim | 32 | ||||
-rw-r--r-- | 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.</td> </table> +## 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]`. + +<table> + +<tr> +<th>Name</th> +<th>Value</th> +<th>Function</th> +</tr> + +<tr> +<td>form-request</td> +<td>http, ftp, data, mailto</td> +<td>Specify which protocol to imitate when submitting forms to this +protocol.<br> +Defaults to HTTP.</td> +</tr> + +</table> + ## 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 "<option>" "" -proc getImageHover(styledNode: StyledNode): string = +proc getImageHover(buffer: Buffer; styledNode: StyledNode): string = var styledNode = styledNode while styledNode != nil: if styledNode.t == stElement: @@ -838,7 +838,7 @@ proc updateHover*(buffer: Buffer; cursorx, cursory: int): UpdateHoverResult elem.hover = true repaint = true for ht in HoverType: - let s = HoverFun[ht](thisnode) + let s = HoverFun[ht](buffer, thisnode) if buffer.hoverText[ht] != s: hover.add((ht, s)) buffer.hoverText[ht] = s @@ -1290,124 +1290,104 @@ func pickCharset(form: HTMLFormElement): Charset = return CHARSET_UTF_8 return form.document.charset.getOutputEncoding() +proc getFormRequestType(buffer: Buffer; scheme: string): FormRequestType = + buffer.config.protocol.withValue(scheme, p): + return p[].form_request + return frtHttp + +proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod; + entryList: seq[FormDataEntry]; enctype: FormEncodingType): Request = + assert httpMethod in {hmGet, hmPost} + case buffer.getFormRequestType(parsedAction.scheme) + of frtFtp: + return newRequest(parsedAction) # get action URL + of frtData: + if httpMethod == hmGet: + # mutate action URL + let kvlist = entryList.toNameValuePairs() + #TODO with charset + parsedAction.query = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) + return newRequest(parsedAction, httpMethod) + return newRequest(parsedAction) # get action URL + of frtMailto: + if httpMethod == hmGet: + # mailWithHeaders + let kvlist = entryList.toNameValuePairs() + #TODO with charset + let headers = serializeApplicationXWWWFormUrlEncoded(kvlist, + spaceAsPlus = false) + parsedAction.query = some(headers) + return newRequest(parsedAction, httpMethod) + # mail as body + let kvlist = entryList.toNameValuePairs() + let body = if enctype == fetTextPlain: + let text = serializePlainTextFormData(kvlist) + percentEncode(text, PathPercentEncodeSet) + else: + #TODO with charset + serializeApplicationXWWWFormUrlEncoded(kvlist) + if parsedAction.query.isNone: + parsedAction.query = some("") + if parsedAction.query.get != "": + parsedAction.query.get &= '&' + parsedAction.query.get &= "body=" & body + return newRequest(parsedAction, httpMethod) + of frtHttp: + if httpMethod == hmGet: + # mutate action URL + let kvlist = entryList.toNameValuePairs() + #TODO with charset + let query = serializeApplicationXWWWFormUrlEncoded(kvlist) + parsedAction.query = some(query) + return newRequest(parsedAction, httpMethod) + # submit as entity body + var body: Option[string] + var multipart: Option[FormData] + case enctype + of fetUrlencoded: + #TODO with charset + let kvlist = entryList.toNameValuePairs() + body = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) + of fetMultipart: + #TODO with charset + multipart = some(serializeMultipartFormData(entryList)) + of fetTextPlain: + #TODO with charset + let kvlist = entryList.toNameValuePairs() + body = some(serializePlainTextFormData(kvlist)) + let headers = newHeaders({"Content-Type": $enctype}) + return newRequest(parsedAction, httpMethod, headers, body, multipart) + # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm -proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request] = +proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element): Request = if form.constructingEntryList: - return none(Request) + return nil #TODO submit() let charset = form.pickCharset() discard charset #TODO pass to constructEntryList - let entrylist = form.constructEntryList(submitter) - + let entryList = form.constructEntryList(submitter) let subAction = submitter.action() let action = if subAction != "": subAction else: $form.document.url - #TODO encoding-parse let url = submitter.document.parseURL(action) if url.isNone: - return none(Request) - - var parsedaction = url.get - let scheme = parsedaction.scheme + return nil + let parsedAction = url.get let enctype = submitter.enctype() - let formmethod = submitter.formmethod() - if formmethod == fmDialog: - #TODO - return none(Request) - let httpmethod = if formmethod == fmGet: - hmGet - else: - assert formmethod == fmPost - hmPost - + let formMethod = submitter.formmethod() + let httpMethod = case formMethod + of fmDialog: return nil #TODO + of fmGet: hmGet + of fmPost: hmPost #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): # submitter.attr("formtarget") #else: # submitter.target() #let noopener = true #TODO - - template mutateActionUrl() = - let kvlist = entrylist.toNameValuePairs() - #TODO with charset - let query = serializeApplicationXWWWFormUrlEncoded(kvlist) - parsedaction.query = some(query) - return some(newRequest(parsedaction, httpmethod)) - - template submitAsEntityBody() = - var mimetype: string - var body: Option[string] - var multipart: Option[FormData] - case enctype - of fetUrlencoded: - #TODO with charset - let kvlist = entrylist.toNameValuePairs() - body = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) - mimeType = $enctype - of fetMultipart: - #TODO with charset - multipart = some(serializeMultipartFormData(entrylist)) - mimetype = $enctype - of fetTextPlain: - #TODO with charset - let kvlist = entrylist.toNameValuePairs() - body = some(serializePlainTextFormData(kvlist)) - mimetype = $enctype - let req = newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype}, - body, multipart) - return some(req) #TODO multipart - - template getActionUrl() = - return some(newRequest(parsedaction)) - - template mailWithHeaders() = - let kvlist = entrylist.toNameValuePairs() - #TODO with charset - let headers = serializeApplicationXWWWFormUrlEncoded(kvlist, - spaceAsPlus = false) - parsedaction.query = some(headers) - return some(newRequest(parsedaction, httpmethod)) - - template mailAsBody() = - let kvlist = entrylist.toNameValuePairs() - let body = if enctype == fetTextPlain: - let text = serializePlainTextFormData(kvlist) - percentEncode(text, PathPercentEncodeSet) - else: - #TODO with charset - serializeApplicationXWWWFormUrlEncoded(kvlist) - if parsedaction.query.isNone: - parsedaction.query = some("") - if parsedaction.query.get != "": - parsedaction.query.get &= '&' - parsedaction.query.get &= "body=" & body - return some(newRequest(parsedaction, httpmethod)) - - case scheme - of "ftp", "javascript": - getActionUrl - of "data": - if formmethod == fmGet: - mutateActionUrl - else: - assert formmethod == fmPost - getActionUrl - of "mailto": - if formmethod == fmGet: - mailWithHeaders - else: - assert formmethod == fmPost - mailAsBody - else: - # Note: only http & https are defined by the standard. - # Assume an HTTP-like protocol. - if formmethod == fmGet: - mutateActionUrl - else: - assert formmethod == fmPost - submitAsEntityBody + return buffer.makeFormRequest(parsedAction, httpMethod, entryList, enctype) proc setFocus(buffer: Buffer; e: Element): bool = if buffer.document.focus != e: @@ -1422,10 +1402,10 @@ proc restoreFocus(buffer: Buffer): bool = return true type ReadSuccessResult* = object - open*: Option[Request] + open*: Request repaint*: bool -proc implicitSubmit(input: HTMLInputElement): Option[Request] = +proc implicitSubmit(buffer: Buffer; input: HTMLInputElement): Request = let form = input.form if form != nil and form.canSubmitImplicitly(): var defaultButton: Element @@ -1434,9 +1414,10 @@ proc implicitSubmit(input: HTMLInputElement): Option[Request] = defaultButton = element break if defaultButton != nil: - return submitForm(form, defaultButton) + return buffer.submitForm(form, defaultButton) else: - return submitForm(form, form) + return buffer.submitForm(form, form) + return nil proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult {.proxy.} = @@ -1453,13 +1434,13 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult input.invalid = true buffer.do_reshape() result.repaint = true - result.open = implicitSubmit(input) + result.open = buffer.implicitSubmit(input) else: input.value = s input.invalid = true buffer.do_reshape() result.repaint = true - result.open = implicitSubmit(input) + result.open = buffer.implicitSubmit(input) of TAG_TEXTAREA: let textarea = HTMLTextAreaElement(buffer.document.focus) textarea.value = s @@ -1488,7 +1469,7 @@ type selected*: seq[int] ClickResult* = object - open*: Option[Request] + open*: Request readline*: Option[ReadLineResult] repaint*: bool select*: Option[SelectResult] @@ -1542,26 +1523,20 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult = var repaint = buffer.restoreFocus() let url = parseURL(anchor.href, some(buffer.baseURL)) if url.isSome: - let url = url.get + var url = url.get if url.scheme == "javascript": - if buffer.config.scripting: - let s = buffer.evalJSURL(url) - buffer.do_reshape() - repaint = true - if s.isSome: - let url = newURL("data:text/html," & s.get).get - let req = newRequest(url, hmGet) - return ClickResult( - repaint: repaint, - open: some(req) - ) - return ClickResult( - repaint: repaint - ) - return ClickResult( - repaint: repaint, - open: some(newRequest(url, hmGet)) - ) + if not buffer.config.scripting: + return ClickResult(repaint: repaint) + let s = buffer.evalJSURL(url) + buffer.do_reshape() + repaint = true + if s.isNone: + return ClickResult(repaint: repaint) + let urls = newURL("data:text/html," & s.get) + if urls.isNone: + return ClickResult(repaint: repaint) + url = urls.get + return ClickResult(repaint: repaint, open: newRequest(url, hmGet)) return ClickResult(repaint: repaint) proc click(buffer: Buffer; option: HTMLOptionElement): ClickResult = @@ -1572,10 +1547,10 @@ proc click(buffer: Buffer; option: HTMLOptionElement): ClickResult = proc click(buffer: Buffer; button: HTMLButtonElement): ClickResult = if button.form != nil: - var open = none(Request) + var open: Request = nil case button.ctype of btSubmit: - open = submitForm(button.form, button) + open = buffer.submitForm(button.form, button) of btReset: button.form.reset() buffer.do_reshape() @@ -1651,7 +1626,10 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult = return ClickResult(repaint: false) of itSubmit, itButton: if input.form != nil: - return ClickResult(open: submitForm(input.form, input), repaint: repaint) + return ClickResult( + open: buffer.submitForm(input.form, input), + repaint: repaint + ) return ClickResult(repaint: false) else: # default is text. |