From b47d5486dbd0e6677e42d2afd613fb921b3e12eb Mon Sep 17 00:00:00 2001 From: Huy Doan <106477+ba0f3@users.noreply.github.com> Date: Sun, 9 Oct 2022 13:29:22 +0700 Subject: FTP client is now able to connect to server over TLS by set `useTls =… (#17219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FTP client is now able to connect to server over TLS by set `useTls = true` in newAsyncFtpClient proc * Update asyncftpclient.nim * fix CI * shouldn't use {.error.} * Update lib/pure/asyncftpclient.nim Co-authored-by: flywind Co-authored-by: ringabout <43030857+ringabout@users.noreply.github.com> Co-authored-by: Clay Sweetser --- lib/pure/asyncftpclient.nim | 85 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/lib/pure/asyncftpclient.nim b/lib/pure/asyncftpclient.nim index 0d2ee80c5..6098e753f 100644 --- a/lib/pure/asyncftpclient.nim +++ b/lib/pure/asyncftpclient.nim @@ -79,7 +79,17 @@ import asyncdispatch, asyncnet, nativesockets, strutils, parseutils, os, times -from net import BufferSize +from net import BufferSize, SslContext + +when defined(ssl): + from net import SslHandshakeType, newContext, SslCVerifyMode + var defaultSslContext {.threadvar.}: SslContext + + proc getSSLContext(): SslContext = + if defaultSSLContext == nil: + defaultSSLContext = newContext(verifyMode = CVerifyPeer) + result = defaultSSLContext + when defined(nimPreviewSlimSystem): import std/assertions @@ -95,6 +105,9 @@ type jobInProgress*: bool job*: FtpJob dsockConnected*: bool + useTls: bool + when defined(ssl): + sslContext: SslContext FtpJobType* = enum JRetrText, JRetr, JStore @@ -179,9 +192,20 @@ proc pasv(ftp: AsyncFtpClient) {.async.} = var ip = nums[0 .. ^3] var port = nums[^2 .. ^1] var properPort = port[0].parseInt()*256+port[1].parseInt() - await ftp.dsock.connect(ip.join("."), Port(properPort)) + let address = ip.join(".") + await ftp.dsock.connect(address, Port(properPort)) ftp.dsockConnected = true + if ftp.useTls: + when defined(ssl): + try: + ftp.sslContext.wrapConnectedSocket(ftp.dsock, handshakeAsClient, address) + except: + ftp.dsock.close() + raise getCurrentException() + else: + doAssert false, "TLS support is not available. Cannot connect over TLS. Compile with -d:ssl to enable." + proc normalizePathSep(path: string): string = return replace(path, '\\', '/') @@ -198,12 +222,28 @@ proc connect*(ftp: AsyncFtpClient) {.async.} = # Handle 220 messages from the server assertReply(reply, "220") + if ftp.useTls: + when defined(ssl): + assertReply(await(ftp.send("AUTH TLS")), "234") + try: + ftp.sslContext.wrapConnectedSocket(ftp.csock, handshakeAsClient, ftp.address) + except: + ftp.csock.close() + raise getCurrentException() + else: + doAssert false, "TLS support is not available. Cannot connect over TLS. Compile with -d:ssl to enable." + if ftp.user != "": assertReply(await(ftp.send("USER " & ftp.user)), "230", "331") if ftp.pass != "": assertReply(await(ftp.send("PASS " & ftp.pass)), "230") + if ftp.useTls: + assertReply(await(ftp.send("PBSZ 0")), "200") + assertReply(await(ftp.send("PROT P")), "200") + assertReply(await(ftp.send("TYPE I")), "200") + proc pwd*(ftp: AsyncFtpClient): Future[string] {.async.} = ## Returns the current working directory. let wd = await ftp.send("PWD") @@ -220,15 +260,15 @@ proc cdup*(ftp: AsyncFtpClient) {.async.} = proc getLines(ftp: AsyncFtpClient): Future[string] {.async.} = ## Downloads text data in ASCII mode - result = "" assert ftp.dsockConnected while ftp.dsockConnected: let r = await ftp.dsock.recvLine() - if r == "": + if r.len == 0: + ftp.dsock.close() ftp.dsockConnected = false else: - result.add(r & "\n") - + if result.len > 0: result.add "\n" + result.add r assertReply(await(ftp.expectReply()), "226") proc listDirs*(ftp: AsyncFtpClient, dir = ""): Future[seq[string]] {.async.} = @@ -322,13 +362,14 @@ proc getFile(ftp: AsyncFtpClient, file: File, total: BiggestInt, if dataFut.finished: let data = dataFut.read - if data != "": + if data.len > 0: progress.inc(data.len) progressInSecond.inc(data.len) file.write(data) dataFut = ftp.dsock.recv(BufferSize) else: ftp.dsockConnected = false + ftp.dsock.close() assertReply(await(ftp.expectReply()), "226") @@ -371,7 +412,7 @@ proc doUpload(ftp: AsyncFtpClient, file: File, while ftp.dsockConnected: if sendFut == nil or sendFut.finished: # TODO: Async file reading. - let len = file.readBuffer(addr(data[0]), 4000) + let len = file.readBuffer(addr data[0], 4000) setLen(data, len) if len == 0: # File finished uploading. @@ -422,7 +463,7 @@ proc removeDir*(ftp: AsyncFtpClient, dir: string) {.async.} = assertReply(await ftp.send("RMD " & dir), "250") proc newAsyncFtpClient*(address: string, port = Port(21), - user, pass = "", progressInterval: int = 1000): AsyncFtpClient = + user, pass = "", progressInterval: int = 1000, useTls = false, sslContext: SslContext = nil): AsyncFtpClient = ## Creates a new `AsyncFtpClient` object. new result result.user = user @@ -432,8 +473,17 @@ proc newAsyncFtpClient*(address: string, port = Port(21), result.progressInterval = progressInterval result.dsockConnected = false result.csock = newAsyncSocket() + if useTls: + when defined(ssl): + result.useTls = true + if sslContext == nil: + result.sslContext = getSSLContext() + else: + result.sslContext = sslContext + else: + doAssert false, "TLS support is not available. Cannot connect over TLS. Compile with -d:ssl to enable." -when not defined(testing) and isMainModule: +when not defined(testing) and defined(ssl) and isMainModule: var ftp = newAsyncFtpClient("example.com", user = "test", pass = "test") proc main(ftp: AsyncFtpClient) {.async.} = await ftp.connect() @@ -448,4 +498,19 @@ when not defined(testing) and isMainModule: await ftp.removeDir("deleteme") echo("Finished") + var ftps = newAsyncFtpClient("example.com", user = "test", pass = "test", useTls = true) + proc main1(ftp: AsyncFtpClient) {.async.} = + await ftps.connect() + echo await ftps.pwd() + echo await ftps.listDirs() + await ftps.store("payload.jpg", "payload.jpg") + await ftps.retrFile("payload.jpg", "payload2.jpg") + await ftps.rename("payload.jpg", "payload_renamed.jpg") + await ftps.store("payload.jpg", "payload_remove.jpg") + await ftps.removeFile("payload_remove.jpg") + await ftps.createDir("deleteme") + await ftps.removeDir("deleteme") + echo("Finished") + waitFor main(ftp) + waitFor main1(ftp) -- cgit 1.4.1-2-gfad0