| /* |
| * Copyright (C) 2007 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| */ |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <assert.h> |
| #include <errno.h> |
| #include <ctype.h> |
| #include <byteswap.h> |
| #include <ipxe/socket.h> |
| #include <ipxe/tcpip.h> |
| #include <ipxe/in.h> |
| #include <ipxe/iobuf.h> |
| #include <ipxe/xfer.h> |
| #include <ipxe/open.h> |
| #include <ipxe/uri.h> |
| #include <ipxe/features.h> |
| #include <ipxe/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_SIZE, |
| 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 interface xfer; |
| |
| /** URI being fetched */ |
| struct uri *uri; |
| /** FTP control channel interface */ |
| struct interface control; |
| /** FTP data channel interface */ |
| struct 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" */ |
| /** File size, as text */ |
| char filesize[20]; |
| }; |
| |
| /** |
| * 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 */ |
| intf_shutdown ( &ftp->data, rc ); |
| intf_shutdown ( &ftp->control, rc ); |
| intf_shutdown ( &ftp->xfer, 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 = "ipxe@ipxe.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_SIZE] = { "SIZE ", ftp_uri_path }, |
| [FTP_PASV] = { "PASV", NULL }, |
| [FTP_RETR] = { "RETR ", ftp_uri_path }, |
| [FTP_WAIT] = { NULL, NULL }, |
| [FTP_QUIT] = { "QUIT", NULL }, |
| [FTP_DONE] = { NULL, NULL }, |
| }; |
| |
| /** |
| * 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; |
| |
| /* If the SIZE command is not supported by the server, we go to |
| * the next step. |
| */ |
| if ( ( status_major == '5' ) && ( ftp->state == FTP_SIZE ) ) { |
| ftp_next_state ( ftp ); |
| 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; |
| } |
| |
| /* Parse file size */ |
| if ( ftp->state == FTP_SIZE ) { |
| size_t filesize; |
| char *endptr; |
| |
| /* Parse size */ |
| filesize = strtoul ( ftp->filesize, &endptr, 10 ); |
| if ( *endptr != '\0' ) { |
| DBGC ( ftp, "FTP %p invalid SIZE \"%s\"\n", |
| ftp, ftp->filesize ); |
| ftp_done ( ftp, -EPROTO ); |
| return; |
| } |
| |
| /* Use seek() to notify recipient of filesize */ |
| DBGC ( ftp, "FTP %p file size is %zd bytes\n", ftp, filesize ); |
| xfer_seek ( &ftp->xfer, filesize ); |
| xfer_seek ( &ftp->xfer, 0 ); |
| } |
| |
| /* 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 ftp FTP request |
| * @v iob I/O buffer |
| * @v meta Data transfer metadata |
| * @ret rc Return status code |
| * |
| * Data is collected until a complete line is received, at which point |
| * its information is passed to ftp_reply(). |
| */ |
| static int ftp_control_deliver ( struct ftp_request *ftp, |
| struct io_buffer *iobuf, |
| struct xfer_metadata *meta __unused ) { |
| char *data = iobuf->data; |
| size_t len = iob_len ( iobuf ); |
| char *recvbuf = ftp->recvbuf; |
| size_t recvsize = ftp->recvsize; |
| char c; |
| |
| while ( len-- ) { |
| c = *(data++); |
| if ( ( c == '\r' ) || ( c == '\n' ) ) { |
| /* End of line: call ftp_reply() to handle |
| * completed reply. Avoid calling ftp_reply() |
| * twice if we receive both \r and \n. |
| */ |
| if ( recvbuf != ftp->status_text ) |
| ftp_reply ( ftp ); |
| /* Start filling up the status code buffer */ |
| recvbuf = ftp->status_text; |
| recvsize = sizeof ( ftp->status_text ) - 1; |
| } else if ( ( ftp->state == FTP_PASV ) && ( c == '(' ) ) { |
| /* Start filling up the passive parameter buffer */ |
| recvbuf = ftp->passive_text; |
| recvsize = sizeof ( ftp->passive_text ) - 1; |
| } else if ( ( ftp->state == FTP_PASV ) && ( c == ')' ) ) { |
| /* Stop filling the passive parameter buffer */ |
| recvsize = 0; |
| } else if ( ( ftp->state == FTP_SIZE ) && ( c == ' ' ) ) { |
| /* Start filling up the file size buffer */ |
| recvbuf = ftp->filesize; |
| recvsize = sizeof ( ftp->filesize ) - 1; |
| } else { |
| /* Fill up buffer if applicable */ |
| if ( recvsize > 0 ) { |
| *(recvbuf++) = c; |
| recvsize--; |
| } |
| } |
| } |
| |
| /* Store for next invocation */ |
| ftp->recvbuf = recvbuf; |
| ftp->recvsize = recvsize; |
| |
| /* Free I/O buffer */ |
| free_iob ( iobuf ); |
| |
| return 0; |
| } |
| |
| /** FTP control channel interface operations */ |
| static struct interface_operation ftp_control_operations[] = { |
| INTF_OP ( xfer_deliver, struct ftp_request *, ftp_control_deliver ), |
| INTF_OP ( intf_close, struct ftp_request *, ftp_done ), |
| }; |
| |
| /** FTP control channel interface descriptor */ |
| static struct interface_descriptor ftp_control_desc = |
| INTF_DESC ( struct ftp_request, control, ftp_control_operations ); |
| |
| /***************************************************************************** |
| * |
| * FTP data channel |
| * |
| */ |
| |
| /** |
| * Handle FTP data channel being closed |
| * |
| * @v ftp FTP request |
| * @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 ftp_request *ftp, int rc ) { |
| |
| 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 ); |
| } |
| } |
| |
| /** FTP data channel interface operations */ |
| static struct interface_operation ftp_data_operations[] = { |
| INTF_OP ( intf_close, struct ftp_request *, ftp_data_closed ), |
| }; |
| |
| /** FTP data channel interface descriptor */ |
| static struct interface_descriptor ftp_data_desc = |
| INTF_DESC_PASSTHRU ( struct ftp_request, data, ftp_data_operations, |
| xfer ); |
| |
| /***************************************************************************** |
| * |
| * Data transfer interface |
| * |
| */ |
| |
| /** FTP data transfer interface operations */ |
| static struct interface_operation ftp_xfer_operations[] = { |
| INTF_OP ( intf_close, struct ftp_request *, ftp_done ), |
| }; |
| |
| /** FTP data transfer interface descriptor */ |
| static struct interface_descriptor ftp_xfer_desc = |
| INTF_DESC_PASSTHRU ( struct ftp_request, xfer, ftp_xfer_operations, |
| data ); |
| |
| /***************************************************************************** |
| * |
| * URI opener |
| * |
| */ |
| |
| /** |
| * Check validity of FTP control channel string |
| * |
| * @v string String |
| * @ret rc Return status code |
| */ |
| static int ftp_check_string ( const char *string ) { |
| char c; |
| |
| /* The FTP control channel is line-based. Check for invalid |
| * non-printable characters (e.g. newlines). |
| */ |
| while ( ( c = *(string++) ) ) { |
| if ( ! isprint ( c ) ) |
| return -EINVAL; |
| } |
| return 0; |
| } |
| |
| /** |
| * Initiate an FTP connection |
| * |
| * @v xfer Data transfer interface |
| * @v uri Uniform Resource Identifier |
| * @ret rc Return status code |
| */ |
| static int ftp_open ( struct interface *xfer, struct uri *uri ) { |
| struct ftp_request *ftp; |
| struct sockaddr_tcpip server; |
| int rc; |
| |
| /* Sanity checks */ |
| if ( ! uri->host ) |
| return -EINVAL; |
| if ( ! uri->path ) |
| return -EINVAL; |
| if ( ( rc = ftp_check_string ( uri->path ) ) != 0 ) |
| return rc; |
| if ( uri->user && ( ( rc = ftp_check_string ( uri->user ) ) != 0 ) ) |
| return rc; |
| if ( uri->password && |
| ( ( rc = ftp_check_string ( uri->password ) ) != 0 ) ) |
| return rc; |
| |
| /* Allocate and populate structure */ |
| ftp = zalloc ( sizeof ( *ftp ) ); |
| if ( ! ftp ) |
| return -ENOMEM; |
| ref_init ( &ftp->refcnt, ftp_free ); |
| intf_init ( &ftp->xfer, &ftp_xfer_desc, &ftp->refcnt ); |
| intf_init ( &ftp->control, &ftp_control_desc, &ftp->refcnt ); |
| intf_init ( &ftp->data, &ftp_data_desc, &ftp->refcnt ); |
| ftp->uri = uri_get ( uri ); |
| 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 */ |
| intf_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, |
| }; |