test_31_vsftpds.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #***************************************************************************
  4. # _ _ ____ _
  5. # Project ___| | | | _ \| |
  6. # / __| | | | |_) | |
  7. # | (__| |_| | _ <| |___
  8. # \___|\___/|_| \_\_____|
  9. #
  10. # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
  11. #
  12. # This software is licensed as described in the file COPYING, which
  13. # you should have received as part of this distribution. The terms
  14. # are also available at https://curl.se/docs/copyright.html.
  15. #
  16. # You may opt to use, copy, modify, merge, publish, distribute and/or sell
  17. # copies of the Software, and permit persons to whom the Software is
  18. # furnished to do so, under the terms of the COPYING file.
  19. #
  20. # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
  21. # KIND, either express or implied.
  22. #
  23. # SPDX-License-Identifier: curl
  24. #
  25. ###########################################################################
  26. #
  27. import difflib
  28. import filecmp
  29. import logging
  30. import os
  31. import shutil
  32. import pytest
  33. from testenv import Env, CurlClient, VsFTPD
  34. log = logging.getLogger(__name__)
  35. @pytest.mark.skipif(condition=not Env.has_vsftpd(), reason="missing vsftpd")
  36. class TestVsFTPD:
  37. SUPPORTS_SSL = True
  38. @pytest.fixture(autouse=True, scope='class')
  39. def vsftpds(self, env):
  40. if not TestVsFTPD.SUPPORTS_SSL:
  41. pytest.skip('vsftpd does not seem to support SSL')
  42. vsftpds = VsFTPD(env=env, with_ssl=True)
  43. if not vsftpds.start():
  44. vsftpds.stop()
  45. TestVsFTPD.SUPPORTS_SSL = False
  46. pytest.skip('vsftpd does not seem to support SSL')
  47. yield vsftpds
  48. vsftpds.stop()
  49. def _make_docs_file(self, docs_dir: str, fname: str, fsize: int):
  50. fpath = os.path.join(docs_dir, fname)
  51. data1k = 1024*'x'
  52. flen = 0
  53. with open(fpath, 'w') as fd:
  54. while flen < fsize:
  55. fd.write(data1k)
  56. flen += len(data1k)
  57. return flen
  58. @pytest.fixture(autouse=True, scope='class')
  59. def _class_scope(self, env, vsftpds):
  60. if os.path.exists(vsftpds.docs_dir):
  61. shutil.rmtree(vsftpds.docs_dir)
  62. if not os.path.exists(vsftpds.docs_dir):
  63. os.makedirs(vsftpds.docs_dir)
  64. self._make_docs_file(docs_dir=vsftpds.docs_dir, fname='data-1k', fsize=1024)
  65. self._make_docs_file(docs_dir=vsftpds.docs_dir, fname='data-10k', fsize=10*1024)
  66. self._make_docs_file(docs_dir=vsftpds.docs_dir, fname='data-1m', fsize=1024*1024)
  67. self._make_docs_file(docs_dir=vsftpds.docs_dir, fname='data-10m', fsize=10*1024*1024)
  68. env.make_data_file(indir=env.gen_dir, fname="upload-1k", fsize=1024)
  69. env.make_data_file(indir=env.gen_dir, fname="upload-100k", fsize=100*1024)
  70. env.make_data_file(indir=env.gen_dir, fname="upload-1m", fsize=1024*1024)
  71. def test_31_01_list_dir(self, env: Env, vsftpds: VsFTPD, repeat):
  72. curl = CurlClient(env=env)
  73. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/'
  74. r = curl.ftp_ssl_get(urls=[url], with_stats=True)
  75. r.check_stats(count=1, http_status=226)
  76. lines = open(os.path.join(curl.run_dir, 'download_#1.data')).readlines()
  77. assert len(lines) == 4, f'list: {lines}'
  78. # download 1 file, no SSL
  79. @pytest.mark.parametrize("docname", [
  80. 'data-1k', 'data-1m', 'data-10m'
  81. ])
  82. def test_31_02_download_1(self, env: Env, vsftpds: VsFTPD, docname, repeat):
  83. curl = CurlClient(env=env)
  84. srcfile = os.path.join(vsftpds.docs_dir, f'{docname}')
  85. count = 1
  86. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}?[0-{count-1}]'
  87. r = curl.ftp_ssl_get(urls=[url], with_stats=True)
  88. r.check_stats(count=count, http_status=226)
  89. self.check_downloads(curl, srcfile, count)
  90. @pytest.mark.parametrize("docname", [
  91. 'data-1k', 'data-1m', 'data-10m'
  92. ])
  93. def test_31_03_download_10_serial(self, env: Env, vsftpds: VsFTPD, docname, repeat):
  94. curl = CurlClient(env=env)
  95. srcfile = os.path.join(vsftpds.docs_dir, f'{docname}')
  96. count = 10
  97. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}?[0-{count-1}]'
  98. r = curl.ftp_ssl_get(urls=[url], with_stats=True)
  99. r.check_stats(count=count, http_status=226)
  100. self.check_downloads(curl, srcfile, count)
  101. @pytest.mark.parametrize("docname", [
  102. 'data-1k', 'data-1m', 'data-10m'
  103. ])
  104. def test_31_04_download_10_parallel(self, env: Env, vsftpds: VsFTPD, docname, repeat):
  105. curl = CurlClient(env=env)
  106. srcfile = os.path.join(vsftpds.docs_dir, f'{docname}')
  107. count = 10
  108. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}?[0-{count-1}]'
  109. r = curl.ftp_ssl_get(urls=[url], with_stats=True, extra_args=[
  110. '--parallel'
  111. ])
  112. r.check_stats(count=count, http_status=226)
  113. self.check_downloads(curl, srcfile, count)
  114. @pytest.mark.parametrize("docname", [
  115. 'upload-1k', 'upload-100k', 'upload-1m'
  116. ])
  117. def test_31_05_upload_1(self, env: Env, vsftpds: VsFTPD, docname, repeat):
  118. curl = CurlClient(env=env)
  119. srcfile = os.path.join(env.gen_dir, docname)
  120. dstfile = os.path.join(vsftpds.docs_dir, docname)
  121. self._rmf(dstfile)
  122. count = 1
  123. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/'
  124. r = curl.ftp_ssl_upload(urls=[url], fupload=f'{srcfile}', with_stats=True)
  125. r.check_stats(count=count, http_status=226)
  126. self.check_upload(env, vsftpds, docname=docname)
  127. def _rmf(self, path):
  128. if os.path.exists(path):
  129. return os.remove(path)
  130. # check with `tcpdump` if curl causes any TCP RST packets
  131. @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
  132. def test_31_06_shutdownh_download(self, env: Env, vsftpds: VsFTPD, repeat):
  133. docname = 'data-1k'
  134. curl = CurlClient(env=env)
  135. count = 1
  136. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}?[0-{count-1}]'
  137. r = curl.ftp_ssl_get(urls=[url], with_stats=True, with_tcpdump=True)
  138. r.check_stats(count=count, http_status=226)
  139. # vsftp closes control connection without niceties,
  140. # disregard RST packets it sent from its port to curl
  141. assert len(r.tcpdump.stats_excluding(src_port=env.ftps_port)) == 0, 'Unexpected TCP RSTs packets'
  142. # check with `tcpdump` if curl causes any TCP RST packets
  143. @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
  144. def test_31_07_shutdownh_upload(self, env: Env, vsftpds: VsFTPD, repeat):
  145. docname = 'upload-1k'
  146. curl = CurlClient(env=env)
  147. srcfile = os.path.join(env.gen_dir, docname)
  148. dstfile = os.path.join(vsftpds.docs_dir, docname)
  149. self._rmf(dstfile)
  150. count = 1
  151. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/'
  152. r = curl.ftp_ssl_upload(urls=[url], fupload=f'{srcfile}', with_stats=True, with_tcpdump=True)
  153. r.check_stats(count=count, http_status=226)
  154. # vsftp closes control connection without niceties,
  155. # disregard RST packets it sent from its port to curl
  156. assert len(r.tcpdump.stats_excluding(src_port=env.ftps_port)) == 0, 'Unexpected TCP RSTs packets'
  157. def test_31_08_upload_ascii(self, env: Env, vsftpds: VsFTPD):
  158. docname = 'upload-ascii'
  159. line_length = 21
  160. srcfile = os.path.join(env.gen_dir, docname)
  161. dstfile = os.path.join(vsftpds.docs_dir, docname)
  162. env.make_data_file(indir=env.gen_dir, fname=docname, fsize=100*1024,
  163. line_length=line_length)
  164. srcsize = os.path.getsize(srcfile)
  165. self._rmf(dstfile)
  166. count = 1
  167. curl = CurlClient(env=env)
  168. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/'
  169. r = curl.ftp_ssl_upload(urls=[url], fupload=f'{srcfile}', with_stats=True,
  170. extra_args=['--use-ascii'])
  171. r.check_stats(count=count, http_status=226)
  172. # expect the uploaded file to be number of converted newlines larger
  173. dstsize = os.path.getsize(dstfile)
  174. newlines = len(open(srcfile).readlines())
  175. assert (srcsize + newlines) == dstsize, \
  176. f'expected source with {newlines} lines to be that much larger,'\
  177. f'instead srcsize={srcsize}, upload size={dstsize}, diff={dstsize-srcsize}'
  178. def test_31_08_active_download(self, env: Env, vsftpds: VsFTPD):
  179. docname = 'data-10k'
  180. curl = CurlClient(env=env)
  181. srcfile = os.path.join(vsftpds.docs_dir, f'{docname}')
  182. count = 1
  183. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}?[0-{count-1}]'
  184. r = curl.ftp_ssl_get(urls=[url], with_stats=True, extra_args=[
  185. '--ftp-port', '127.0.0.1'
  186. ])
  187. r.check_stats(count=count, http_status=226)
  188. self.check_downloads(curl, srcfile, count)
  189. def test_31_09_active_upload(self, env: Env, vsftpds: VsFTPD):
  190. docname = 'upload-1k'
  191. curl = CurlClient(env=env)
  192. srcfile = os.path.join(env.gen_dir, docname)
  193. dstfile = os.path.join(vsftpds.docs_dir, docname)
  194. self._rmf(dstfile)
  195. count = 1
  196. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/'
  197. r = curl.ftp_ssl_upload(urls=[url], fupload=f'{srcfile}', with_stats=True, extra_args=[
  198. '--ftp-port', '127.0.0.1'
  199. ])
  200. r.check_stats(count=count, http_status=226)
  201. self.check_upload(env, vsftpds, docname=docname)
  202. @pytest.mark.parametrize("indata", [
  203. '1234567890', ''
  204. ])
  205. def test_31_10_upload_stdin(self, env: Env, vsftpds: VsFTPD, indata):
  206. curl = CurlClient(env=env)
  207. docname = "upload_31_10"
  208. dstfile = os.path.join(vsftpds.docs_dir, docname)
  209. self._rmf(dstfile)
  210. count = 1
  211. url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}'
  212. r = curl.ftp_ssl_upload(urls=[url], updata=indata, with_stats=True)
  213. r.check_stats(count=count, http_status=226)
  214. assert os.path.exists(dstfile)
  215. destdata = open(dstfile).readlines()
  216. expdata = [indata] if len(indata) else []
  217. assert expdata == destdata, f'exected: {expdata}, got: {destdata}'
  218. def check_downloads(self, client, srcfile: str, count: int,
  219. complete: bool = True):
  220. for i in range(count):
  221. dfile = client.download_file(i)
  222. assert os.path.exists(dfile)
  223. if complete and not filecmp.cmp(srcfile, dfile, shallow=False):
  224. diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
  225. b=open(dfile).readlines(),
  226. fromfile=srcfile,
  227. tofile=dfile,
  228. n=1))
  229. assert False, f'download {dfile} differs:\n{diff}'
  230. def check_upload(self, env, vsftpd: VsFTPD, docname):
  231. srcfile = os.path.join(env.gen_dir, docname)
  232. dstfile = os.path.join(vsftpd.docs_dir, docname)
  233. assert os.path.exists(srcfile)
  234. assert os.path.exists(dstfile)
  235. if not filecmp.cmp(srcfile, dstfile, shallow=False):
  236. diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
  237. b=open(dstfile).readlines(),
  238. fromfile=srcfile,
  239. tofile=dstfile,
  240. n=1))
  241. assert False, f'upload {dstfile} differs:\n{diff}'