FTP client is now able to connect to server over TLS by set `useTls =… (#17219)

* 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 <xzsflywind@gmail.com>
Co-authored-by: ringabout <43030857+ringabout@users.noreply.github.com>
Co-authored-by: Clay Sweetser <Varriount@users.noreply.github.com>
This commit is contained in:
Huy Doan 2022-10-09 13:29:22 +07:00 • committed by GitHub
commit b47d5486db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -79,7 +79,17 @@
import asyncdispatch, asyncnet, nativesockets, strutils, parseutils, os, times 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): when defined(nimPreviewSlimSystem):
import std/assertions import std/assertions
@ -95,6 +105,9 @@ type
jobInProgress*: bool jobInProgress*: bool
job*: FtpJob job*: FtpJob
dsockConnected*: bool dsockConnected*: bool
useTls: bool
when defined(ssl):
sslContext: SslContext
FtpJobType* = enum FtpJobType* = enum
JRetrText, JRetr, JStore JRetrText, JRetr, JStore
@ -179,9 +192,20 @@ proc pasv(ftp: AsyncFtpClient) {.async.} =
var ip = nums[0 .. ^3] var ip = nums[0 .. ^3]
var port = nums[^2 .. ^1] var port = nums[^2 .. ^1]
var properPort = port[0].parseInt()*256+port[1].parseInt() 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 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 = proc normalizePathSep(path: string): string =
return replace(path, '\\', '/') return replace(path, '\\', '/')
@ -198,12 +222,28 @@ proc connect*(ftp: AsyncFtpClient) {.async.} =
# Handle 220 messages from the server # Handle 220 messages from the server
assertReply(reply, "220") 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 != "": if ftp.user != "":
assertReply(await(ftp.send("USER " & ftp.user)), "230", "331") assertReply(await(ftp.send("USER " & ftp.user)), "230", "331")
if ftp.pass != "": if ftp.pass != "":
assertReply(await(ftp.send("PASS " & ftp.pass)), "230") 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.} = proc pwd*(ftp: AsyncFtpClient): Future[string] {.async.} =
## Returns the current working directory. ## Returns the current working directory.
let wd = await ftp.send("PWD") let wd = await ftp.send("PWD")
@ -220,15 +260,15 @@ proc cdup*(ftp: AsyncFtpClient) {.async.} =
proc getLines(ftp: AsyncFtpClient): Future[string] {.async.} = proc getLines(ftp: AsyncFtpClient): Future[string] {.async.} =
## Downloads text data in ASCII mode ## Downloads text data in ASCII mode
result = ""
assert ftp.dsockConnected assert ftp.dsockConnected
while ftp.dsockConnected: while ftp.dsockConnected:
let r = await ftp.dsock.recvLine() let r = await ftp.dsock.recvLine()
if r == "": if r.len == 0:
ftp.dsock.close()
ftp.dsockConnected = false ftp.dsockConnected = false
else: else:
result.add(r & "\n") if result.len > 0: result.add "\n"
result.add r
assertReply(await(ftp.expectReply()), "226") assertReply(await(ftp.expectReply()), "226")
proc listDirs*(ftp: AsyncFtpClient, dir = ""): Future[seq[string]] {.async.} = proc listDirs*(ftp: AsyncFtpClient, dir = ""): Future[seq[string]] {.async.} =
@ -322,13 +362,14 @@ proc getFile(ftp: AsyncFtpClient, file: File, total: BiggestInt,
if dataFut.finished: if dataFut.finished:
let data = dataFut.read let data = dataFut.read
if data != "": if data.len > 0:
progress.inc(data.len) progress.inc(data.len)
progressInSecond.inc(data.len) progressInSecond.inc(data.len)
file.write(data) file.write(data)
dataFut = ftp.dsock.recv(BufferSize) dataFut = ftp.dsock.recv(BufferSize)
else: else:
ftp.dsockConnected = false ftp.dsockConnected = false
ftp.dsock.close()
assertReply(await(ftp.expectReply()), "226") assertReply(await(ftp.expectReply()), "226")
@ -371,7 +412,7 @@ proc doUpload(ftp: AsyncFtpClient, file: File,
while ftp.dsockConnected: while ftp.dsockConnected:
if sendFut == nil or sendFut.finished: if sendFut == nil or sendFut.finished:
# TODO: Async file reading. # TODO: Async file reading.
let len = file.readBuffer(addr(data[0]), 4000) let len = file.readBuffer(addr data[0], 4000)
setLen(data, len) setLen(data, len)
if len == 0: if len == 0:
# File finished uploading. # File finished uploading.
@ -422,7 +463,7 @@ proc removeDir*(ftp: AsyncFtpClient, dir: string) {.async.} =
assertReply(await ftp.send("RMD " & dir), "250") assertReply(await ftp.send("RMD " & dir), "250")
proc newAsyncFtpClient*(address: string, port = Port(21), 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. ## Creates a new `AsyncFtpClient` object.
new result new result
result.user = user result.user = user
@ -432,8 +473,17 @@ proc newAsyncFtpClient*(address: string, port = Port(21),
result.progressInterval = progressInterval result.progressInterval = progressInterval
result.dsockConnected = false result.dsockConnected = false
result.csock = newAsyncSocket() 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") var ftp = newAsyncFtpClient("example.com", user = "test", pass = "test")
proc main(ftp: AsyncFtpClient) {.async.} = proc main(ftp: AsyncFtpClient) {.async.} =
await ftp.connect() await ftp.connect()
@ -448,4 +498,19 @@ when not defined(testing) and isMainModule:
await ftp.removeDir("deleteme") await ftp.removeDir("deleteme")
echo("Finished") 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 main(ftp)
waitFor main1(ftp)