#include <stdint.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <assert.h> #include <errno.h> #include <byteswap.h> #include <gpxe/socket.h> #include <gpxe/tcpip.h> #include <gpxe/in.h> #include <gpxe/xfer.h> #include <gpxe/open.h> #include <gpxe/uri.h> #include <gpxe/features.h> #include <gpxe/ftp.h> /** @file * * File transfer protocol * */ FEATURE ( FEATURE_PROTOCOL, "FTP", DHCP_EB_FEATURE_FTP, 1 ); /** * FTP states * * These @b must be sequential, i.e. a successful FTP session must * pass through each of these states in order. */ enum ftp_state { FTP_CONNECT = 0, FTP_USER, FTP_PASS, FTP_TYPE, FTP_PASV, FTP_RETR, FTP_WAIT, FTP_QUIT, FTP_DONE, }; /** * An FTP request * */ struct ftp_request { /** Reference counter */ struct refcnt refcnt; /** Data transfer interface */ struct xfer_interface xfer; /** URI being fetched */ struct uri *uri; /** FTP control channel interface */ struct xfer_interface control; /** FTP data channel interface */ struct xfer_interface data; /** Current state */ enum ftp_state state; /** Buffer to be filled with data received via the control channel */ char *recvbuf; /** Remaining size of recvbuf */ size_t recvsize; /** FTP status code, as text */ char status_text[5]; /** Passive-mode parameters, as text */ char passive_text[24]; /* "aaa,bbb,ccc,ddd,eee,fff" */ }; /** * Free FTP request * * @v refcnt Reference counter */ static void ftp_free ( struct refcnt *refcnt ) { struct ftp_request *ftp = container_of ( refcnt, struct ftp_request, refcnt ); DBGC ( ftp, "FTP %p freed\n", ftp ); uri_put ( ftp->uri ); free ( ftp ); } /** * Mark FTP operation as complete * * @v ftp FTP request * @v rc Return status code */ static void ftp_done ( struct ftp_request *ftp, int rc ) { DBGC ( ftp, "FTP %p completed (%s)\n", ftp, strerror ( rc ) ); /* Close all data transfer interfaces */ xfer_nullify ( &ftp->xfer ); xfer_close ( &ftp->xfer, rc ); xfer_nullify ( &ftp->control ); xfer_close ( &ftp->control, rc ); xfer_nullify ( &ftp->data ); xfer_close ( &ftp->data, rc ); } /***************************************************************************** * * FTP control channel * */ /** An FTP control channel string */ struct ftp_control_string { /** Literal portion */ const char *literal; /** Variable portion * * @v ftp FTP request * @ret string Variable portion of string */ const char * ( *variable ) ( struct ftp_request *ftp ); }; /** * Retrieve FTP pathname * * @v ftp FTP request * @ret path FTP pathname */ static const char * ftp_uri_path ( struct ftp_request *ftp ) { return ftp->uri->path; } /** * Retrieve FTP user * * @v ftp FTP request * @ret user FTP user */ static const char * ftp_user ( struct ftp_request *ftp ) { static char *ftp_default_user = "anonymous"; return ftp->uri->user ? ftp->uri->user : ftp_default_user; } /** * Retrieve FTP password * * @v ftp FTP request * @ret password FTP password */ static const char * ftp_password ( struct ftp_request *ftp ) { static char *ftp_default_password = "etherboot@etherboot.org"; return ftp->uri->password ? ftp->uri->password : ftp_default_password; } /** FTP control channel strings */ static struct ftp_control_string ftp_strings[] = { [FTP_CONNECT] = { NULL, NULL }, [FTP_USER] = { "USER ", ftp_user }, [FTP_PASS] = { "PASS ", ftp_password }, [FTP_TYPE] = { "TYPE I", NULL }, [FTP_PASV] = { "PASV", NULL }, [FTP_RETR] = { "RETR ", ftp_uri_path }, [FTP_WAIT] = { NULL, NULL }, [FTP_QUIT] = { "QUIT", NULL }, [FTP_DONE] = { NULL, NULL }, }; /** * Handle control channel being closed * * @v control FTP control channel interface * @v rc Reason for close * * When the control channel is closed, the data channel must also be * closed, if it is currently open. */ static void ftp_control_close ( struct xfer_interface *control, int rc ) { struct ftp_request *ftp = container_of ( control, struct ftp_request, control ); DBGC ( ftp, "FTP %p control connection closed: %s\n", ftp, strerror ( rc ) ); /* Complete FTP operation */ ftp_done ( ftp, rc ); } /** * Parse FTP byte sequence value * * @v text Text string * @v value Value buffer * @v len Length of value buffer * * This parses an FTP byte sequence value (e.g. the "aaa,bbb,ccc,ddd" * form for IP addresses in PORT commands) into a byte sequence. @c * *text will be updated to point beyond the end of the parsed byte * sequence. * * This function is safe in the presence of malformed data, though the * output is undefined. */ static void ftp_parse_value ( char **text, uint8_t *value, size_t len ) { do { *(value++) = strtoul ( *text, text, 10 ); if ( **text ) (*text)++; } while ( --len ); } /** * Move to next state and send the appropriate FTP control string * * @v ftp FTP request * */ static void ftp_next_state ( struct ftp_request *ftp ) { struct ftp_control_string *ftp_string; const char *literal; const char *variable; /* Move to next state */ if ( ftp->state < FTP_DONE ) ftp->state++; /* Send control string if needed */ ftp_string = &ftp_strings[ftp->state]; literal = ftp_string->literal; variable = ( ftp_string->variable ? ftp_string->variable ( ftp ) : "" ); if ( literal ) { DBGC ( ftp, "FTP %p sending %s%s\n", ftp, literal, variable ); xfer_printf ( &ftp->control, "%s%s\r\n", literal, variable ); } } /** * Handle an FTP control channel response * * @v ftp FTP request * * This is called once we have received a complete response line. */ static void ftp_reply ( struct ftp_request *ftp ) { char status_major = ftp->status_text[0]; char separator = ftp->status_text[3]; DBGC ( ftp, "FTP %p received status %s\n", ftp, ftp->status_text ); /* Ignore malformed lines */ if ( separator != ' ' ) return; /* Ignore "intermediate" responses (1xx codes) */ if ( status_major == '1' ) return; /* Anything other than success (2xx) or, in the case of a * repsonse to a "USER" command, a password prompt (3xx), is a * fatal error. */ if ( ! ( ( status_major == '2' ) || ( ( status_major == '3' ) && ( ftp->state == FTP_USER ) ) ) ){ /* Flag protocol error and close connections */ ftp_done ( ftp, -EPROTO ); return; } /* Open passive connection when we get "PASV" response */ if ( ftp->state == FTP_PASV ) { char *ptr = ftp->passive_text; union { struct sockaddr_in sin; struct sockaddr sa; } sa; int rc; sa.sin.sin_family = AF_INET; ftp_parse_value ( &ptr, ( uint8_t * ) &sa.sin.sin_addr, sizeof ( sa.sin.sin_addr ) ); ftp_parse_value ( &ptr, ( uint8_t * ) &sa.sin.sin_port, sizeof ( sa.sin.sin_port ) ); if ( ( rc = xfer_open_socket ( &ftp->data, SOCK_STREAM, &sa.sa, NULL ) ) != 0 ) { DBGC ( ftp, "FTP %p could not open data connection\n", ftp ); ftp_done ( ftp, rc ); return; } } /* Move to next state and send control string */ ftp_next_state ( ftp ); } /** * Handle new data arriving on FTP control channel * * @v control FTP control channel interface * @v data New data * @v len Length of new data * * Data is collected until a complete line is received, at which point * its information is passed to ftp_reply(). */ static int ftp_control_deliver_raw ( struct xfer_interface *control, const void *data, size_t len ) { struct ftp_request *ftp = container_of ( control, struct ftp_request, control ); char *recvbuf = ftp->recvbuf; size_t recvsize = ftp->recvsize; char c; while ( len-- ) { c = * ( ( char * ) data++ ); switch ( c ) { case '\r' : case '\n' : /* End of line: call ftp_reply() to handle * completed reply. Avoid calling ftp_reply() * twice if we receive both \r and \n. */ if ( recvsize == 0 ) ftp_reply ( ftp ); /* Start filling up the status code buffer */ recvbuf = ftp->status_text; recvsize = sizeof ( ftp->status_text ) - 1; break; case '(' : /* Start filling up the passive parameter buffer */ recvbuf = ftp->passive_text; recvsize = sizeof ( ftp->passive_text ) - 1; break; case ')' : /* Stop filling the passive parameter buffer */ recvsize = 0; break; default : /* Fill up buffer if applicable */ if ( recvsize > 0 ) { *(recvbuf++) = c; recvsize--; } break; } } /* Store for next invocation */ ftp->recvbuf = recvbuf; ftp->recvsize = recvsize; return 0; } /** FTP control channel operations */ static struct xfer_interface_operations ftp_control_operations = { .close = ftp_control_close, .vredirect = xfer_vreopen, .window = unlimited_xfer_window, .alloc_iob = default_xfer_alloc_iob, .deliver_iob = xfer_deliver_as_raw, .deliver_raw = ftp_control_deliver_raw, }; /***************************************************************************** * * FTP data channel * */ /** * Handle FTP data channel being closed * * @v data FTP data channel interface * @v rc Reason for closure * * When the data channel is closed, the control channel should be left * alone; the server will send a completion message via the control * channel which we'll pick up. * * If the data channel is closed due to an error, we abort the request. */ static void ftp_data_closed ( struct xfer_interface *data, int rc ) { struct ftp_request *ftp = container_of ( data, struct ftp_request, data ); DBGC ( ftp, "FTP %p data connection closed: %s\n", ftp, strerror ( rc ) ); /* If there was an error, close control channel and record status */ if ( rc ) { ftp_done ( ftp, rc ); } else { ftp_next_state ( ftp ); } } /** * Handle data delivery via FTP data channel * * @v xfer FTP data channel interface * @v iobuf I/O buffer * @v meta Data transfer metadata * @ret rc Return status code */ static int ftp_data_deliver_iob ( struct xfer_interface *data, struct io_buffer *iobuf, struct xfer_metadata *meta __unused ) { struct ftp_request *ftp = container_of ( data, struct ftp_request, data ); int rc; if ( ( rc = xfer_deliver_iob ( &ftp->xfer, iobuf ) ) != 0 ) { DBGC ( ftp, "FTP %p failed to deliver data: %s\n", ftp, strerror ( rc ) ); return rc; } return 0; } /** FTP data channel operations */ static struct xfer_interface_operations ftp_data_operations = { .close = ftp_data_closed, .vredirect = xfer_vreopen, .window = unlimited_xfer_window, .alloc_iob = default_xfer_alloc_iob, .deliver_iob = ftp_data_deliver_iob, .deliver_raw = xfer_deliver_as_iob, }; /***************************************************************************** * * Data transfer interface * */ /** * Close FTP data transfer interface * * @v xfer FTP data transfer interface * @v rc Reason for close */ static void ftp_xfer_closed ( struct xfer_interface *xfer, int rc ) { struct ftp_request *ftp = container_of ( xfer, struct ftp_request, xfer ); DBGC ( ftp, "FTP %p data transfer interface closed: %s\n", ftp, strerror ( rc ) ); ftp_done ( ftp, rc ); } /** FTP data transfer interface operations */ static struct xfer_interface_operations ftp_xfer_operations = { .close = ftp_xfer_closed, .vredirect = ignore_xfer_vredirect, .window = unlimited_xfer_window, .alloc_iob = default_xfer_alloc_iob, .deliver_iob = xfer_deliver_as_raw, .deliver_raw = ignore_xfer_deliver_raw, }; /***************************************************************************** * * URI opener * */ /** * Initiate an FTP connection * * @v xfer Data transfer interface * @v uri Uniform Resource Identifier * @ret rc Return status code */ static int ftp_open ( struct xfer_interface *xfer, struct uri *uri ) { struct ftp_request *ftp; struct sockaddr_tcpip server; int rc; /* Sanity checks */ if ( ! uri->path ) return -EINVAL; if ( ! uri->host ) return -EINVAL; /* Allocate and populate structure */ ftp = zalloc ( sizeof ( *ftp ) ); if ( ! ftp ) return -ENOMEM; ftp->refcnt.free = ftp_free; xfer_init ( &ftp->xfer, &ftp_xfer_operations, &ftp->refcnt ); ftp->uri = uri_get ( uri ); xfer_init ( &ftp->control, &ftp_control_operations, &ftp->refcnt ); xfer_init ( &ftp->data, &ftp_data_operations, &ftp->refcnt ); ftp->recvbuf = ftp->status_text; ftp->recvsize = sizeof ( ftp->status_text ) - 1; DBGC ( ftp, "FTP %p fetching %s\n", ftp, ftp->uri->path ); /* Open control connection */ memset ( &server, 0, sizeof ( server ) ); server.st_port = htons ( uri_port ( uri, FTP_PORT ) ); if ( ( rc = xfer_open_named_socket ( &ftp->control, SOCK_STREAM, ( struct sockaddr * ) &server, uri->host, NULL ) ) != 0 ) goto err; /* Attach to parent interface, mortalise self, and return */ xfer_plug_plug ( &ftp->xfer, xfer ); ref_put ( &ftp->refcnt ); return 0; err: DBGC ( ftp, "FTP %p could not create request: %s\n", ftp, strerror ( rc ) ); ftp_done ( ftp, rc ); ref_put ( &ftp->refcnt ); return rc; } /** FTP URI opener */ struct uri_opener ftp_uri_opener __uri_opener = { .scheme = "ftp", .open = ftp_open, };