uri
nimony/lib/std/uri.nim
type Uri = object scheme: string username: string password: string hostname: string port: string path: string query: string anchor: string opaque: bool isIpv6: bool
func initUri(isIpv6: bool): Urifunc addEncoded(dest: var string; src: openArray; usePlus: bool)Append
srcpercent-encoded.usePluswrites a space as+, which is theapplication/x-www-form-urlencodedspelling and only correct inside a query โ a+in a path is a literal plus.func encodeUrl(src: openArray; usePlus: bool): stringsrcpercent-encoded.func decodeUrl(src: openArray; dest: var string; decodePlus: bool): boolPercent-decode
srcintodest, replacing whateverdestheld.falseโ and an emptydestโ for a%that is not followed by two hex digits, which is not a URI and must not be guessed at.decodePlusreads+as a space. Off by default because it is right for a form-encoded query and wrong everywhere else, and a decoder that does it unconditionally turns the filenamea+b.txtintoa b.txt.A
%00decodes to a NUL, which is a legal byte here and a catastrophe in a path:safePathis what rejects it, because this proc's job is to say what the bytes were, not whether they are a good idea.proc decodeUrl(src: string; decodePlus: bool): stringThe convenience form, for a caller that already knows its input is a URI โ a literal, or a string this module produced. Raises
SyntaxErroron a malformed escape.Never reach for this on something a peer sent. A request target is not known to be a URI; that is what the request is claiming and what the server has to check.
func parseUri(s: openArray): UriSplit
sinto its components. This is a split, not a validation: it answers where the parts are, and whether a hostname is a hostname or a path is servable is a question for whoever is about to use one.Accepts every form a request target can take โ
/a/b?q(origin),http://h/a(absolute),h:443(authority, for CONNECT) and*.func parseUri(s: string): Urifunc isAbsolute(u: Uri): boolWhether
unames where it lives โ a scheme, and an authority unless it is opaque.func $(u: Uri): stringSpell
uout again. Round-trips whatparseUrisplit, brackets included.iterator decodeQuery(q: openArray; decodePlus: bool): tuple[string, string]The
key=valuepairs of a query string, each side percent-decoded.A pair whose either side is a malformed escape is skipped rather than yielded raw: half-decoded is the one answer that is wrong in a way the consumer cannot see. A caller that must distinguish "absent" from "malformed" splits the string itself and uses
decodeUrldirectly.keywith no=yields an empty value;&&yields nothing. Separators are∧, because a query written by an HTML form of a certain vintage uses the latter.func encodeQuery(pairs: openArray; usePlus: bool; omitEq: bool): stringThe inverse.
omitEqwrites a bare key for an empty value.func normalizedPath(path: openArray; dest: var string): boolRFC 3986 ยง5.2.4
remove_dot_segments, with the one addition that matters to a server:falsewhen the path tried to climb above its own root.The RFC discards such a
..silently, which is right for resolving a reference and wrong for serving a file โ a request for/../etc/passwdis not a request for/etc/passwd, it is a request that should not be answered. Reporting it is what lets a server say 400 instead of quietly serving something adjacent to what was asked for.Operates on a decoded path. See this module's header for why that order is the whole of the defence.
func safePath(target: openArray; dest: var string): boolA request target to a path that is safe to join to a document root, or
false. This is the one proc a static file server should be calling.Decodes first, then normalizes โ the order is the defence, see the module header โ and then refuses three things the normalizer has no opinion about but a filesystem does:
- a path that is not absolute, because a relative one resolves against
whatever the process's working directory happens to be;
- an embedded NUL, which every C API reads as the end of the string and
so turns
/safe.txt\0/../../etc/passwdinto two different paths depending on who is looking;- a backslash, which is a separator on Windows and a literal filename
character everywhere else โ the same request would then mean two things on two hosts, and a cross-platform server has to pick one.
The query is stripped: it is not part of the path, and a target that still has one has not been split yet.
func combine(base: Uri; rel: Uri): UriResolve
relagainstbase, RFC 3986 ยง5.3-ish: enough for a redirect'sLocation, which is what a client actually needs it for.func /(base: Uri; rel: string): UriJoin one more path segment, with exactly one slash between.