/* $Id: ftp.c,v 1.21 2003/01/11 15:54:09 ukai Exp $ */
#include <stdio.h>
#include <pwd.h>
#include <Str.h>
#include <signal.h>
#include <setjmp.h>
#include <time.h>
#include "fm.h"
#include "html.h"
#include "myctype.h"
#ifdef DEBUG
#include <malloc.h>
#endif /* DEBUG */
#include <sys/socket.h>
#if defined(FTPPASS_HOSTNAMEGEN) || defined(INET6)
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#endif
typedef struct _FTP {
char *host;
int port;
char *user;
char *pass;
InputStream rf;
FILE *wf;
FILE *data;
} *FTP;
static struct _FTP current_ftp = {
NULL, 0, NULL, NULL, NULL, NULL, NULL
};
static Str
ftp_command(FTP ftp, char *cmd, char *arg, int *status)
{
Str tmp;
if (!ftp->host)
return NULL;
if (cmd) {
if (arg)
tmp = Sprintf("%s %s\r\n", cmd, arg);
else
tmp = Sprintf("%s\r\n", cmd);
fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wf);
fflush(ftp->wf);
}
if (!status)
return NULL;
*status = -1; /* error */
tmp = StrISgets(ftp->rf);
if (IS_DIGIT(tmp->ptr[0]) && IS_DIGIT(tmp->ptr[1]) &&
IS_DIGIT(tmp->ptr[2]) && tmp->ptr[3] == ' ')
sscanf(tmp->ptr, "%d", status);
if (tmp->ptr[3] != '-')
return tmp;
/* RFC959 4.2 FTP REPLIES */
/* multi-line response start */
/*
* Thus the format for multi-line replies is that the
* first line will begin with the exact required reply
* code, followed immediately by a Hyphen, "-" (also known
* as Minus), followed by text. The last line will begin
* with the same code, followed immediately by Space <SP>,
* optionally some text, and the Telnet end-of-line code. */
while (1) {
tmp = StrISgets(ftp->rf);
if (IS_DIGIT(tmp->ptr[0]) && IS_DIGIT(tmp->ptr[1]) &&
IS_DIGIT(tmp->ptr[2]) && tmp->ptr[3] == ' ') {
sscanf(tmp->ptr, "%d", status);
break;
}
}
return tmp;
}
static void
ftp_close(FTP ftp)
{
if (!ftp->host)
return;
if (ftp->rf) {
IStype(ftp->rf) &= ~IST_UNCLOSE;
ISclose(ftp->rf);
ftp->rf = NULL;
}
if (ftp->wf) {
fclose(ftp->wf);
ftp->wf = NULL;
}
if (ftp->data) {
fclose(ftp->data);
ftp->data = NULL;
}
ftp->host = NULL;
return;
}
static int
ftp_login(FTP ftp)
{
Str tmp;
int sock, status;
sock = openSocket(ftp->host, "ftp", 21);
if (sock < 0)
goto open_err;
#ifdef FTPPASS_HOSTNAMEGEN
if (ftppass_hostnamegen && !strcmp(ftp->user, "anonymous")) {
size_t n = strlen(ftp->pass);
if (n > 0 && ftp->pass[n - 1] == '@') {
struct sockaddr_in sockname;
int socknamelen = sizeof(sockname);
if (!getsockname(sock, (struct sockaddr *)&sockname,
&socknamelen)) {
struct hostent *sockent;
tmp = Strnew_charp(ftp->pass);
if ((sockent = gethostbyaddr((char *)&sockname.sin_addr,
sizeof(sockname.sin_addr),
sockname.sin_family)))
Strcat_charp(tmp, sockent->h_name);
else
Strcat_m_charp(tmp, "[", inet_ntoa(sockname.sin_addr),
"]", NULL);
ftp->pass = tmp->ptr;
}
}
}
#endif
ftp->rf = newInputStream(sock);
ftp->wf = fdopen(dup(sock), "wb");
if (!ftp->rf || !ftp->wf)
goto open_err;
IStype(ftp->rf) |= IST_UNCLOSE;
ftp_command(ftp, NULL, NULL, &status);
if (status != 220)
goto open_err;
if (fmInitialized) {
message(Sprintf("Sending FTP username (%s) to remote server.",
ftp->user)->ptr, 0, 0);
refresh();
}
ftp_command(ftp, "USER", ftp->user, &status);
/*
* Some ftp daemons(e.g. publicfile) return code 230 for user command.
*/
if (status == 230)
goto succeed;
if (status != 331)
goto open_err;
if (fmInitialized) {
message("Sending FTP password to remote server.", 0, 0);
refresh();
}
ftp_command(ftp, "PASS", ftp->pass, &status);
if (status != 230)
goto open_err;
succeed:
return TRUE;
open_err:
ftp_close(ftp);
return FALSE;
}
static int
ftp_pasv(FTP ftp)
{
int status;
int n1, n2, n3, n4, p1, p2;
int data;
char *p;
Str tmp;
int family;
#ifdef INET6
struct sockaddr_storage sockaddr;
int sockaddrlen, port;
unsigned char d1, d2, d3, d4;
char abuf[INET6_ADDRSTRLEN];
#endif
#ifdef INET6
sockaddrlen = sizeof(sockaddr);
if (getpeername(fileno(ftp->wf),
(struct sockaddr *)&sockaddr, &sockaddrlen) < 0)
return -1;
family = sockaddr.ss_family;
#else
family = AF_INET;
#endif
switch (family) {
#ifdef INET6
case AF_INET6:
ftp_command(ftp, "EPSV", NULL, &status);
if (status != 229)
return -1;
for (p = tmp->ptr + 4; *p && *p != '('; p++) ;
if (*p == '\0')
return -1;
if (sscanf(++p, "%c%c%c%d%c", &d1, &d2, &d3, &port, &d4) != 5
|| d1 != d2 || d1 != d3 || d1 != d4)
return -1;
if (getnameinfo((struct sockaddr *)&sockaddr, sockaddrlen,
abuf, sizeof(abuf), NULL, 0, NI_NUMERICHOST) != 0)
return -1;
data = openSocket(abuf, "", port);
break;
#endif
case AF_INET:
tmp = ftp_command(ftp, "PASV", NULL, &status);
if (status != 227)
return -1;
for (p = tmp->ptr + 4; *p && !IS_DIGIT(*p); p++) ;
if (*p == '\0')
return -1;
sscanf(p, "%d,%d,%d,%d,%d,%d", &n1, &n2, &n3, &n4, &p1, &p2);
tmp = Sprintf("%d.%d.%d.%d", n1, n2, n3, n4);
data = openSocket(tmp->ptr, "", p1 * 256 + p2);
break;
default:
return -1;
}
if (data < 0)
return -1;
ftp->data = fdopen(data, "rb");
return 0;
}
static time_t
ftp_modtime(FTP ftp, char *path)
{
int status;
Str tmp;
char *p;
struct tm tm;
time_t t, lt, gt;
tmp = ftp_command(ftp, "MDTM", path, &status);
if (status != 213)
return -1;
for (p = tmp->ptr + 4; *p && *p == ' '; p++) ;
memset(&tm, 0, sizeof(struct tm));
if (sscanf(p, "%04d%02d%02d%02d%02d%02d",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec) < 6)
return -1;
tm.tm_year -= 1900;
tm.tm_mon--;
t = mktime(&tm);
lt = mktime(localtime(&t));
gt = mktime(gmtime(&t));
return t + (lt - gt);
}
static int
ftp_quit(FTP ftp)
{
/*
int status;
ftp_command(ftp, "QUIT", NULL, &status);
ftp_close(ftp);
if (status != 221)
return -1;
*/
ftp_command(ftp, "QUIT", NULL, NULL);
ftp_close(ftp);
return 0;
}
static int ex_ftpdir_name_size_date(char *, char **, char **, char **);
#define SERVER_NONE 0
#define UNIXLIKE_SERVER 1
#define FTPDIR_NONE 0
#define FTPDIR_DIR 1
#define FTPDIR_LINK 2
#define FTPDIR_FILE 3
static void
closeFTPdata(FILE *f)
{
int status;
if (f) {
fclose(f);
if (f == current_ftp.data)
current_ftp.data = NULL;
}
ftp_command(¤t_ftp, NULL, NULL, &status);
/* status == 226 */
}
void
closeFTP(void)
{
ftp_close(¤t_ftp);
}
InputStream
openFTPStream(ParsedURL *pu, URLFile *uf)
{
Str tmp;
int status;
char *user = NULL;
char *pass = NULL;
Str pwd = NULL;
int add_auth_cookie_flag = FALSE;
char *realpathname = NULL;
if (!pu->host)
return NULL;
if (pu->user == NULL && pu->pass == NULL) {
Str uname, pwd;
if (find_auth_user_passwd(pu, NULL, &uname, &pwd, 0)) {
if (uname)
user = uname->ptr;
if (pwd)
pass = pwd->ptr;
}
}
if (user)
/* do nothing */ ;
else if (pu->user)
user = pu->user;
else
user = "anonymous";
if (current_ftp.host) {
if (!strcmp(current_ftp.host, pu->host) &&
current_ftp.port == pu->port &&
!strcmp(current_ftp.user, user)) {
ftp_command(¤t_ftp, "NOOP", NULL, &status);
if (status != 200)
ftp_close(¤t_ftp);
else
goto ftp_read;
}
else
ftp_quit(¤t_ftp);
}
if (pass)
/* do nothing */ ;
else if (pu->pass)
pass = pu->pass;
else if (pu->user) {
pwd = find_auth_cookie(pu->host, pu->port, pu->file, pu->user);
if (pwd == NULL) {
if (fmInitialized) {
term_raw();
pwd = Strnew_charp(inputLine("Password: ", NULL, IN_PASSWORD));
pwd = Str_conv_to_system(pwd);
term_cbreak();
}
else {
pwd = Strnew_charp((char *)getpass("Password: "));
}
add_auth_cookie_flag = TRUE;
}
pass = pwd->ptr;
}
else if (ftppasswd != NULL && *ftppasswd != '\0')
pass = ftppasswd;
else {
struct passwd *mypw = getpwuid(getuid());
tmp = Strnew_charp(mypw ? mypw->pw_name : "anonymous");
Strcat_char(tmp, '@');
pass = tmp->ptr;
}
if (!current_ftp.host) {
current_ftp.host = allocStr(pu->host, -1);
current_ftp.port = pu->port;
current_ftp.user = allocStr(user, -1);
current_ftp.pass = allocStr(pass, -1);
if (!ftp_login(¤t_ftp))
return NULL;
}
if (add_auth_cookie_flag)
add_auth_cookie(pu->host, pu->port, pu->file, pu->user, pwd);
ftp_read:
ftp_command(¤t_ftp, "TYPE", "I", &status);
if (ftp_pasv(¤t_ftp) < 0) {
ftp_quit(¤t_ftp);
return NULL;
}
if (pu->file == NULL || *pu->file == '\0')
goto ftp_dir;
else
realpathname = file_unquote(pu->file);
if (pu->file[strlen(pu->file) - 1] == '/')
goto ftp_dir;
/* Get file */
uf->modtime = ftp_modtime(¤t_ftp, realpathname);
ftp_command(¤t_ftp, "RETR", realpathname, &status);
if (status == 125 || status == 150)
return newFileStream(current_ftp.data, (void (*)())closeFTPdata);
ftp_dir:
pu->scheme = SCM_FTPDIR;
return NULL;
}
Str
readFTPDir(ParsedURL *pu)
{
Str FTPDIRtmp;
Str tmp;
int status, sv_type;
char *realpathname, *fn;
char **flist;
int i, nfile, nfile_max = 100;
if (current_ftp.data == NULL)
return NULL;
tmp = ftp_command(¤t_ftp, "SYST", NULL, &status);
if (strstr(tmp->ptr, "UNIX") != NULL ||
!strncmp(tmp->ptr + 4, "Windows_NT", 10)) /* :-) */
sv_type = UNIXLIKE_SERVER;
else
sv_type = SERVER_NONE;
if (pu->file == NULL || *pu->file == '\0') {
if (sv_type == UNIXLIKE_SERVER)
ftp_command(¤t_ftp, "LIST", NULL, &status);
else
ftp_command(¤t_ftp, "NLST", NULL, &status);
pu->file = "/";
}
else {
char *realpathname = file_unquote(pu->file);
if (sv_type == UNIXLIKE_SERVER) {
ftp_command(¤t_ftp, "CWD", realpathname, &status);
if (status == 250)
ftp_command(¤t_ftp, "LIST", NULL, &status);
}
else
ftp_command(¤t_ftp, "NLST", realpathname, &status);
}
if (status != 125 && status != 150) {
fclose(current_ftp.data);
current_ftp.data = NULL;
return NULL;
}
tmp = parsedURL2Str(pu);
if (Strlastchar(tmp) != '/')
Strcat_char(tmp, '/');
fn = html_quote(tmp->ptr);
FTPDIRtmp = Strnew_m_charp("<html><head><title>", fn,
"</title></head><body><h1>Index of ", fn,
"</h1>\n", NULL);
if (sv_type == UNIXLIKE_SERVER)
Strcat_charp(FTPDIRtmp, "<pre>");
else
Strcat_charp(FTPDIRtmp, "<ul><li>");
Strcat_charp(FTPDIRtmp, "<a href=\"..\">[Upper Directory]</a>\n");
flist = New_N(char *, nfile_max);
nfile = 0;
if (sv_type == UNIXLIKE_SERVER) {
char *name, *date, *size, *type_str;
int ftype, max_len, len, j;
Str line_tmp;
max_len = 0;
while (tmp = Strfgets(current_ftp.data), tmp->length > 0) {
Strchop(tmp);
if ((ftype = ex_ftpdir_name_size_date(tmp->ptr, &name, &date,
&size)) == FTPDIR_NONE)
continue;
if (!strcmp(".", name) || !strcmp("..", name))
continue;
len = strlen(name);
if (!len)
continue;
if (ftype == FTPDIR_DIR) {
len++;
type_str = "/";
}
else if (ftype == FTPDIR_LINK) {
len++;
type_str = "@";
}
else {
type_str = "";
}
if (max_len < len)
max_len = len;
line_tmp =
Sprintf("%s%s %-12.12s %6.6s", name, type_str, date, size);
flist[nfile++] = line_tmp->ptr;
if (nfile == nfile_max) {
nfile_max *= 2;
flist = New_Reuse(char *, flist, nfile_max);
}
}
qsort(flist, nfile, sizeof(char *), strCmp);
for (j = 0; j < nfile; j++) {
fn = flist[j];
date = fn + strlen(fn) - 20;
if (*(date - 1) == '/') {
ftype = FTPDIR_DIR;
*date = '\0';
}
else if (*(date - 1) == '@') {
ftype = FTPDIR_LINK;
*(date - 1) = '\0';
}
else {
ftype = FTPDIR_FILE;
*date = '\0';
}
date++;
len = strlen(fn);
Strcat_m_charp(FTPDIRtmp, "<a href=\"", html_quote(file_quote(fn)),
"\">", html_quote(fn), NULL);
if (ftype == FTPDIR_LINK) {
Strcat_charp(FTPDIRtmp, "@");
len++;
}
Strcat_charp(FTPDIRtmp, "</a>");
for (i = len; i <= max_len; i++) {
if ((max_len % 2 + i) % 2) {
Strcat_charp(FTPDIRtmp, ".");
}
else {
Strcat_charp(FTPDIRtmp, " ");
}
}
Strcat_m_charp(FTPDIRtmp, date, "\n", NULL);
}
Strcat_charp(FTPDIRtmp, "</pre></body></html>\n");
}
else {
while (tmp = Strfgets(current_ftp.data), tmp->length > 0) {
Strchop(tmp);
flist[nfile++] = mybasename(tmp->ptr);
if (nfile == nfile_max) {
nfile_max *= 2;
flist = New_Reuse(char *, flist, nfile_max);
}
}
qsort(flist, nfile, sizeof(char *), strCmp);
for (i = 0; i < nfile; i++) {
fn = flist[i];
Strcat_m_charp(FTPDIRtmp, "<li><a href=\"",
html_quote(file_quote(fn)), "\">", html_quote(fn),
"</a>\n", NULL);
}
Strcat_charp(FTPDIRtmp, "</ul></body></html>\n");
}
closeFTPdata(current_ftp.data);
return FTPDIRtmp;
}
void
disconnectFTP(void)
{
ftp_quit(¤t_ftp);
}
#define XD_CTOD(c) {\
if (c >= '0' && c <= '9') {\
c -= (unsigned char)'0';\
} else if (c >= 'a' && c <= 'f') {\
c = c - (unsigned char)'a' + (unsigned char)10;\
} else if (c >= 'A' && c <= 'F') {\
c = c - (unsigned char)'A' + (unsigned char)10;\
} else {\
goto skip;\
}\
}
#define EX_SKIP_SPACE(cp) {\
while (IS_SPACE(*cp) && *cp != '\0') cp++;\
if (*cp == '\0') {\
goto done;\
}\
}
#define EX_SKIP_NONE_SPACE(cp) {\
while (!IS_SPACE(*cp) && *cp != '\0') cp++;\
if (*cp == '\0') {\
goto done;\
}\
}
static Str size_int2str(clen_t);
static int
ex_ftpdir_name_size_date(char *line, char **name, char **date, char **sizep)
{
int ftype = FTPDIR_NONE;
char *cp, *endp;
Str date_str, name_str, size_str;
clen_t size;
if (strlen(line) < 11) {
goto done;
}
/* skip permission */
if (!IS_SPACE(line[10])) {
goto done;
}
cp = line + 11;
/* skip link count */
EX_SKIP_SPACE(cp)
while (IS_DIGIT(*cp) && *cp != '\0')
cp++;
if (!IS_SPACE(*cp) || *cp == '\0') {
goto done;
}
cp++;
/* skip owner string */
EX_SKIP_SPACE(cp)
EX_SKIP_NONE_SPACE(cp)
cp++;
/* skip group string */
EX_SKIP_SPACE(cp)
EX_SKIP_NONE_SPACE(cp)
cp++;
/* extract size */
EX_SKIP_SPACE(cp)
size = 0;
while (*cp && IS_DIGIT(*cp)) {
size = size * 10 + *(cp++) - '0';
}
if (*cp == '\0') {
goto done;
}
/* extract date */
EX_SKIP_SPACE(cp)
if (IS_ALPHA(cp[0]) && IS_ALPHA(cp[1]) && IS_ALPHA(cp[2])
&& IS_SPACE(cp[3])
&& (IS_SPACE(cp[4]) || IS_DIGIT(cp[4])) && IS_DIGIT(cp[5])
&& IS_SPACE(cp[6])
&& (IS_SPACE(cp[7]) || IS_DIGIT(cp[7])) && IS_DIGIT(cp[8])
&& (cp[9] == ':' || IS_DIGIT(cp[9]))
&& IS_DIGIT(cp[10]) && (IS_DIGIT(cp[11]) || IS_SPACE(cp[11]))
&& IS_SPACE(cp[12])) {
cp[12] = '\0';
date_str = Strnew_charp(cp);
cp += 13;
}
else {
goto done;
}
/* extract file name */
EX_SKIP_SPACE(cp)
if (line[0] == 'l') {
if ((endp = strstr(cp, " -> ")) == NULL) {
goto done;
}
*endp = '\0';
size_str = Strnew_charp("-");
ftype = FTPDIR_LINK;
}
else if (line[0] == 'd') {
size_str = Strnew_charp("-");
ftype = FTPDIR_DIR;
}
else {
size_str = size_int2str(size);
ftype = FTPDIR_FILE;
}
name_str = Strnew_charp(cp);
*date = date_str->ptr;
*name = name_str->ptr;
*sizep = size_str->ptr;
done:
return (ftype);
}
static Str
size_int2str(clen_t size)
{
Str size_str;
int unit;
double dtmp;
char *size_format, *unit_str;
dtmp = (double)size;
for (unit = 0; unit < 3; unit++) {
if (dtmp < 1024) {
break;
}
dtmp /= 1024;
}
if (!unit || dtmp > 100) {
size_format = "%.0f%s";
}
else if (dtmp > 10) {
size_format = "%.1f%s";
}
else {
size_format = "%.2f%s";
}
switch (unit) {
case 3:
unit_str = "G";
break;
case 2:
unit_str = "M";
break;
case 1:
unit_str = "K";
break;
default:
unit_str = "";
break;
}
size_str = Sprintf(size_format, dtmp, unit_str);
return (size_str);
}