diff options
-rw-r--r-- | adapter/protocol/curl.nim | 4 | ||||
-rw-r--r-- | adapter/protocol/curlwrap.nim | 5 | ||||
-rw-r--r-- | adapter/protocol/dirlist.nim | 9 | ||||
-rw-r--r-- | adapter/protocol/ftp.nim | 146 | ||||
-rw-r--r-- | doc/protocols.md | 11 |
5 files changed, 135 insertions, 40 deletions
diff --git a/adapter/protocol/curl.nim b/adapter/protocol/curl.nim index a12e81c4..810435d2 100644 --- a/adapter/protocol/curl.nim +++ b/adapter/protocol/curl.nim @@ -111,10 +111,14 @@ type CURLOPT_WRITEDATA = CURLOPTTYPE_CBPOINT + 1 CURLOPT_URL = CURLOPTTYPE_STRINGPOINT + 2 CURLOPT_PROXY = CURLOPTTYPE_STRINGPOINT + 4 + CURLOPT_ERRORBUFFER = CURLOPTTYPE_OBJECTPOINT + 10 CURLOPT_POSTFIELDS = CURLOPTTYPE_OBJECTPOINT + 15 CURLOPT_HTTPHEADER = CURLOPTTYPE_SLISTPOINT + 23 + CURLOPT_KEYPASSWD = CURLOPTTYPE_STRINGPOINT + 26 CURLOPT_HEADERDATA = CURLOPTTYPE_CBPOINT + 29 CURLOPT_ACCEPT_ENCODING = CURLOPTTYPE_STRINGPOINT + 102 + CURLOPT_SSH_PUBLIC_KEYFILE = CURLOPTTYPE_STRINGPOINT + 152 + CURLOPT_SSH_PRIVATE_KEYFILE = CURLOPTTYPE_STRINGPOINT + 153 CURLOPT_MIMEPOST = CURLOPTTYPE_OBJECTPOINT + 269 CURLOPT_CURLU = CURLOPTTYPE_OBJECTPOINT + 282 CURLOPT_PREREQDATA = CURLOPTTYPE_CBPOINT + 313 diff --git a/adapter/protocol/curlwrap.nim b/adapter/protocol/curlwrap.nim index 523c260f..c3c69a68 100644 --- a/adapter/protocol/curlwrap.nim +++ b/adapter/protocol/curlwrap.nim @@ -9,8 +9,11 @@ template setopt*(curl: CURL, opt: CURLoption, arg: string) = template getinfo*(curl: CURL, info: CURLINFO, arg: typed) = discard curl_easy_getinfo(curl, info, arg) +template set*(url: CURLU, part: CURLUPart, content: cstring, flags: cuint) = + discard curl_url_set(url, part, content, flags) + template set*(url: CURLU, part: CURLUPart, content: string, flags: cuint) = - discard curl_url_set(url, part, cstring(content), flags) + url.set(part, cstring(content), flags) template get*(url: CURLU, part: CURLUPart, flags: cuint): cstring = var outs: cstring diff --git a/adapter/protocol/dirlist.nim b/adapter/protocol/dirlist.nim index d0933fed..a8623f35 100644 --- a/adapter/protocol/dirlist.nim +++ b/adapter/protocol/dirlist.nim @@ -17,7 +17,7 @@ type DirlistItem* = object of ITEM_DIR: discard -type NameWidthTuple = tuple[name: string, width: int] +type NameWidthTuple = tuple[name: string, width: int, item: ptr DirlistItem] func makeDirlist*(items: seq[DirlistItem]): string = var names: seq[NameWidthTuple] @@ -30,12 +30,11 @@ func makeDirlist*(items: seq[DirlistItem]): string = name &= '/' let w = name.width() maxw = max(w, maxw) - names.add((name, w)) + names.add((name, w, unsafeAddr item)) names.sort(proc(a, b: NameWidthTuple): int = cmp(a.name, b.name)) var outs = "<A HREF=\"../\">[Upper Directory]</A>\n" - for i in 0 ..< items.len: - let item = items[i] - var (name, width) = names[i] + for (name, width, itemp) in names.mitems: + let item = itemp[] var path = percentEncode(item.name, PathPercentEncodeSet) if item.t == ITEM_LINK: if item.linkto.len > 0 and item.linkto[^1] == '/': diff --git a/adapter/protocol/ftp.nim b/adapter/protocol/ftp.nim index 41c2183f..88ced6be 100644 --- a/adapter/protocol/ftp.nim +++ b/adapter/protocol/ftp.nim @@ -1,8 +1,5 @@ -when NimMajor >= 2: - import std/envvars -else: - import std/os import std/options +import std/os import std/strutils import curl @@ -21,8 +18,24 @@ type FtpHandle = ref object path: string statusline: bool -proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, - userdata: pointer): csize_t {.cdecl.} = +proc printHeader(op: FtpHandle) = + if op.dirmode: + stdout.write("""Content-Type: text/html + +<HTML> +<HEAD> +<BASE HREF=""" & op.base & """> +<TITLE>""" & op.path & """</TITLE> +</HEAD> +<BODY> +<H1>Index of """ & htmlEscape(op.path) & """</H1> +<PRE> +""") + else: + stdout.write('\n') + +proc curlWriteHeader(p: cstring; size, nitems: csize_t; userdata: pointer): + csize_t {.cdecl.} = var line = newString(nitems) if nitems > 0: prepareMutation(line) @@ -34,20 +47,7 @@ proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, var status: clong op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) stdout.write("Status: " & $status & "\n") - if op.dirmode: - stdout.write("Content-Type: text/html\n") - stdout.write("\n") - if op.dirmode: - stdout.write(""" -<HTML> -<HEAD> -<BASE HREF=""" & op.base & """> -<TITLE>""" & op.path & """</TITLE> -</HEAD> -<BODY> -<H1>Index of """ & htmlEscape(op.path) & """</H1> -<PRE> -""") + op.printHeader() return nitems elif line.startsWith("530"): # login incorrect op.statusline = true @@ -72,14 +72,16 @@ Content-Type: text/html return nitems # From the documentation: size is always 1. -proc curlWriteBody(p: cstring, size, nmemb: csize_t, userdata: pointer): +proc curlWriteBody(p: cstring; size, nmemb: csize_t; userdata: pointer): csize_t {.cdecl.} = let op = cast[FtpHandle](userdata) + if not op.statusline: + op.statusline = true + op.printHeader() if nmemb > 0: if op.dirmode: let i = op.buffer.len op.buffer.setLen(op.buffer.len + int(nmemb)) - prepareMutation(op.buffer) copyMem(addr op.buffer[i], p, nmemb) else: return csize_t(stdout.writeBuffer(p, int(nmemb))) @@ -87,7 +89,7 @@ proc curlWriteBody(p: cstring, size, nmemb: csize_t, userdata: pointer): proc finish(op: FtpHandle) = let op = op - var items: seq[DirlistItem] + var items: seq[DirlistItem] = @[] for line in op.buffer.split('\n'): if line.len == 0: continue var i = 10 # permission @@ -151,10 +153,70 @@ proc finish(op: FtpHandle) = stdout.write(makeDirlist(items)) stdout.write("\n</PRE>\n</BODY>\n</HTML>\n") +proc matchesPattern(s, pat: openArray[char]): bool = + var i = 0 + for j, c in pat: + if c == '*': + while i < s.len: + if s.toOpenArray(i, s.high).matchesPattern(pat.toOpenArray(j + 1, + pat.high)): + return true + inc i + return false + if i >= s.len or c != '?' and c != s[i]: + return false + inc i + return true + +proc parseSSHConfig(f: File; curl: CURL; host: string; idSet: var bool) = + var skipTillNext = false + var line: string + var certificateFile = "" + var identityFile = "" + while f.readLine(line): + var i = line.skipBlanks(0) + if i == line.len or line[i] == '#': + continue + let k = line.until(AsciiWhitespace, i) + i = line.skipBlanks(i + k.len) + if i < line.len and line[i] == '=': + i = line.skipBlanks(i + 1) + if i == line.len or line[i] == '#': + continue + var arg = "" + let isStr = line[i] in {'"', '\''} + if isStr: + inc i + var quot = false + while i < line.len and (quot or line[i] != '"' or not isStr): + if not quot and line[i] == '\\': + quot = true + else: + arg &= line[i] + inc i + if k == "Match": #TODO support this + skipTillNext = true + elif k == "Host": + skipTillNext = not host.matchesPattern(arg) + elif skipTillNext: + continue + elif k == "IdentityFile": + identityFile = expandTilde(arg) + elif k == "CertificateFile": + certificateFile = expandTilde(arg) + if identityFile != "": + curl.setopt(CURLOPT_SSH_PRIVATE_KEYFILE, identityFile) + idSet = true + if certificateFile != "": + curl.setopt(CURLOPT_SSH_PUBLIC_KEYFILE, certificateFile) + f.close() + proc main() = let curl = curl_easy_init() doAssert curl != nil - let opath = getEnv("MAPPED_URI_PATH") + var opath = getEnv("MAPPED_URI_PATH") + if opath == "": + opath = "/" let path = percentDecode(opath) let op = FtpHandle( curl: curl, @@ -162,14 +224,30 @@ proc main() = ) let url = curl_url() const flags = cuint(CURLU_PATH_AS_IS) - url.set(CURLUPART_SCHEME, getEnv("MAPPED_URI_SCHEME"), flags) + let scheme = getEnv("MAPPED_URI_SCHEME") + url.set(CURLUPART_SCHEME, scheme, flags) let username = getEnv("MAPPED_URI_USERNAME") if username != "": url.set(CURLUPART_USER, username, flags) + let host = getEnv("MAPPED_URI_HOST") let password = getEnv("MAPPED_URI_PASSWORD") - if password != "": - url.set(CURLUPART_PASSWORD, password, flags) - url.set(CURLUPART_HOST, getEnv("MAPPED_URI_HOST"), flags) + var idSet = false + # Parse SSH config for sftp. + if scheme == "sftp": + let systemConfig = "/etc/ssh/ssh_config" + if fileExists(systemConfig): + var f: File + if f.open(systemConfig): + parseSSHConfig(f, curl, host, idSet) + let userConfig = expandTilde("~/.ssh/config") + if fileExists(userConfig): + var f: File + if f.open(userConfig): + parseSSHConfig(f, curl, host, idSet) + if idSet: + curl.setopt(CURLOPT_KEYPASSWD, password) + url.set(CURLUPART_PASSWORD, password, flags) + url.set(CURLUPART_HOST, host, flags) let port = getEnv("MAPPED_URI_PORT") if port != "": url.set(CURLUPART_PORT, port, flags) @@ -192,7 +270,12 @@ proc main() = op.path = path curl_free(surl) # Now for the workaround: - url.set(CURLUPART_PATH, '/' & opath, flags) + if scheme != "sftp" and (opath.len <= 1 or opath[1] != '~'): + url.set(CURLUPART_PATH, '/' & opath, flags) + # Another hack: if password was set for the identity file, then clear it from + # the URL. + if idSet: + url.set(CURLUPART_PASSWORD, nil, flags) # Set opts for the request curl.setopt(CURLOPT_CURLU, url) curl.setopt(CURLOPT_HEADERDATA, op) @@ -211,7 +294,10 @@ proc main() = let res = curl_easy_perform(curl) if res != CURLE_OK: if not op.statusline: - stdout.write(getCurlConnectionError(res)) + if res == CURLE_LOGIN_DENIED: + stdout.write("Status: 401\n") + else: + stdout.write(getCurlConnectionError(res)) elif op.dirmode: op.finish() curl_url_cleanup(url) diff --git a/doc/protocols.md b/doc/protocols.md index 84a8dde1..6eb07112 100644 --- a/doc/protocols.md +++ b/doc/protocols.md @@ -52,11 +52,14 @@ not work. Chawan supports FTP through the `adapter/protocol/ftp.nim` libcurl adapter. For directory listings, it assumes UNIX output style, and will probably break horribly on receiving anything else. Otherwise, the directory listing view -is identical to (and uses the same code path as) the file:// directory listing. +is identical to the file:// directory listing. -In theory, SFTP and FTPS should be supported as well. In practice, SFTP does -not really work yet because there is no way to specify private keys, and I -have never seen an FTPS server in the wild so I assume it is broken too. +SFTP "works" too, but YMMV. Note that if an IdentityFile declaration is found in +your ssh config, then it will prompt for the identity file password, but there +is no way to tell whether it is really asking for that. Also, settings covered +by the Match field are ignored. + +In theory, FTPS should work too, but it is completely untested. ## Gopher |