xref: /curl/tests/http/testenv/vsftpd.py (revision 5a913d8d)
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#
27import inspect
28import logging
29import os
30import subprocess
31from datetime import timedelta, datetime
32from json import JSONEncoder
33import time
34from typing import List, Union, Optional
35
36from .curl import CurlClient, ExecResult
37from .env import Env
38
39
40log = logging.getLogger(__name__)
41
42
43class VsFTPD:
44
45    def __init__(self, env: Env, with_ssl=False):
46        self.env = env
47        self._cmd = env.vsftpd
48        self._scheme = 'ftp'
49        self._with_ssl = with_ssl
50        if self._with_ssl:
51            self._port = self.env.ftps_port
52            name = 'vsftpds'
53        else:
54            self._port = self.env.ftp_port
55            name = 'vsftpd'
56        self._vsftpd_dir = os.path.join(env.gen_dir, name)
57        self._run_dir = os.path.join(self._vsftpd_dir, 'run')
58        self._docs_dir = os.path.join(self._vsftpd_dir, 'docs')
59        self._tmp_dir = os.path.join(self._vsftpd_dir, 'tmp')
60        self._conf_file = os.path.join(self._vsftpd_dir, 'test.conf')
61        self._pid_file = os.path.join(self._vsftpd_dir, 'vsftpd.pid')
62        self._error_log = os.path.join(self._vsftpd_dir, 'vsftpd.log')
63        self._process = None
64
65        self.clear_logs()
66
67    @property
68    def domain(self):
69        return self.env.ftp_domain
70
71    @property
72    def docs_dir(self):
73        return self._docs_dir
74
75    @property
76    def port(self) -> str:
77        return self._port
78
79    def clear_logs(self):
80        self._rmf(self._error_log)
81
82    def exists(self):
83        return os.path.exists(self._cmd)
84
85    def is_running(self):
86        if self._process:
87            self._process.poll()
88            return self._process.returncode is None
89        return False
90
91    def start_if_needed(self):
92        if not self.is_running():
93            return self.start()
94        return True
95
96    def start(self, wait_live=True):
97        pass
98
99    def stop_if_running(self):
100        if self.is_running():
101            return self.stop()
102        return True
103
104    def stop(self, wait_dead=True):
105        self._mkpath(self._tmp_dir)
106        if self._process:
107            self._process.terminate()
108            self._process.wait(timeout=2)
109            self._process = None
110            return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
111        return True
112
113    def restart(self):
114        self.stop()
115        return self.start()
116
117    def start(self, wait_live=True):
118        self._mkpath(self._tmp_dir)
119        if self._process:
120            self.stop()
121        self._write_config()
122        args = [
123            self._cmd,
124            f'{self._conf_file}',
125        ]
126        procerr = open(self._error_log, 'a')
127        self._process = subprocess.Popen(args=args, stderr=procerr)
128        if self._process.returncode is not None:
129            return False
130        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
131
132    def wait_dead(self, timeout: timedelta):
133        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
134        try_until = datetime.now() + timeout
135        while datetime.now() < try_until:
136            check_url = f'{self._scheme}://{self.domain}:{self.port}/'
137            r = curl.ftp_get(urls=[check_url], extra_args=['-v'])
138            if r.exit_code != 0:
139                return True
140            log.debug(f'waiting for vsftpd to stop responding: {r}')
141            time.sleep(.1)
142        log.debug(f"Server still responding after {timeout}")
143        return False
144
145    def wait_live(self, timeout: timedelta):
146        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
147        try_until = datetime.now() + timeout
148        while datetime.now() < try_until:
149            check_url = f'{self._scheme}://{self.domain}:{self.port}/'
150            r = curl.ftp_get(urls=[check_url], extra_args=[
151                '--trace', 'curl-start.trace', '--trace-time'
152            ])
153            if r.exit_code == 0:
154                return True
155            log.debug(f'waiting for vsftpd to become responsive: {r}')
156            time.sleep(.1)
157        log.error(f"Server still not responding after {timeout}")
158        return False
159
160    def _run(self, args, intext=''):
161        env = {}
162        for key, val in os.environ.items():
163            env[key] = val
164        with open(self._error_log, 'w') as cerr:
165            self._process = subprocess.run(args, stderr=cerr, stdout=cerr,
166                                           cwd=self._vsftpd_dir,
167                                           input=intext.encode() if intext else None,
168                                           env=env)
169            start = datetime.now()
170            return ExecResult(args=args, exit_code=self._process.returncode,
171                              duration=datetime.now() - start)
172
173    def _rmf(self, path):
174        if os.path.exists(path):
175            return os.remove(path)
176
177    def _mkpath(self, path):
178        if not os.path.exists(path):
179            return os.makedirs(path)
180
181    def _write_config(self):
182        self._mkpath(self._docs_dir)
183        self._mkpath(self._tmp_dir)
184        conf = [  # base server config
185            f'listen=YES',
186            f'run_as_launching_user=YES',
187            f'#listen_address=127.0.0.1',
188            f'listen_port={self.port}',
189            f'local_enable=NO',
190            f'anonymous_enable=YES',
191            f'anon_root={self._docs_dir}',
192            f'dirmessage_enable=YES',
193            f'write_enable=YES',
194            f'anon_upload_enable=YES',
195            f'log_ftp_protocol=YES',
196            f'xferlog_enable=YES',
197            f'xferlog_std_format=NO',
198            f'vsftpd_log_file={self._error_log}',
199            f'\n',
200        ]
201        if self._with_ssl:
202            creds = self.env.get_credentials(self.domain)
203            conf.extend([
204                f'ssl_enable=YES',
205                f'debug_ssl=YES',
206                f'allow_anon_ssl=YES',
207                f'rsa_cert_file={creds.cert_file}',
208                f'rsa_private_key_file={creds.pkey_file}',
209                # require_ssl_reuse=YES means ctrl and data connection need to use the same session
210                f'require_ssl_reuse=NO',
211            ])
212
213        with open(self._conf_file, 'w') as fd:
214            fd.write("\n".join(conf))
215