test_30_vsftpd.py (10661B)
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 34 from testenv import Env, CurlClient, VsFTPD 35 36 37 log = logging.getLogger(__name__) 38 39 40 @pytest.mark.skipif(condition=not Env.has_vsftpd(), reason="missing vsftpd") 41 class TestVsFTPD: 42 43 @pytest.fixture(autouse=True, scope='class') 44 def vsftpd(self, env): 45 vsftpd = VsFTPD(env=env) 46 assert vsftpd.initial_start() 47 yield vsftpd 48 vsftpd.stop() 49 50 def _make_docs_file(self, docs_dir: str, fname: str, fsize: int): 51 fpath = os.path.join(docs_dir, fname) 52 data1k = 1024*'x' 53 flen = 0 54 with open(fpath, 'w') as fd: 55 while flen < fsize: 56 fd.write(data1k) 57 flen += len(data1k) 58 return flen 59 60 @pytest.fixture(autouse=True, scope='class') 61 def _class_scope(self, env, vsftpd): 62 if os.path.exists(vsftpd.docs_dir): 63 shutil.rmtree(vsftpd.docs_dir) 64 if not os.path.exists(vsftpd.docs_dir): 65 os.makedirs(vsftpd.docs_dir) 66 self._make_docs_file(docs_dir=vsftpd.docs_dir, fname='data-1k', fsize=1024) 67 self._make_docs_file(docs_dir=vsftpd.docs_dir, fname='data-10k', fsize=10*1024) 68 self._make_docs_file(docs_dir=vsftpd.docs_dir, fname='data-1m', fsize=1024*1024) 69 self._make_docs_file(docs_dir=vsftpd.docs_dir, fname='data-10m', fsize=10*1024*1024) 70 env.make_data_file(indir=env.gen_dir, fname="upload-1k", fsize=1024) 71 env.make_data_file(indir=env.gen_dir, fname="upload-100k", fsize=100*1024) 72 env.make_data_file(indir=env.gen_dir, fname="upload-1m", fsize=1024*1024) 73 74 def test_30_01_list_dir(self, env: Env, vsftpd: VsFTPD): 75 curl = CurlClient(env=env) 76 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/' 77 r = curl.ftp_get(urls=[url], with_stats=True) 78 r.check_stats(count=1, http_status=226) 79 lines = open(os.path.join(curl.run_dir, 'download_#1.data')).readlines() 80 assert len(lines) == 4, f'list: {lines}' 81 82 # download 1 file, no SSL 83 @pytest.mark.parametrize("docname", [ 84 'data-1k', 'data-1m', 'data-10m' 85 ]) 86 def test_30_02_download_1(self, env: Env, vsftpd: VsFTPD, docname): 87 curl = CurlClient(env=env) 88 srcfile = os.path.join(vsftpd.docs_dir, f'{docname}') 89 count = 1 90 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}?[0-{count-1}]' 91 r = curl.ftp_get(urls=[url], with_stats=True) 92 r.check_stats(count=count, http_status=226) 93 self.check_downloads(curl, srcfile, count) 94 95 @pytest.mark.parametrize("docname", [ 96 'data-1k', 'data-1m', 'data-10m' 97 ]) 98 def test_30_03_download_10_serial(self, env: Env, vsftpd: VsFTPD, docname): 99 curl = CurlClient(env=env) 100 srcfile = os.path.join(vsftpd.docs_dir, f'{docname}') 101 count = 10 102 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}?[0-{count-1}]' 103 r = curl.ftp_get(urls=[url], with_stats=True) 104 r.check_stats(count=count, http_status=226) 105 self.check_downloads(curl, srcfile, count) 106 assert r.total_connects == count + 1, 'should reuse the control conn' 107 108 @pytest.mark.parametrize("docname", [ 109 'data-1k', 'data-1m', 'data-10m' 110 ]) 111 def test_30_04_download_10_parallel(self, env: Env, vsftpd: VsFTPD, docname): 112 curl = CurlClient(env=env) 113 srcfile = os.path.join(vsftpd.docs_dir, f'{docname}') 114 count = 10 115 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}?[0-{count-1}]' 116 r = curl.ftp_get(urls=[url], with_stats=True, extra_args=[ 117 '--parallel' 118 ]) 119 r.check_stats(count=count, http_status=226) 120 self.check_downloads(curl, srcfile, count) 121 assert r.total_connects > count + 1, 'should have used several control conns' 122 123 @pytest.mark.parametrize("docname", [ 124 'upload-1k', 'upload-100k', 'upload-1m' 125 ]) 126 def test_30_05_upload_1(self, env: Env, vsftpd: VsFTPD, docname): 127 curl = CurlClient(env=env) 128 srcfile = os.path.join(env.gen_dir, docname) 129 dstfile = os.path.join(vsftpd.docs_dir, docname) 130 self._rmf(dstfile) 131 count = 1 132 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/' 133 r = curl.ftp_upload(urls=[url], fupload=f'{srcfile}', with_stats=True) 134 r.check_stats(count=count, http_status=226) 135 self.check_upload(env, vsftpd, docname=docname) 136 137 def _rmf(self, path): 138 if os.path.exists(path): 139 return os.remove(path) 140 141 # check with `tcpdump` if curl causes any TCP RST packets 142 @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available") 143 def test_30_06_shutdownh_download(self, env: Env, vsftpd: VsFTPD): 144 docname = 'data-1k' 145 curl = CurlClient(env=env) 146 count = 1 147 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}?[0-{count-1}]' 148 r = curl.ftp_get(urls=[url], with_stats=True, with_tcpdump=True) 149 r.check_stats(count=count, http_status=226) 150 assert r.tcpdump 151 # vsftp closes control connection without niceties, 152 # look only at ports from DATA connection. 153 data_ports = vsftpd.get_data_ports(r) 154 assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' 155 assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' 156 157 # check with `tcpdump` if curl causes any TCP RST packets 158 @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available") 159 def test_30_07_shutdownh_upload(self, env: Env, vsftpd: VsFTPD): 160 docname = 'upload-1k' 161 curl = CurlClient(env=env) 162 srcfile = os.path.join(env.gen_dir, docname) 163 dstfile = os.path.join(vsftpd.docs_dir, docname) 164 self._rmf(dstfile) 165 count = 1 166 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/' 167 r = curl.ftp_upload(urls=[url], fupload=f'{srcfile}', with_stats=True, with_tcpdump=True) 168 r.check_stats(count=count, http_status=226) 169 assert r.tcpdump 170 # vsftp closes control connection without niceties, 171 # look only at ports from DATA connection. 172 data_ports = vsftpd.get_data_ports(r) 173 assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' 174 assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' 175 176 def test_30_08_active_download(self, env: Env, vsftpd: VsFTPD): 177 docname = 'data-10k' 178 curl = CurlClient(env=env) 179 srcfile = os.path.join(vsftpd.docs_dir, f'{docname}') 180 count = 1 181 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}?[0-{count-1}]' 182 r = curl.ftp_get(urls=[url], with_stats=True, extra_args=[ 183 '--ftp-port', '127.0.0.1' 184 ]) 185 r.check_stats(count=count, http_status=226) 186 self.check_downloads(curl, srcfile, count) 187 188 def test_30_09_active_up_file(self, env: Env, vsftpd: VsFTPD): 189 docname = 'upload-1k' 190 curl = CurlClient(env=env) 191 srcfile = os.path.join(env.gen_dir, docname) 192 dstfile = os.path.join(vsftpd.docs_dir, docname) 193 self._rmf(dstfile) 194 count = 1 195 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/' 196 r = curl.ftp_upload(urls=[url], fupload=f'{srcfile}', with_stats=True, extra_args=[ 197 '--ftp-port', '127.0.0.1' 198 ]) 199 r.check_stats(count=count, http_status=226) 200 self.check_upload(env, vsftpd, docname=docname) 201 202 def test_30_10_active_up_ascii(self, env: Env, vsftpd: VsFTPD): 203 docname = 'upload-1k' 204 curl = CurlClient(env=env) 205 srcfile = os.path.join(env.gen_dir, docname) 206 dstfile = os.path.join(vsftpd.docs_dir, docname) 207 self._rmf(dstfile) 208 count = 1 209 url = f'ftp://{env.ftp_domain}:{vsftpd.port}/' 210 r = curl.ftp_upload(urls=[url], fupload=f'{srcfile}', with_stats=True, extra_args=[ 211 '--ftp-port', '127.0.0.1', '--use-ascii' 212 ]) 213 r.check_stats(count=count, http_status=226) 214 self.check_upload(env, vsftpd, docname=docname, binary=False) 215 216 def check_downloads(self, client, srcfile: str, count: int, 217 complete: bool = True): 218 for i in range(count): 219 dfile = client.download_file(i) 220 assert os.path.exists(dfile) 221 if complete and not filecmp.cmp(srcfile, dfile, shallow=False): 222 diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(), 223 b=open(dfile).readlines(), 224 fromfile=srcfile, 225 tofile=dfile, 226 n=1)) 227 assert False, f'download {dfile} differs:\n{diff}' 228 229 def check_upload(self, env, vsftpd: VsFTPD, docname, binary=True): 230 srcfile = os.path.join(env.gen_dir, docname) 231 dstfile = os.path.join(vsftpd.docs_dir, docname) 232 assert os.path.exists(srcfile) 233 assert os.path.exists(dstfile) 234 if not filecmp.cmp(srcfile, dstfile, shallow=False): 235 diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(), 236 b=open(dstfile).readlines(), 237 fromfile=srcfile, 238 tofile=dstfile, 239 n=1)) 240 assert not binary and len(diff) == 0, f'upload {dstfile} differs:\n{diff}'