mirror of
https://github.com/InfrastructureServices/vsftpd.git
synced 2025-04-19 01:24:02 +03:00
Updated to v2.0.6
This commit is contained in:
parent
c4e45fe8ac
commit
2b30cb98d0
40
Changelog
40
Changelog
@ -935,3 +935,43 @@ fix.
|
||||
- So, timezone and daylight are not available on BSD, so redo the whole TZ
|
||||
thing again. Should use only very portable constructs now.
|
||||
|
||||
At this point: v2.0.5 released!
|
||||
===============================
|
||||
|
||||
- Fix delay_failed_login typo. Oops.
|
||||
- Patch the getcwd and readlink sysutil helpers to reflect that they wouldn't
|
||||
like a 0-sized buf. No caller is affected. Thanks Ilja van Sprundel
|
||||
<ilja@suresec.org>.
|
||||
- Allow a (fake) reauth as the same user as the logged in user. Should resolve
|
||||
.NET related report from Sabo Jim <Jim.Sabo@thomson.net>.
|
||||
- Tweak from Lucian Adrian Grijincu <lucian.grijincu@gmail.com> to take
|
||||
unnecessary port calculations out of a loop.
|
||||
- Fix byte I/O accounting in the error path of do_file_send_rwloop, thanks to
|
||||
<echen@siac.com>.
|
||||
- Don't log FireFox's attempts to RETR directories! Reported by
|
||||
Nixdorf, Tim <tnixdorf@dnps.com>.
|
||||
- Fix STOU sending the same 150 status line twice - oops! Reported by
|
||||
<yamazaki@iij.ad.jp>.
|
||||
- Fix xferlog format for virtual (guest) users, reported by Andy Fletcher
|
||||
<andy@withnail.org>.
|
||||
- Fix bug with empty user list file and userlist_deny=NO. Reported by
|
||||
Marcin Zawadzki/GlobalVanet.com <marcin.zawadzki@globalvanet.com>.
|
||||
- Pretend we have proper UTF8 support and respond positively to OPTS UTF8 ON.
|
||||
Thanks Stanislav Maslovski <stanislav.maslovski@gmail.com>.
|
||||
- Add control over the file permissions used in the chown()ing of anonymous
|
||||
uploads: chown_upload_mode (default 0600 as before). Suggestion from
|
||||
An Pham <apham@medforcetech.com>.
|
||||
- Do a retry getting the active ftp socket in vsf_privop_get_ftp_port_sock();
|
||||
should help buggy Solaris systems. Reported by Michael Masterson
|
||||
<mjmasterson@xo.com>.
|
||||
- Add debug_ssl option to dump out some SSL connection details.
|
||||
- Use code 522, not 521, to indicate that the server requires an encrypted
|
||||
data connection. Still does not seem to coax lftp to retry :(
|
||||
- Recognize OPTS pre-login.
|
||||
- A whole ton of SSL improvements, including ability to force requirement of
|
||||
a client cert; data and control channel client cert cross checking. Ability
|
||||
to require fully valid / authentic client certs. No cert-based auth yet.
|
||||
- Change my e-mail to my GMail account.
|
||||
|
||||
At this point: v2.0.6 released!
|
||||
===============================
|
||||
|
2
Makefile
2
Makefile
@ -13,7 +13,7 @@ OBJS = main.o utility.o prelogin.o ftpcmdio.o postlogin.o privsock.o \
|
||||
postprivparent.o logging.o str.o netstr.o sysstr.o strlist.o \
|
||||
banner.o filestr.o parseconf.o secutil.o \
|
||||
ascii.o oneprocess.o twoprocess.o privops.o standalone.o hash.o \
|
||||
tcpwrap.o ipaddrparse.o access.o features.o readwrite.o \
|
||||
tcpwrap.o ipaddrparse.o access.o features.o readwrite.o opts.o \
|
||||
ssl.o sysutil.o sysdeputil.o
|
||||
|
||||
|
||||
|
4
README
4
README
@ -1,6 +1,6 @@
|
||||
This is vsftpd, version 2.0.5
|
||||
This is vsftpd, version 2.0.6
|
||||
Author: Chris Evans
|
||||
Contact: chris@scary.beasts.org
|
||||
Contact: scarybeasts@gmail.com
|
||||
Website: http://vsftpd.beasts.org/
|
||||
- All options are documented in the vsftpd.conf.5 manual page.
|
||||
- See the FAQ file for solutions to frequently asked questions.
|
||||
|
1
TODO
1
TODO
@ -13,7 +13,6 @@ NOT SO CRITICAL
|
||||
|
||||
- "add_group" support.
|
||||
- Implict SSL support (port 990?)
|
||||
- Do something with client certs.
|
||||
- Add CHLD handler to priv parent process to take care of session crashes?
|
||||
- Still reports FlashFXP broken when trying to do FXP.
|
||||
- Add negation, other support to regex handler.
|
||||
|
@ -42,6 +42,7 @@ handle_feat(struct vsf_session* p_sess)
|
||||
vsf_cmdio_write_raw(p_sess, " REST STREAM\r\n");
|
||||
vsf_cmdio_write_raw(p_sess, " SIZE\r\n");
|
||||
vsf_cmdio_write_raw(p_sess, " TVFS\r\n");
|
||||
vsf_cmdio_write_raw(p_sess, " UTF8\r\n");
|
||||
vsf_cmdio_write(p_sess, FTP_FEAT, "End");
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
#define FTP_MODEOK 200
|
||||
#define FTP_PBSZOK 200
|
||||
#define FTP_PROTOK 200
|
||||
#define FTP_OPTSOK 200
|
||||
#define FTP_ALLOOK 202
|
||||
#define FTP_FEAT 211
|
||||
#define FTP_STATOK 211
|
||||
@ -64,7 +65,7 @@
|
||||
#define FTP_BADMODE 504
|
||||
#define FTP_BADAUTH 504
|
||||
#define FTP_NOSUCHPROT 504
|
||||
#define FTP_NEEDENCRYPT 521
|
||||
#define FTP_NEEDENCRYPT 522
|
||||
#define FTP_EPSVBAD 522
|
||||
#define FTP_DATATLSBAD 522
|
||||
#define FTP_LOGINERR 530
|
||||
|
@ -478,13 +478,16 @@ do_file_send_rwloop(struct vsf_session* p_sess, int file_fd, int is_ascii)
|
||||
num_to_write = (unsigned int) retval;
|
||||
}
|
||||
retval = ftp_write_data(p_sess, p_writefrom_buf, num_to_write);
|
||||
if (!vsf_sysutil_retval_is_error(retval))
|
||||
{
|
||||
ret_struct.transferred += (unsigned int) retval;
|
||||
}
|
||||
if (vsf_sysutil_retval_is_error(retval) ||
|
||||
(unsigned int) retval != num_to_write)
|
||||
{
|
||||
ret_struct.retval = -2;
|
||||
return ret_struct;
|
||||
}
|
||||
ret_struct.transferred += (unsigned int) retval;
|
||||
}
|
||||
}
|
||||
|
||||
|
25
logging.c
25
logging.c
@ -105,6 +105,12 @@ vsf_log_entry_pending(struct vsf_session* p_sess)
|
||||
return 1;
|
||||
}
|
||||
|
||||
void
|
||||
vsf_log_clear_entry(struct vsf_session* p_sess)
|
||||
{
|
||||
p_sess->log_type = 0;
|
||||
}
|
||||
|
||||
void
|
||||
vsf_log_do_log(struct vsf_session* p_sess, int succeeded)
|
||||
{
|
||||
@ -211,14 +217,21 @@ vsf_log_do_log_wuftpd_format(struct vsf_session* p_sess, struct mystr* p_str,
|
||||
str_append_text(p_str, "o ");
|
||||
}
|
||||
/* Access mode: anonymous/real user, and identity */
|
||||
if (p_sess->is_anonymous)
|
||||
if (p_sess->is_anonymous && !p_sess->is_guest)
|
||||
{
|
||||
str_append_text(p_str, "a ");
|
||||
str_append_str(p_str, &p_sess->anon_pass_str);
|
||||
}
|
||||
else
|
||||
{
|
||||
str_append_text(p_str, "r ");
|
||||
if (p_sess->is_guest)
|
||||
{
|
||||
str_append_text(p_str, "g ");
|
||||
}
|
||||
else
|
||||
{
|
||||
str_append_text(p_str, "r ");
|
||||
}
|
||||
str_append_str(p_str, &p_sess->user_str);
|
||||
}
|
||||
str_append_char(p_str, ' ');
|
||||
@ -255,7 +268,7 @@ vsf_log_do_log_vsftpd_format(struct vsf_session* p_sess, struct mystr* p_str,
|
||||
}
|
||||
/* And the action */
|
||||
if (what != kVSFLogEntryFTPInput && what != kVSFLogEntryFTPOutput &&
|
||||
what != kVSFLogEntryConnection)
|
||||
what != kVSFLogEntryConnection && what != kVSFLogEntryDebug)
|
||||
{
|
||||
if (succeeded)
|
||||
{
|
||||
@ -301,6 +314,9 @@ vsf_log_do_log_vsftpd_format(struct vsf_session* p_sess, struct mystr* p_str,
|
||||
case kVSFLogEntryChmod:
|
||||
str_append_text(p_str, "CHMOD");
|
||||
break;
|
||||
case kVSFLogEntryDebug:
|
||||
str_append_text(p_str, "DEBUG");
|
||||
break;
|
||||
default:
|
||||
bug("bad entry_type in vsf_log_do_log");
|
||||
break;
|
||||
@ -320,7 +336,8 @@ vsf_log_do_log_vsftpd_format(struct vsf_session* p_sess, struct mystr* p_str,
|
||||
str_append_str(p_str, p_log_str);
|
||||
str_append_char(p_str, '"');
|
||||
}
|
||||
if (what != kVSFLogEntryFTPInput && what != kVSFLogEntryFTPOutput)
|
||||
if (what != kVSFLogEntryFTPInput && what != kVSFLogEntryFTPOutput &&
|
||||
what != kVSFLogEntryDebug)
|
||||
{
|
||||
if (p_sess->transfer_size)
|
||||
{
|
||||
|
@ -18,7 +18,8 @@ enum EVSFLogEntryType
|
||||
kVSFLogEntryDelete,
|
||||
kVSFLogEntryRename,
|
||||
kVSFLogEntryRmdir,
|
||||
kVSFLogEntryChmod
|
||||
kVSFLogEntryChmod,
|
||||
kVSFLogEntryDebug,
|
||||
};
|
||||
|
||||
/* vsf_log_init()
|
||||
@ -49,6 +50,12 @@ void vsf_log_start_entry(struct vsf_session* p_sess,
|
||||
*/
|
||||
int vsf_log_entry_pending(struct vsf_session* p_sess);
|
||||
|
||||
/* vsf_log_clear_entry()
|
||||
* PURPOSE
|
||||
* Clears any pending log entry.
|
||||
*/
|
||||
void vsf_log_clear_entry(struct vsf_session* p_sess);
|
||||
|
||||
/* vsf_log_do_log()
|
||||
* PURPOSE
|
||||
* Denote the end of a logged operation, specifying whether the operation
|
||||
|
10
main.c
10
main.c
@ -41,7 +41,7 @@ main(int argc, const char* argv[])
|
||||
/* Data connection */
|
||||
-1, 0, -1, 0, 0, 0, 0,
|
||||
/* Login */
|
||||
1, INIT_MYSTR, INIT_MYSTR,
|
||||
1, 0, INIT_MYSTR, INIT_MYSTR,
|
||||
/* Protocol state */
|
||||
0, 1, INIT_MYSTR, 0, 0,
|
||||
/* Session state */
|
||||
@ -61,7 +61,7 @@ main(int argc, const char* argv[])
|
||||
/* Home directory */
|
||||
INIT_MYSTR,
|
||||
/* Secure connection state */
|
||||
0, 0, 0, 0, 0, 0, -1, -1,
|
||||
0, 0, 0, 0, 0, INIT_MYSTR, 0, -1, -1,
|
||||
/* Login fails */
|
||||
0
|
||||
};
|
||||
@ -128,7 +128,7 @@ main(int argc, const char* argv[])
|
||||
vsf_sysutil_setproctitle_init(argc, argv);
|
||||
}
|
||||
/* Initialize the SSL system here if needed - saves the overhead of each
|
||||
* child doing this itself.
|
||||
* child doing this itself.
|
||||
*/
|
||||
if (tunable_ssl_enable)
|
||||
{
|
||||
@ -270,6 +270,10 @@ do_sanity_checks(void)
|
||||
{
|
||||
die("vsftpd: security: 'one_process_model' needs a better OS");
|
||||
}
|
||||
if (tunable_ssl_enable)
|
||||
{
|
||||
die("vsftpd: SSL mode not compatible with 'one_process_model'");
|
||||
}
|
||||
}
|
||||
if (!tunable_local_enable && !tunable_anonymous_enable)
|
||||
{
|
||||
|
27
opts.c
Normal file
27
opts.c
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Part of Very Secure FTPd
|
||||
* Licence: GPL v2
|
||||
* Author: Chris Evans
|
||||
* opts.c
|
||||
*
|
||||
* Routines to handle OPTS.
|
||||
*/
|
||||
|
||||
#include "ftpcodes.h"
|
||||
#include "ftpcmdio.h"
|
||||
#include "session.h"
|
||||
|
||||
void
|
||||
handle_opts(struct vsf_session* p_sess)
|
||||
{
|
||||
str_upper(&p_sess->ftp_arg_str);
|
||||
if (str_equal_text(&p_sess->ftp_arg_str, "UTF8 ON"))
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_OPTSOK, "Always in UTF8 mode.");
|
||||
}
|
||||
else
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_BADOPTS, "Option not understood.");
|
||||
}
|
||||
}
|
||||
|
9
opts.h
Normal file
9
opts.h
Normal file
@ -0,0 +1,9 @@
|
||||
#ifndef VSF_OPTS_H
|
||||
#define VSF_OPTS_H
|
||||
|
||||
struct vsf_session;
|
||||
|
||||
void handle_opts(struct vsf_session* p_sess);
|
||||
|
||||
#endif /* VSF_OPTS_H */
|
||||
|
@ -99,6 +99,9 @@ parseconf_bool_array[] =
|
||||
{ "mdtm_write", &tunable_mdtm_write },
|
||||
{ "lock_upload_files", &tunable_lock_upload_files },
|
||||
{ "pasv_addr_resolve", &tunable_pasv_addr_resolve },
|
||||
{ "debug_ssl", &tunable_debug_ssl },
|
||||
{ "require_cert", &tunable_require_cert },
|
||||
{ "validate_cert", &tunable_validate_cert },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
@ -128,6 +131,7 @@ parseconf_uint_array[] =
|
||||
{ "delay_failed_login", &tunable_delay_failed_login },
|
||||
{ "delay_successful_login", &tunable_delay_successful_login },
|
||||
{ "max_login_fails", &tunable_max_login_fails },
|
||||
{ "chown_upload_mode", &tunable_chown_upload_mode },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
@ -168,6 +172,7 @@ parseconf_str_array[] =
|
||||
{ "ssl_ciphers", &tunable_ssl_ciphers },
|
||||
{ "rsa_private_key_file", &tunable_rsa_private_key_file },
|
||||
{ "dsa_private_key_file", &tunable_dsa_private_key_file },
|
||||
{ "ca_certs_file", &tunable_ca_certs_file },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
|
71
postlogin.c
71
postlogin.c
@ -26,6 +26,7 @@
|
||||
#include "features.h"
|
||||
#include "ssl.h"
|
||||
#include "vsftpver.h"
|
||||
#include "opts.h"
|
||||
|
||||
/* Private local functions */
|
||||
static void handle_pwd(struct vsf_session* p_sess);
|
||||
@ -57,6 +58,8 @@ static void handle_help(struct vsf_session* p_sess);
|
||||
static void handle_stou(struct vsf_session* p_sess);
|
||||
static void handle_stat(struct vsf_session* p_sess);
|
||||
static void handle_stat_file(struct vsf_session* p_sess);
|
||||
static void handle_logged_in_user(struct vsf_session* p_sess);
|
||||
static void handle_logged_in_pass(struct vsf_session* p_sess);
|
||||
|
||||
static int pasv_active(struct vsf_session* p_sess);
|
||||
static int port_active(struct vsf_session* p_sess);
|
||||
@ -340,7 +343,7 @@ process_post_login(struct vsf_session* p_sess)
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "OPTS"))
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_BADOPTS, "Option not understood.");
|
||||
handle_opts(p_sess);
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "STAT") &&
|
||||
str_isempty(&p_sess->ftp_arg_str))
|
||||
@ -360,6 +363,14 @@ process_post_login(struct vsf_session* p_sess)
|
||||
{
|
||||
handle_prot(p_sess);
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "USER"))
|
||||
{
|
||||
handle_logged_in_user(p_sess);
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "PASS"))
|
||||
{
|
||||
handle_logged_in_pass(p_sess);
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "PASV") ||
|
||||
str_equal_text(&p_sess->ftp_cmd_str, "PORT") ||
|
||||
str_equal_text(&p_sess->ftp_cmd_str, "STOR") ||
|
||||
@ -499,6 +510,9 @@ handle_pasv(struct vsf_session* p_sess, int is_epsv)
|
||||
static struct vsf_sysutil_sockaddr* s_p_sockaddr;
|
||||
int bind_retries = 10;
|
||||
unsigned short the_port = 0;
|
||||
/* IPPORT_RESERVED */
|
||||
unsigned short min_port = 1024;
|
||||
unsigned short max_port = 65535;
|
||||
int is_ipv6 = vsf_sysutil_sockaddr_is_ipv6(p_sess->p_local_addr);
|
||||
if (is_epsv && !str_isempty(&p_sess->ftp_arg_str))
|
||||
{
|
||||
@ -528,21 +542,20 @@ handle_pasv(struct vsf_session* p_sess, int is_epsv)
|
||||
p_sess->pasv_listen_fd = vsf_sysutil_get_ipv4_sock();
|
||||
}
|
||||
vsf_sysutil_activate_reuseaddr(p_sess->pasv_listen_fd);
|
||||
|
||||
if (tunable_pasv_min_port > min_port && tunable_pasv_min_port <= max_port)
|
||||
{
|
||||
min_port = tunable_pasv_min_port;
|
||||
}
|
||||
if (tunable_pasv_max_port >= min_port && tunable_pasv_max_port < max_port)
|
||||
{
|
||||
max_port = tunable_pasv_max_port;
|
||||
}
|
||||
|
||||
while (--bind_retries)
|
||||
{
|
||||
int retval;
|
||||
double scaled_port;
|
||||
/* IPPORT_RESERVED */
|
||||
unsigned short min_port = 1024;
|
||||
unsigned short max_port = 65535;
|
||||
if (tunable_pasv_min_port > min_port && tunable_pasv_min_port <= max_port)
|
||||
{
|
||||
min_port = tunable_pasv_min_port;
|
||||
}
|
||||
if (tunable_pasv_max_port >= min_port && tunable_pasv_max_port < max_port)
|
||||
{
|
||||
max_port = tunable_pasv_max_port;
|
||||
}
|
||||
the_port = vsf_sysutil_get_random_byte();
|
||||
the_port <<= 8;
|
||||
the_port |= vsf_sysutil_get_random_byte();
|
||||
@ -657,6 +670,13 @@ handle_retr(struct vsf_session* p_sess)
|
||||
{
|
||||
/* Note - pretend open failed */
|
||||
vsf_cmdio_write(p_sess, FTP_FILEFAIL, "Failed to open file.");
|
||||
/* Irritating FireFox does RETR on directories, so avoid logging this
|
||||
* very common and noisy case.
|
||||
*/
|
||||
if (vsf_sysutil_statbuf_is_dir(s_p_statbuf))
|
||||
{
|
||||
vsf_log_clear_entry(p_sess);
|
||||
}
|
||||
goto file_close_out;
|
||||
}
|
||||
/* Now deactive O_NONBLOCK, otherwise we have a problem on DMAPI filesystems
|
||||
@ -1009,7 +1029,7 @@ handle_upload_common(struct vsf_session* p_sess, int is_append, int is_unique)
|
||||
/* Are we required to chown() this file for security? */
|
||||
if (p_sess->is_anonymous && tunable_chown_uploads)
|
||||
{
|
||||
vsf_sysutil_fchmod(new_file_fd, 0600);
|
||||
vsf_sysutil_fchmod(new_file_fd, tunable_chown_upload_mode);
|
||||
if (tunable_one_process_model)
|
||||
{
|
||||
vsf_one_process_chown_upload(p_sess, new_file_fd);
|
||||
@ -1034,7 +1054,6 @@ handle_upload_common(struct vsf_session* p_sess, int is_append, int is_unique)
|
||||
struct mystr resp_str = INIT_MYSTR;
|
||||
str_alloc_text(&resp_str, "FILE: ");
|
||||
str_append_str(&resp_str, p_filename);
|
||||
vsf_cmdio_write_str(p_sess, FTP_DATACONN, &resp_str);
|
||||
remote_fd = get_remote_transfer_fd(p_sess, str_getbuf(&resp_str));
|
||||
str_free(&resp_str);
|
||||
}
|
||||
@ -1684,7 +1703,9 @@ handle_stou(struct vsf_session* p_sess)
|
||||
static void
|
||||
get_unique_filename(struct mystr* p_outstr, const struct mystr* p_base_str)
|
||||
{
|
||||
/* Use silly wu-ftpd algorithm for compatibility */
|
||||
/* Use silly wu-ftpd algorithm for compatibility. It has races of course, if
|
||||
* two sessions are using the same file prefix at the same time.
|
||||
*/
|
||||
static struct vsf_sysutil_statbuf* s_p_statbuf;
|
||||
unsigned int suffix = 1;
|
||||
int retval;
|
||||
@ -1828,3 +1849,23 @@ resolve_tilde(struct mystr* p_str, struct vsf_session* p_sess)
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_logged_in_user(struct vsf_session* p_sess)
|
||||
{
|
||||
if (p_sess->is_anonymous)
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_LOGINERR, "Can't change from guest user.");
|
||||
}
|
||||
else if (str_equal(&p_sess->user_str, &p_sess->ftp_arg_str))
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_GIVEPWORD, "Any password will do.");
|
||||
}
|
||||
else
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_LOGINERR, "Can't change to another user.");
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_logged_in_pass(struct vsf_session* p_sess)
|
||||
{
|
||||
vsf_cmdio_write(p_sess, FTP_LOGINOK, "Already logged in.");
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "ssl.h"
|
||||
#include "features.h"
|
||||
#include "defs.h"
|
||||
#include "opts.h"
|
||||
|
||||
/* Functions used */
|
||||
static void emit_greeting(struct vsf_session* p_sess);
|
||||
@ -116,6 +117,10 @@ parse_username_password(struct vsf_session* p_sess)
|
||||
{
|
||||
handle_feat(p_sess);
|
||||
}
|
||||
else if (str_equal_text(&p_sess->ftp_cmd_str, "OPTS"))
|
||||
{
|
||||
handle_opts(p_sess);
|
||||
}
|
||||
else if (tunable_ssl_enable && str_equal_text(&p_sess->ftp_cmd_str, "AUTH"))
|
||||
{
|
||||
handle_auth(p_sess);
|
||||
@ -182,7 +187,7 @@ handle_user_command(struct vsf_session* p_sess)
|
||||
str_empty(&p_sess->user_str);
|
||||
return;
|
||||
}
|
||||
if (!str_isempty(&p_sess->userlist_str))
|
||||
if (tunable_userlist_enable)
|
||||
{
|
||||
int located = str_contains_line(&p_sess->userlist_str, &p_sess->user_str);
|
||||
if ((located && tunable_userlist_deny) ||
|
||||
|
25
privops.c
25
privops.c
@ -36,14 +36,29 @@ vsf_privop_get_ftp_port_sock(struct vsf_session* p_sess)
|
||||
{
|
||||
static struct vsf_sysutil_sockaddr* p_sockaddr;
|
||||
int retval;
|
||||
int i;
|
||||
int s = vsf_sysutil_get_ipsock(p_sess->p_local_addr);
|
||||
vsf_sysutil_activate_reuseaddr(s);
|
||||
vsf_sysutil_sockaddr_clone(&p_sockaddr, p_sess->p_local_addr);
|
||||
vsf_sysutil_sockaddr_set_port(p_sockaddr, tunable_ftp_data_port);
|
||||
retval = vsf_sysutil_bind(s, p_sockaddr);
|
||||
if (retval != 0)
|
||||
/* A report of failure here on Solaris, presumably buggy address reuse
|
||||
* support? We'll retry.
|
||||
*/
|
||||
for (i = 0; i < 2; ++i)
|
||||
{
|
||||
die("vsf_sysutil_bind");
|
||||
vsf_sysutil_sockaddr_clone(&p_sockaddr, p_sess->p_local_addr);
|
||||
vsf_sysutil_sockaddr_set_port(p_sockaddr, tunable_ftp_data_port);
|
||||
retval = vsf_sysutil_bind(s, p_sockaddr);
|
||||
if (retval == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
if (vsf_sysutil_get_error() != kVSFSysUtilErrADDRINUSE || i == 1)
|
||||
{
|
||||
die("vsf_sysutil_bind");
|
||||
}
|
||||
double sleep_for = vsf_sysutil_get_random_byte();
|
||||
sleep_for /= 256.0;
|
||||
sleep_for += 1.0;
|
||||
vsf_sysutil_sleep(sleep_for);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ struct vsf_session
|
||||
|
||||
/* Details of the login */
|
||||
int is_anonymous;
|
||||
int is_guest;
|
||||
struct mystr user_str;
|
||||
struct mystr anon_pass_str;
|
||||
|
||||
@ -87,6 +88,7 @@ struct vsf_session
|
||||
void* p_ssl_ctx;
|
||||
void* p_control_ssl;
|
||||
void* p_data_ssl;
|
||||
struct mystr control_cert_digest;
|
||||
int ssl_slave_active;
|
||||
int ssl_slave_fd;
|
||||
int ssl_consumer_fd;
|
||||
|
181
ssl.c
181
ssl.c
@ -20,6 +20,7 @@
|
||||
#include "tunables.h"
|
||||
#include "utility.h"
|
||||
#include "builddefs.h"
|
||||
#include "logging.h"
|
||||
|
||||
#ifdef VSF_BUILD_SSL
|
||||
|
||||
@ -34,8 +35,12 @@ static int ssl_session_init(struct vsf_session* p_sess);
|
||||
static void setup_bio_callbacks();
|
||||
static long bio_callback(
|
||||
BIO* p_bio, int oper, const char* p_arg, int argi, long argl, long retval);
|
||||
static int ssl_verify_callback(int verify_ok, X509_STORE_CTX* p_ctx);
|
||||
static int ssl_cert_digest(
|
||||
SSL* p_ssl, struct vsf_session* p_sess, struct mystr* p_str);
|
||||
|
||||
static int ssl_inited;
|
||||
static struct mystr debug_str;
|
||||
|
||||
void
|
||||
ssl_init(struct vsf_session* p_sess)
|
||||
@ -44,6 +49,7 @@ ssl_init(struct vsf_session* p_sess)
|
||||
{
|
||||
SSL_CTX* p_ctx;
|
||||
long options;
|
||||
int verify_option = 0;
|
||||
SSL_library_init();
|
||||
p_ctx = SSL_CTX_new(SSLv23_server_method());
|
||||
if (p_ctx == NULL)
|
||||
@ -105,6 +111,19 @@ ssl_init(struct vsf_session* p_sess)
|
||||
{
|
||||
die("SSL: RNG is not seeded");
|
||||
}
|
||||
verify_option = SSL_VERIFY_PEER;
|
||||
if (tunable_require_cert)
|
||||
{
|
||||
verify_option |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
|
||||
}
|
||||
SSL_CTX_set_verify(p_ctx, verify_option, ssl_verify_callback);
|
||||
if (tunable_ca_certs_file)
|
||||
{
|
||||
if (!SSL_CTX_load_verify_locations(p_ctx, tunable_ca_certs_file, NULL))
|
||||
{
|
||||
die("SSL: could not load verify file");
|
||||
}
|
||||
}
|
||||
p_sess->p_ssl_ctx = p_ctx;
|
||||
ssl_inited = 1;
|
||||
}
|
||||
@ -124,6 +143,7 @@ handle_auth(struct vsf_session* p_sess)
|
||||
{
|
||||
struct mystr err_str = INIT_MYSTR;
|
||||
str_alloc_text(&err_str, "Negotiation failed: ");
|
||||
/* Technically, we shouldn't leak such detailed error messages. */
|
||||
str_append_text(&err_str, get_ssl_error());
|
||||
vsf_cmdio_write_str(p_sess, FTP_TLS_FAIL, &err_str);
|
||||
vsf_sysutil_exit(0);
|
||||
@ -187,17 +207,26 @@ void
|
||||
ssl_getline(const struct vsf_session* p_sess, struct mystr* p_str,
|
||||
char end_char, char* p_buf, unsigned int buflen)
|
||||
{
|
||||
if (buflen == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
char* p_buf_start = p_buf;
|
||||
p_buf[buflen - 1] = '\0';
|
||||
p_buf[buflen - 1] = end_char;
|
||||
buflen--;
|
||||
while (1)
|
||||
{
|
||||
/* Should I use SSL_peek here? Won't lack of it break pipelining? (SSL
|
||||
* clients seem to work just fine, mind you, so maybe pipelining is banned
|
||||
* over SSL connections. Also note that OpenSSL didn't always have
|
||||
* SSL_peek).
|
||||
*/
|
||||
int retval = SSL_read(p_sess->p_control_ssl, p_buf, buflen);
|
||||
if (retval <= 0)
|
||||
{
|
||||
die("SSL_read");
|
||||
}
|
||||
p_buf[retval] = '\0';
|
||||
p_buf[retval] = end_char;
|
||||
buflen -= retval;
|
||||
if (p_buf[retval - 1] == end_char || buflen == 0)
|
||||
{
|
||||
@ -250,9 +279,27 @@ ssl_write_str(void* p_ssl, const struct mystr* p_str)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
ssl_data_close(struct vsf_session* p_sess)
|
||||
{
|
||||
if (p_sess->p_data_ssl)
|
||||
{
|
||||
SSL_free(p_sess->p_data_ssl);
|
||||
p_sess->p_data_ssl = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
ssl_accept(struct vsf_session* p_sess, int fd)
|
||||
{
|
||||
/* SECURITY: data SSL connections don't have any auth on them as part of the
|
||||
* protocol. If a client sends an unfortunately optional client cert then
|
||||
* we can check for a match between the control and data connections.
|
||||
*/
|
||||
if (p_sess->p_data_ssl != NULL)
|
||||
{
|
||||
die("p_data_ssl should be NULL.");
|
||||
}
|
||||
SSL* p_ssl = get_ssl(p_sess, fd);
|
||||
if (p_ssl == NULL)
|
||||
{
|
||||
@ -260,15 +307,38 @@ ssl_accept(struct vsf_session* p_sess, int fd)
|
||||
}
|
||||
p_sess->p_data_ssl = p_ssl;
|
||||
setup_bio_callbacks(p_ssl);
|
||||
if (str_getlen(&p_sess->control_cert_digest) > 0)
|
||||
{
|
||||
static struct mystr data_cert_digest;
|
||||
if (!ssl_cert_digest(p_ssl, p_sess, &data_cert_digest))
|
||||
{
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "Missing cert on data channel.");
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
ssl_data_close(p_sess);
|
||||
return 0;
|
||||
}
|
||||
if (str_strcmp(&p_sess->control_cert_digest, &data_cert_digest))
|
||||
{
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "DIFFERENT cert on data channel.");
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
ssl_data_close(p_sess);
|
||||
return 0;
|
||||
}
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "Matching cert on data channel.");
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void
|
||||
ssl_data_close(struct vsf_session* p_sess)
|
||||
{
|
||||
SSL_free(p_sess->p_data_ssl);
|
||||
}
|
||||
|
||||
void
|
||||
ssl_comm_channel_init(struct vsf_session* p_sess)
|
||||
{
|
||||
@ -284,19 +354,66 @@ get_ssl(struct vsf_session* p_sess, int fd)
|
||||
SSL* p_ssl = SSL_new(p_sess->p_ssl_ctx);
|
||||
if (p_ssl == NULL)
|
||||
{
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "SSL_new failed");
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (!SSL_set_fd(p_ssl, fd))
|
||||
{
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "SSL_set_fd failed");
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
SSL_free(p_ssl);
|
||||
return NULL;
|
||||
}
|
||||
if (SSL_accept(p_ssl) != 1)
|
||||
{
|
||||
die(get_ssl_error());
|
||||
const char* p_err = get_ssl_error();
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
str_alloc_text(&debug_str, "SSL_accept failed: ");
|
||||
str_append_text(&debug_str, p_err);
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
die(p_err);
|
||||
SSL_free(p_ssl);
|
||||
return NULL;
|
||||
}
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
const char* p_ssl_version = SSL_get_cipher_version(p_ssl);
|
||||
SSL_CIPHER* p_ssl_cipher = SSL_get_current_cipher(p_ssl);
|
||||
const char* p_cipher_name = SSL_CIPHER_get_name(p_ssl_cipher);
|
||||
int reused = SSL_session_reused(p_ssl);
|
||||
X509* p_ssl_cert = SSL_get_peer_certificate(p_ssl);
|
||||
str_alloc_text(&debug_str, "SSL version: ");
|
||||
str_append_text(&debug_str, p_ssl_version);
|
||||
str_append_text(&debug_str, ", SSL cipher: ");
|
||||
str_append_text(&debug_str, p_cipher_name);
|
||||
if (reused)
|
||||
{
|
||||
str_append_text(&debug_str, ", reused");
|
||||
}
|
||||
else
|
||||
{
|
||||
str_append_text(&debug_str, ", not reused");
|
||||
}
|
||||
if (p_ssl_cert != NULL)
|
||||
{
|
||||
str_append_text(&debug_str, ", CERT PRESENTED");
|
||||
X509_free(p_ssl_cert);
|
||||
}
|
||||
else
|
||||
{
|
||||
str_append_text(&debug_str, ", no cert");
|
||||
}
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
return p_ssl;
|
||||
}
|
||||
|
||||
@ -309,10 +426,45 @@ ssl_session_init(struct vsf_session* p_sess)
|
||||
return 0;
|
||||
}
|
||||
p_sess->p_control_ssl = p_ssl;
|
||||
(void) ssl_cert_digest(p_ssl, p_sess, &p_sess->control_cert_digest);
|
||||
setup_bio_callbacks(p_ssl);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
ssl_cert_digest(SSL* p_ssl, struct vsf_session* p_sess, struct mystr* p_str)
|
||||
{
|
||||
X509* p_cert = SSL_get_peer_certificate(p_ssl);
|
||||
unsigned int num_bytes = 0;
|
||||
if (p_cert == NULL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
str_reserve(p_str, EVP_MAX_MD_SIZE);
|
||||
str_empty(p_str);
|
||||
str_rpad(p_str, EVP_MAX_MD_SIZE);
|
||||
if (!X509_digest(p_cert, EVP_sha256(), (unsigned char*) str_getbuf(p_str),
|
||||
&num_bytes))
|
||||
{
|
||||
die("X509_digest failed");
|
||||
}
|
||||
X509_free(p_cert);
|
||||
if (tunable_debug_ssl)
|
||||
{
|
||||
unsigned int i;
|
||||
str_alloc_text(&debug_str, "Cert digest:");
|
||||
for (i = 0; i < num_bytes; ++i)
|
||||
{
|
||||
str_append_char(&debug_str, ' ');
|
||||
str_append_ulong(
|
||||
&debug_str, (unsigned long) (unsigned char) str_get_char_at(p_str, i));
|
||||
}
|
||||
vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
|
||||
}
|
||||
str_trunc(p_str, num_bytes);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static char*
|
||||
get_ssl_error()
|
||||
{
|
||||
@ -347,6 +499,17 @@ bio_callback(
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
ssl_verify_callback(int verify_ok, X509_STORE_CTX* p_ctx)
|
||||
{
|
||||
(void) p_ctx;
|
||||
if (tunable_validate_cert)
|
||||
{
|
||||
return verify_ok;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
#else /* VSF_BUILD_SSL */
|
||||
|
||||
void
|
||||
|
@ -917,6 +917,9 @@ vsf_sysutil_isdigit(int the_char)
|
||||
char*
|
||||
vsf_sysutil_getcwd(char* p_dest, const unsigned int buf_size)
|
||||
{
|
||||
if (buf_size == 0) {
|
||||
return p_dest;
|
||||
}
|
||||
char* p_retval = getcwd(p_dest, buf_size);
|
||||
p_dest[buf_size - 1] = '\0';
|
||||
return p_retval;
|
||||
@ -1512,6 +1515,9 @@ vsf_sysutil_unlock_file(int fd)
|
||||
int
|
||||
vsf_sysutil_readlink(const char* p_filename, char* p_dest, unsigned int bufsiz)
|
||||
{
|
||||
if (bufsiz == 0) {
|
||||
return -1;
|
||||
}
|
||||
int retval = readlink(p_filename, p_dest, bufsiz - 1);
|
||||
if (retval < 0)
|
||||
{
|
||||
|
@ -71,6 +71,9 @@ int tunable_force_anon_data_ssl = 0;
|
||||
int tunable_mdtm_write = 1;
|
||||
int tunable_lock_upload_files = 1;
|
||||
int tunable_pasv_addr_resolve = 0;
|
||||
int tunable_debug_ssl = 0;
|
||||
int tunable_require_cert = 0;
|
||||
int tunable_validate_cert = 0;
|
||||
|
||||
unsigned int tunable_accept_timeout = 60;
|
||||
unsigned int tunable_connect_timeout = 60;
|
||||
@ -94,6 +97,8 @@ unsigned int tunable_trans_chunk_size = 0;
|
||||
unsigned int tunable_delay_failed_login = 1;
|
||||
unsigned int tunable_delay_successful_login = 0;
|
||||
unsigned int tunable_max_login_fails = 3;
|
||||
/* -rw------- */
|
||||
unsigned int tunable_chown_upload_mode = 0600;
|
||||
|
||||
const char* tunable_secure_chroot_dir = "/usr/share/empty";
|
||||
const char* tunable_ftp_username = "ftp";
|
||||
@ -125,4 +130,5 @@ const char* tunable_dsa_cert_file = 0;
|
||||
const char* tunable_ssl_ciphers = "DES-CBC3-SHA";
|
||||
const char* tunable_rsa_private_key_file = 0;
|
||||
const char* tunable_dsa_private_key_file = 0;
|
||||
const char* tunable_ca_certs_file = 0;
|
||||
|
||||
|
@ -67,6 +67,9 @@ extern int tunable_force_anon_data_ssl; /* Require anon data uses SSL */
|
||||
extern int tunable_mdtm_write; /* Allow MDTM to set timestamps */
|
||||
extern int tunable_lock_upload_files; /* Lock uploading files */
|
||||
extern int tunable_pasv_addr_resolve; /* DNS resolve pasv_addr */
|
||||
extern int tunable_debug_ssl; /* Verbose SSL logging */
|
||||
extern int tunable_require_cert; /* SSL client cert required */
|
||||
extern int tunable_validate_cert; /* SSL certs must be valid */
|
||||
|
||||
/* Integer/numeric defines */
|
||||
extern unsigned int tunable_accept_timeout;
|
||||
@ -88,6 +91,7 @@ extern unsigned int tunable_trans_chunk_size;
|
||||
extern unsigned int tunable_delay_failed_login;
|
||||
extern unsigned int tunable_delay_successful_login;
|
||||
extern unsigned int tunable_max_login_fails;
|
||||
extern unsigned int tunable_chown_upload_mode;
|
||||
|
||||
/* String defines */
|
||||
extern const char* tunable_secure_chroot_dir;
|
||||
@ -120,6 +124,7 @@ extern const char* tunable_dsa_cert_file;
|
||||
extern const char* tunable_ssl_ciphers;
|
||||
extern const char* tunable_rsa_private_key_file;
|
||||
extern const char* tunable_dsa_private_key_file;
|
||||
extern const char* tunable_ca_certs_file;
|
||||
|
||||
#endif /* VSF_TUNABLES_H */
|
||||
|
||||
|
@ -282,6 +282,7 @@ process_login_req(struct vsf_session* p_sess)
|
||||
static void
|
||||
process_ssl_slave_req(struct vsf_session* p_sess)
|
||||
{
|
||||
priv_sock_send_str(p_sess->ssl_slave_fd, &p_sess->control_cert_digest);
|
||||
while (1)
|
||||
{
|
||||
char cmd = priv_sock_get_cmd(p_sess->ssl_slave_fd);
|
||||
@ -342,9 +343,15 @@ common_do_login(struct vsf_session* p_sess, const struct mystr* p_user_str,
|
||||
if (tunable_ssl_enable)
|
||||
{
|
||||
vsf_sysutil_close(p_sess->ssl_slave_fd);
|
||||
if (p_sess->ssl_slave_active)
|
||||
{
|
||||
priv_sock_get_str(p_sess->ssl_consumer_fd,
|
||||
&p_sess->control_cert_digest);
|
||||
}
|
||||
}
|
||||
if (tunable_guest_enable && !anon)
|
||||
{
|
||||
p_sess->is_guest = 1;
|
||||
/* Remap to the guest user */
|
||||
str_alloc_text(&guest_user_str, tunable_guest_username);
|
||||
p_user_str = &guest_user_str;
|
||||
|
@ -163,6 +163,12 @@ slightly less privilege.
|
||||
|
||||
Default: NO (but the sample config file enables it)
|
||||
.TP
|
||||
.B debug_ssl
|
||||
If true, OpenSSL connection diagnostics are dumped to the vsftpd log file.
|
||||
(Added in v2.0.6).
|
||||
|
||||
Default: NO
|
||||
.TP
|
||||
.B deny_email_enable
|
||||
If activated, you may provide a list of anonymous password e-mail responses
|
||||
which cause login to be denied. By default, the file containing this list is
|
||||
@ -282,7 +288,7 @@ downloads proceed with a shared read lock on the download file. WARNING!
|
||||
Before enabling this, be aware that malicious readers could starve a writer
|
||||
wanting to e.g. append a file.
|
||||
|
||||
Default: NO
|
||||
Default: YES
|
||||
.TP
|
||||
.B log_ftp_protocol
|
||||
When enabled, all FTP requests and responses are logged, providing the option
|
||||
@ -369,6 +375,15 @@ Set to YES if you want to disable the PORT security check that ensures that
|
||||
outgoing data connections can only connect to the client. Only enable if
|
||||
you know what you are doing!
|
||||
|
||||
Default: NO
|
||||
.TP
|
||||
.B require_cert
|
||||
If set to yes, all SSL client connections are required to present a client
|
||||
certificate. The degree of validation applied to this certificate is
|
||||
controlled by
|
||||
.BR validate_cert
|
||||
(Added in v2.0.6).
|
||||
|
||||
Default: NO
|
||||
.TP
|
||||
.B run_as_launching_user
|
||||
@ -518,6 +533,12 @@ before they are asked for a password. This may be useful in preventing
|
||||
cleartext passwords being transmitted. See also
|
||||
.BR userlist_deny .
|
||||
|
||||
Default: NO
|
||||
.TP
|
||||
.B validate_cert
|
||||
If set to yes, all SSL client certificates received must validate OK.
|
||||
Self-signed certs do not constitute OK validation. (New in v2.0.6).
|
||||
|
||||
Default: NO
|
||||
.TP
|
||||
.B virtual_use_local_privs
|
||||
@ -575,6 +596,11 @@ value will be treated as a base 10 integer!
|
||||
|
||||
Default: 077
|
||||
.TP
|
||||
.B chown_upload_mode
|
||||
The file mode to force for chown()ed anonymous uploads. (Added in v2.0.6).
|
||||
|
||||
Default: 0600
|
||||
.TP
|
||||
.B connect_timeout
|
||||
The timeout, in seconds, for a remote client to respond to our PORT style
|
||||
data connection.
|
||||
@ -588,12 +614,12 @@ client is kicked off.
|
||||
|
||||
Default: 300
|
||||
.TP
|
||||
.B delay_failed_logins
|
||||
.B delay_failed_login
|
||||
The number of seconds to pause prior to reporting a failed login.
|
||||
|
||||
Default: 1
|
||||
.TP
|
||||
.B delay_successful_logins
|
||||
.B delay_successful_login
|
||||
The number of seconds to pause prior to allowing a successful login.
|
||||
|
||||
Default: 0
|
||||
@ -700,6 +726,14 @@ the
|
||||
.BR ftpd_banner
|
||||
option.
|
||||
|
||||
Default: (none)
|
||||
.TP
|
||||
.B ca_certs_file
|
||||
This option is the name of a file to load Certificate Authority certs from, for
|
||||
the purpose of validating client certs. Regrettably, the default SSL CA cert
|
||||
paths are not used, because of vsftpd's use of restricted filesystem spaces
|
||||
(chroot). (Added in v2.0.6).
|
||||
|
||||
Default: (none)
|
||||
.TP
|
||||
.B chown_username
|
||||
@ -962,5 +996,5 @@ Alternatively, it is written if you have set the option
|
||||
Default: /var/log/xferlog
|
||||
|
||||
.SH AUTHOR
|
||||
chris@scary.beasts.org
|
||||
scarybeasts@gmail.com
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
#ifndef VSF_VERSION_H
|
||||
#define VSF_VERSION_H
|
||||
|
||||
#define VSF_VERSION "2.0.5"
|
||||
#define VSF_VERSION "2.0.6"
|
||||
|
||||
#endif /* VSF_VERSION_H */
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user