Files
motion/netcam_ftp.c

869 lines
19 KiB
C

/**
* Much of the FTP code was inspired by the nanoftp.c module from
* libxml2 (Copyright Daniel Veillard, 2003). The routines have been
* modified to fit the needs of the Motion project.
*
* Copyright 2005, William M. Brack
* This software is distributed under the GNU Public license Version 2.
* See also the file 'COPYING'.
*
*/
#include "motion.h" /* needs to come first, because _GNU_SOURCE_ set there */
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <regex.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <netinet/in.h>
#include "netcam_ftp.h"
/**
* ftp_new_context
*
* Create a new FTP context structure
*
* Parameters
*
* None
*
* Returns: Pointer to the newly-created structure, NULL if error
*
*/
ftp_context_pointer ftp_new_context(void)
{
ftp_context_pointer ret;
/* note that mymalloc will exit on any problem */
ret = mymalloc(sizeof(ftp_context));
memset(ret, 0, sizeof(ftp_context));
ret->control_file_desc = -1; /* no control connection yet */
ret->data_file_desc = -1; /* no data connection yet */
return ret;
}
/**
* ftp_free_context
*
* Free the resources allocated for this context
*
* Parameters
*
* ctxt Pointer to the ftp_context structure
*
* Returns: Nothing
*
*/
void ftp_free_context(ftp_context_pointer ctxt)
{
if (ctxt == NULL)
return;
if (ctxt->path != NULL)
free(ctxt->path);
if (ctxt->user)
free(ctxt->user);
if (ctxt->passwd)
free(ctxt->passwd);
if (ctxt->control_file_desc >= 0)
close(ctxt->control_file_desc);
free(ctxt);
}
/**
* ftp_parse_response
*
* Parses the answer from the server, extracting the numeric code.
*
* Parameters:
*
* buf the buffer containing the response
* len the buffer length
*
* Returns:
* 0 for errors
* +XXX for last line of response
* -XXX for response to be continued
*/
static int ftp_parse_response(char *buf, int len) {
int val = 0;
if (len < 3)
return(-1);
if ((*buf >= '0') && (*buf <= '9'))
val = val * 10 + (*buf - '0');
else
return(0);
buf++;
if ((*buf >= '0') && (*buf <= '9'))
val = val * 10 + (*buf - '0');
else
return(0);
buf++;
if ((*buf >= '0') && (*buf <= '9'))
val = val * 10 + (*buf - '0');
else
return(0);
buf++;
if (*buf == '-')
return(-val);
return(val);
}
/**
* ftp_get_more
*
* Read more information from the FTP control connection
*
* Parameters:
*
* ctxt pointer to an FTP context
*
* Returns the number of bytes read, < 0 indicates an error
*/
static int ftp_get_more(ftp_context_pointer ctxt) {
int len;
int size;
/* Validate that our context structure is valid */
if ((ctxt == NULL) || (ctxt->control_file_desc < 0))
return(-1);
if ((ctxt->control_buffer_index < 0) || (ctxt->control_buffer_index > FTP_BUF_SIZE))
return(-1);
if ((ctxt->control_buffer_used < 0) || (ctxt->control_buffer_used > FTP_BUF_SIZE))
return(-1);
if (ctxt->control_buffer_index > ctxt->control_buffer_used)
return(-1);
/*
* First pack the control buffer
*/
if (ctxt->control_buffer_index > 0) {
memmove(&ctxt->control_buffer[0],
&ctxt->control_buffer[ctxt->control_buffer_index],
ctxt->control_buffer_used - ctxt->control_buffer_index);
ctxt->control_buffer_used -= ctxt->control_buffer_index;
ctxt->control_buffer_index = 0;
}
size = FTP_BUF_SIZE - ctxt->control_buffer_used;
if (size == 0) {
return(0);
}
/*
* Read the amount left on the control connection
*/
if ((len = recv(ctxt->control_file_desc,
&ctxt->control_buffer[ctxt->control_buffer_index],
size, 0)) < 0) {
motion_log(LOG_ERR, 1, "recv failed in ftp_get_more");
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
ctxt->control_buffer_used += len;
ctxt->control_buffer[ctxt->control_buffer_used] = 0;
return(len);
}
/**
* ftp_get_response
*
* Read the response from the FTP server after a command.
*
* Parameters
*
* ctxt pointer to an FTP context
*
* Returns the code number
*/
static int ftp_get_response(ftp_context_pointer ctxt) {
char *ptr, *end;
int len;
int res = -1, cur = -1;
if ((ctxt == NULL) || (ctxt->control_file_desc < 0))
return(-1);
get_more:
/*
* Assumes everything up to control_buffer[control_buffer_index] has been read
* and analyzed.
*/
len = ftp_get_more(ctxt);
if (len < 0) {
return(-1);
}
if ((ctxt->control_buffer_used == 0) && (len == 0)) {
return(-1);
}
ptr = &ctxt->control_buffer[ctxt->control_buffer_index];
end = &ctxt->control_buffer[ctxt->control_buffer_used];
while (ptr < end) {
cur = ftp_parse_response(ptr, end - ptr);
if (cur > 0) {
/*
* Successfully scanned the control code, skip
* till the end of the line, but keep the index to be
* able to analyze the result if needed.
*/
res = cur;
ptr += 3;
ctxt->control_buffer_answer = ptr - ctxt->control_buffer;
while ((ptr < end) && (*ptr != '\n')) ptr++;
if (*ptr == '\n') ptr++;
if (*ptr == '\r') ptr++;
break;
}
while ((ptr < end) && (*ptr != '\n'))
ptr++;
if (ptr >= end) {
ctxt->control_buffer_index = ctxt->control_buffer_used;
goto get_more;
}
if (*ptr != '\r') ptr++;
}
if (res < 0)
goto get_more;
ctxt->control_buffer_index = ptr - ctxt->control_buffer;
return(res / 100);
}
/**
* Send the user authentication
*/
static int ftp_send_user(ftp_context_pointer ctxt) {
char buf[200];
int len;
int res;
if (ctxt->user == NULL)
snprintf(buf, sizeof(buf), "USER anonymous\r\n");
else
snprintf(buf, sizeof(buf), "USER %s\r\n", ctxt->user);
buf[sizeof(buf) - 1] = 0;
len = strlen(buf);
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_send_user");
return(res);
}
return(0);
}
/**
* Send the password authentication
*/
static int ftp_send_passwd(ftp_context_pointer ctxt) {
char buf[200];
int len;
int res;
if (ctxt->passwd == NULL)
snprintf(buf, sizeof(buf), "PASS anonymous@\r\n");
else
snprintf(buf, sizeof(buf), "PASS %s\r\n", ctxt->passwd);
buf[sizeof(buf) - 1] = 0;
len = strlen(buf);
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_send_passwd");
return(res);
}
return(0);
}
/**
* ftp_quit
*
* Send a QUIT command to the server
*
* Parameters:
*
* ctxt pointer to an FTP context
*
* Returns -1 in case of error, 0 otherwise
*/
static int ftp_quit(ftp_context_pointer ctxt) {
char buf[200];
int len, res;
if ((ctxt == NULL) || (ctxt->control_file_desc < 0))
return(-1);
snprintf(buf, sizeof(buf), "QUIT\r\n");
len = strlen(buf);
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_quit");
return(res);
}
return(0);
}
/**
* ftp_connect
*
* Tries to open a control connection
*
* Parameters:
*
* ctxt an FTP context
*
* Returns -1 in case of error, 0 otherwise
*/
int ftp_connect(netcam_context_ptr netcam) {
ftp_context_pointer ctxt;
struct hostent *hp;
int port;
int res;
int addrlen = sizeof (struct sockaddr_in);
if (netcam == NULL)
return -1;
ctxt = netcam->ftp;
if (ctxt == NULL)
return(-1);
if (netcam->connect_host == NULL)
return(-1);
/*
* do the blocking DNS query.
*/
port = netcam->connect_port;
if (port == 0)
port = 21;
memset (&ctxt->ftp_address, 0, sizeof(ctxt->ftp_address));
hp = gethostbyname (netcam->connect_host);
if (hp == NULL) {
motion_log(LOG_ERR, 1, "gethostbyname failed in ftp_connect");
return (-1);
}
if ((unsigned int) hp->h_length >
sizeof(((struct sockaddr_in *)&ctxt->ftp_address)->sin_addr)) {
motion_log(LOG_ERR, 1, "gethostbyname address mismatch "
"in ftp_connect");
return (-1);
}
/*
* Prepare the socket
*/
((struct sockaddr_in *)&ctxt->ftp_address)->sin_family = AF_INET;
memcpy (&((struct sockaddr_in *)&ctxt->ftp_address)->sin_addr,
hp->h_addr_list[0], hp->h_length);
((struct sockaddr_in *)&ctxt->ftp_address)->sin_port =
(u_short)htons ((unsigned short)port);
ctxt->control_file_desc = socket (AF_INET, SOCK_STREAM, 0);
addrlen = sizeof (struct sockaddr_in);
if (ctxt->control_file_desc < 0) {
motion_log(LOG_ERR, 1, "socket failed");
return(-1);
}
/*
* Do the connect.
*/
if (connect(ctxt->control_file_desc, (struct sockaddr *) &ctxt->ftp_address,
addrlen) < 0) {
motion_log(LOG_ERR, 1, "Failed to create a connection");
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
/*
* Wait for the HELLO from the server.
*/
res = ftp_get_response(ctxt);
if (res != 2) {
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
/*
* Do the authentication
*/
res = ftp_send_user(ctxt);
if (res < 0) {
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
res = ftp_get_response(ctxt);
switch (res) {
case 2:
return(0);
case 3:
break;
case 1:
case 4:
case 5:
case -1:
default:
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
res = ftp_send_passwd(ctxt);
if (res < 0) {
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
res = ftp_get_response(ctxt);
switch (res) {
case 2:
break;
case 3:
motion_log(LOG_ERR, 0, "FTP server asking for ACCT on anonymous");
case 1:
case 4:
case 5:
case -1:
default:
close(ctxt->control_file_desc); ctxt->control_file_desc = -1;
ctxt->control_file_desc = -1;
return(-1);
}
return(0);
}
/**
* ftp_get_connection
*
* Try to open a data connection to the server.
*
* Parameters:
*
* ctxt pointer to an FTP context
*
* Returns -1 in case of error, 0 otherwise
*/
static int ftp_get_connection(ftp_context_pointer ctxt) {
char buf[200], *cur;
int len, i;
int res;
int on;
unsigned char ad[6], *adp, *portp;
unsigned int temp[6];
struct sockaddr_in data_address;
unsigned int data_address_length;
if (ctxt == NULL)
return(-1);
/* set up a socket for our data address */
if (ctxt->data_file_desc != -1)
close(ctxt->data_file_desc);
memset (&data_address, 0, sizeof(data_address));
ctxt->data_file_desc = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ctxt->data_file_desc < 0) {
motion_log(LOG_ERR, 1, "socket failed");
return (-1);
}
on = 1;
if (setsockopt(ctxt->data_file_desc, SOL_SOCKET, SO_REUSEADDR,
(char *)&on, sizeof(on)) < 0) {
motion_log(LOG_ERR, 1, "setting socket option SO_REUSEADDR");
return -1;
}
((struct sockaddr_in *)&data_address)->sin_family = AF_INET;
data_address_length = sizeof (struct sockaddr_in);
if (ctxt->passive) {
/* send PASV command over control channel */
snprintf (buf, sizeof(buf), "PASV\r\n");
len = strlen (buf);
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_get_connection");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(res);
}
/* check server's answer */
res = ftp_get_response(ctxt);
if (res != 2) {
if (res == 5) {
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(-1);
} else {
/*
* retry with an active connection
*/
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
ctxt->passive = 0;
}
}
/* parse the IP address and port supplied by the server */
cur = &ctxt->control_buffer[ctxt->control_buffer_answer];
while (((*cur < '0') || (*cur > '9')) && *cur != '\0')
cur++;
if (sscanf (cur, "%u,%u,%u,%u,%u,%u", &temp[0], &temp[1], &temp[2],
&temp[3], &temp[4], &temp[5]) != 6) {
motion_log(LOG_ERR, 0, "Invalid answer to PASV");
if (ctxt->data_file_desc != -1) {
close (ctxt->data_file_desc);
ctxt->data_file_desc = -1;
}
return (-1);
}
for (i=0; i<6; i++)
ad[i] = (unsigned char) (temp[i] & 0xff) ;
memcpy (&((struct sockaddr_in *)&data_address)->sin_addr, &ad[0], 4);
memcpy (&((struct sockaddr_in *)&data_address)->sin_port, &ad[4], 2);
/* Now try to connect to the data port */
if (connect(ctxt->data_file_desc, (struct sockaddr *) &data_address,
data_address_length) < 0) {
motion_log(LOG_ERR, 1, "Failed to create a data connection");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return (-1);
}
} else {
/*
* We want to bind to a port to receive the data. To do this,
* we need the address of our host. One easy way to get it is
* to get the info from the control connection that we have
* with the remote server
*/
getsockname(ctxt->control_file_desc, (struct sockaddr *)&data_address,
&data_address_length);
((struct sockaddr_in *)&data_address)->sin_port = 0;
/* bind to the socket - should give us a unique port */
if (bind(ctxt->data_file_desc, (struct sockaddr *) &data_address,
data_address_length) < 0) {
motion_log(LOG_ERR, 1, "bind failed");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return (-1);
}
/* we get the port number by reading back in the sockaddr */
getsockname(ctxt->data_file_desc, (struct sockaddr *)&data_address,
&data_address_length);
/* set up a 'listen' on the port to get the server's connection */
if (listen(ctxt->data_file_desc, 1) < 0) {
motion_log(LOG_ERR, 1, "listen failed");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return (-1);
}
/* now generate the PORT command */
adp = (unsigned char *) &((struct sockaddr_in *)&data_address)->sin_addr;
portp = (unsigned char *) &((struct sockaddr_in *)&data_address)->sin_port;
snprintf (buf, sizeof(buf), "PORT %d,%d,%d,%d,%d,%d\r\n",
adp[0] & 0xff, adp[1] & 0xff, adp[2] & 0xff, adp[3] & 0xff,
portp[0] & 0xff, portp[1] & 0xff);
buf[sizeof(buf) - 1] = 0;
len = strlen(buf);
/* send the PORT command to the server */
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_get_connection");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(res);
}
res = ftp_get_response(ctxt);
if (res != 2) {
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(-1);
}
}
return(ctxt->data_file_desc);
}
/**
* ftp_close_connection
*
* Close the data connection from the server
*
* Parameters:
*
* ctxt Pointer to an FTP context
*
* Returns -1 in case of error, 0 otherwise
*/
static int ftp_close_connection(ftp_context_pointer ctxt) {
int res;
fd_set rfd, efd;
struct timeval tv;
if ((ctxt == NULL) || (ctxt->control_file_desc < 0))
return(-1);
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
/* Check for data on the control channel */
tv.tv_sec = 15;
tv.tv_usec = 0;
FD_ZERO(&rfd);
FD_SET(ctxt->control_file_desc, &rfd);
FD_ZERO(&efd);
FD_SET(ctxt->control_file_desc, &efd);
res = select(ctxt->control_file_desc + 1, &rfd, NULL, &efd, &tv);
if (res < 0) {
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
if (res == 0) { /* timeout */
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
} else { /* read the response */
res = ftp_get_response(ctxt);
if (res != 2) { /* should be positive completion (2) */
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
return(-1);
}
}
return(0);
}
/**
* ftp_get_socket
*
* Initiate fetch of the given file from the server.
*
* Parameters:
*
* ctxt an FTP context
*
* Returns the socket for the data connection, or <0 in case of error
*/
int ftp_get_socket(ftp_context_pointer ctxt) {
char buf[300];
int res, len;
int acfd;
if ((ctxt == NULL) || (ctxt->path == NULL))
return(-1);
/* Set up the data connection */
ctxt->data_file_desc = ftp_get_connection(ctxt);
if (ctxt->data_file_desc == -1)
return(-1);
/* generate a "retrieve" command for the file */
snprintf(buf, sizeof(buf), "RETR %s\r\n", ctxt->path);
buf[sizeof(buf) - 1] = 0;
len = strlen(buf);
/* send it to the server */
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_get_socket");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(res);
}
/* check the answer */
res = ftp_get_response(ctxt);
if (res != 1) {
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(-res);
}
/*
* if not a passive connection, need to do an accept to get the
* connection from the server
*/
if (!ctxt->passive) {
struct sockaddr_in data_address;
unsigned int data_address_length = sizeof(struct sockaddr_in);
if ((acfd = accept(ctxt->data_file_desc, (struct sockaddr *)&data_address,
&data_address_length)) < 0) {
motion_log(LOG_ERR, 1, "accept in ftp_get_socket");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return -1;
}
close(ctxt->data_file_desc);
ctxt->data_file_desc = acfd;
}
return(ctxt->data_file_desc);
}
/**
* ftp_send_type
*
* Send a TYPE (either 'I' or 'A') command to the server
*
* Parameters
*
* ctxt pointer to the ftp_context
* type ascii character ('I' or 'A')
*
* Returns 0 for success, negative error code for failure
*
*/
int ftp_send_type(ftp_context_pointer ctxt, char type) {
char buf[100], utype;
int len, res;
utype = toupper(type);
/* Assure transfer will be in "image" mode */
snprintf(buf, sizeof(buf), "TYPE I\r\n");
len = strlen(buf);
res = send(ctxt->control_file_desc, buf, len, 0);
if (res < 0) {
motion_log(LOG_ERR, 1, "send failed in ftp_get_socket");
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(res);
}
res = ftp_get_response(ctxt);
if (res != 2) {
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
return(-res);
}
return 0;
}
/**
* ftp_read
*
* This function tries to read len bytes from the existing FTP
* connection and saves them in dest. This is a blocking call.
*
* Parameters:
* ctxt the FTP context
* dest a buffer
* len the buffer length
*
* Returns: the number of bytes read.
* 0 is an indication of an end of connection.
* -1 indicates a parameter error.
*/
int ftp_read(ftp_context_pointer ctxt, void *dest, int len) {
if (ctxt == NULL)
return(-1);
if (ctxt->data_file_desc < 0)
return(0);
if (dest == NULL)
return(-1);
if (len <= 0)
return(0);
len = recv(ctxt->data_file_desc, dest, len, 0);
if (len <= 0) {
if (len < 0)
motion_log(LOG_ERR, 1, "recv failed in ftp_read");
ftp_close_connection(ctxt);
}
return(len);
}
/**
* ftp_close
*
* Close the connection and both control and transport
*
* Parameters:
*
* ctxt Pointer to an FTP context
*
* Returns -1 in case of error, 0 otherwise
*/
int ftp_close(ftp_context_pointer ctxt) {
if (ctxt == NULL)
return(-1);
if (ctxt->data_file_desc >= 0) {
close(ctxt->data_file_desc);
ctxt->data_file_desc = -1;
}
if (ctxt->control_file_desc >= 0) {
ftp_quit(ctxt);
close(ctxt->control_file_desc);
ctxt->control_file_desc = -1;
}
ftp_free_context(ctxt);
return(0);
}