|
|
| |
|
![]() |


| $
cat /etc/services | grep ftp ftp-data 20/tcp ftp 21/tcp tftp 69/udp sftp 115/tcp ftps-data 989/tcp # FTP over SSL (data) [...] $ |
| command parameter1 parameter2... CRLF |
| Command name | Parameters | Description |
|---|---|---|
| USER (*) | user_name | It is the first command sent to the server to identify the user. |
| PASS | password | Send a password to the server if the name specified by the USER command needs one. By the way, we can note that the password is sent without any encryption. This is frequently considered as a security weakness of the FTP protocol. |
| CWD | directory | Change working directory (i.e. current directory). |
| CDUP | This is the shortcut of the CWD command to go to the upper directory. On Unix, this is the same as "cd .." but on other systems this may be something else. Hence this command to ignore the system specificities. | |
| QUIT (*) | Disconnect current user and close control channel. | |
| REIN | Disconnect current user but the control channel stays opened to accept a subsequent USER command in order to connect another user. | |
| PORT (*) | TCP_port | The client specifies to the server the TCP port number on which it waits for a data connection: the server is in active mode. |
| PASV | This is the opposite of PORT command. The client requests to the server a TCP port on which it will establish a data connection: the server is in passive mode. | |
| TYPE (*) | type | Specify the type of the information on the data channel. Among the multiple choices, the mostly used and often the only ones supported by the servers are ASCII (type = A) and BINARY (type = I). |
| STRU (*) | structure | This was used in the past by servers which organized their data into pages and records for efficiency reasons. This command is also used for the data recovery upon errors. The default structure is FILE (structure = F). |
| MODE (*) | mode | Specifies the data transfer mode. This command is also used for the data recovery upon errors. The default mode is STREAM (mode = S). |
| RETR (*) | file | Request the transfer of file from the server to the client. |
| STOR (*) | file | Request the transfer of file from the client to the server. |
| RNFR | file | Request the renaming of a file on the server. This command specifies the source file name and is followed by the RNTO command to specify the destination file name. |
| RNTO | file | Cf. RNFR |
| ABOR | Stop the running command. If a data channel is opened, it is closed by the server. | |
| DELE | file | Request the destruction of a file on the server. |
| RMD | directory | Request the destruction of a directory on the server. |
| MKD | directory | Request the creation of a directory on the server. |
| PWD | Request to the server the name of the current directory. | |
| LIST | [directory] or [file] | Request to the server information about a given file name or all the files of a given directory name (name, access rights, creation date, size...). By default, the information concern the files of the current directory. On Unix, it is normally the result of the "ls -l" command but on other systems, this can be something else. |
| NLST | [directory] or [file] | This command is the same as LIST but provides only the name of the files. Compared to LIST, this command has the advantage to be portable as it returns the same result no matter which kind of operating system that the server is running on. |
| SYST | Request to the server to identify its operating system. | |
| NOOP (*) | Do not trigger any action except to request the server to answer OK. This can be used to maintain a minimum traffic with the server which may implement an inactivity connection timeout. |
| Value | Description |
|---|---|
| 1yz |
Positive Preliminary reply. |
| 2yz |
Positive Completion reply. |
| 3yz |
Positive Intermediate reply. |
| 4yz |
Transient Negative Completion reply. |
| 5yz |
Permanent Negative Completion reply. |
| 331 Password required for foo. |
| 230-
Linux toto-host 2.6.22-14-generic #1 SMP Tue Dec 18 08:02:57 UTC 2007
i686 230- 230- The programs included with the Ubuntu system are free software; 230- the exact distribution terms for each program are described in the 230- individual files in /usr/share/doc/*/copyright. 230- 230- Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by 230- applicable law. 230 User foo logged in. |

| $ tar xvfz roofxxx.tgz |

| $ cd
roofxxx $ cmake . -- Check for working C compiler: /usr/bin/gcc [...] -- Build files have been written to: [...] $ make [...] Linking C executable roof [...] $ sudo make install [...] Linking C executable CMakeFiles/CMakeRelink.dir/roof Install the project... [...] |
| $
man 3 roof ROOF(3) Linux Programmer’s Manual ROOF(3) NAME roof - API for Remote Operations On Files [...] |
| $
roof roof 1.5 [...] Type 'help' or '?' for the list of available commands. ftp> quit $ |
| void
__attribute__ ((constructor)) roof_initialize(void); void roof_initialize(void) { int rc; // Creation of the mutex (unlocked) rc = pthread_mutex_init(&roof_mtx, NULL); [...] } // roof_initialize |
| #define
ROOF_LOCK() (pthread_mutex_lock(&roof_mtx)) #define ROOF_UNLOCK() (pthread_mutex_unlock(&roof_mtx)) |
| typedef
struct { void *ctx; // User context } roof_ctx_t; |
| typedef
struct { unsigned int debug_level; // Niveau de debug char *iobuf; // Buffer d'E/S unsigned int l_iobuf; // Taille du buffer d'E/S void *ctx; // Données utilisateur int busy; // 1, si contexte occupé int internal_iobuf; // 1, si buffer d'E/S alloué en interne int ctrl; // Socket de la cnx de contrôle unsigned int timeout_ms; // Timeout avec le serveur en ms char type; // Type pour la commande TYPE char code; // Code pour la commande TYPE } roof_context_t; |
| #define ROOF_CTX(p) ((roof_context_t *) ((char *)p - offsetof(roof_context_t, ctx))) |
| #define ROOF_EXT_CTX(p) ((roof_ctx_t *)&(p->ctx)) |
| roof_ctx_t
*roof_new( unsigned int timeout_ms, // Timeout (ms) to interact with the server // 0 = Infinite wait char *iobuf, // I/O buffer (default if NULL) unsigned int l_iobuf, // Lenght of the I/O buffer (default if 0) void *ctx // User context ) { unsigned int i; ROOF_LOCK(); // Look for a free context for (i = 0; i < ROOF_NB_MAX_CTX; i ++) { if (0 == roof_context[i].busy) { roof_context[i].busy = 1; break; } } // End for ROOF_UNLOCK(); if (i >= ROOF_NB_MAX_CTX) { errno = ENOSPC; return NULL; } roof_context[i].debug_level = 0; // If the buffer has been allocated by the user if (iobuf && l_iobuf) { [...] roof_context[i].iobuf = iobuf; roof_context[i].l_iobuf = l_iobuf; roof_context[i].internal_iobuf = 0; } else // Buffer allocated internally { roof_context[i].iobuf = (char *)malloc(ROOF_IO_BUF_SIZE); [...] roof_context[i].l_iobuf = ROOF_IO_BUF_SIZE; roof_context[i].internal_iobuf = 1; } roof_context[i].ctrl = -1; roof_context[i].timeout_ms = timeout_ms; roof_context[i].ctx = ctx; return (roof_ctx_t *)&(roof_context[i].ctx); } // roof_new |
| void
roof_delete(roof_ctx_t *pContext) // external context { roof_context_t *pCtx = ROOF_CTX(pContext); // Deallocation of the I/O buffer if allocated internally if (pCtx->internal_iobuf) { free(pCtx->iobuf); } // Close the control socket if opened if (pCtx->ctrl >= 0) { shutdown(pCtx->ctrl, SHUT_RDWR); close(pCtx->ctrl); } ROOF_LOCK(); // Free the context pCtx->busy = 0; ROOF_UNLOCK(); } // roof_delete |
| static
int roof_write( roof_context_t *pCtx, // Internal context int fd, // Output file descriptor const void *buf, // Writing buffer size_t len // Number of bytes to write ) { int rc; unsigned int l; l = len; do { rc = write(fd, ((const char *)buf) + (len - l), l); if (rc < 0) { if (EINTR == errno) { // Reiterate the read() rc = 0; } } if (rc > 0) { assert(l >= (unsigned)rc); l -= rc; } } while (l && (rc >= 0)); if (-1 == rc) { int saved_errno = errno; ROOF_ERR(pCtx, "Error '%s' (%d) on write()\n", strerror(errno), errno); errno = saved_errno; } else { rc = len; } return rc; } // roof_write |
| static
int roof_read( roof_context_t *pCtx, // Internal context int fd, // Input descriptor char *buf, // Read buffer unsigned int len // Maximum number of bytes to read ) { int rc; int saved_errno; do { rc = read(fd, buf, len); if (-1 == rc) { if (EINTR == errno) { continue; } saved_errno = errno; ROOF_ERR(pCtx, "Error '%s' (%d) on read(%d)\n", strerror(errno), errno, fd); errno = saved_errno; return -1; } } while (rc < 0); return rc; } // roof_read |
| static
int roof_send_cmd( roof_context_t *pCtx, // Internal context const char *format, // Command to send ... ) { int rc = 0; va_list args_list; int sz; va_start(args_list, format); sz = vsnprintf(pCtx->iobuf, pCtx->l_iobuf, format, args_list); va_end(args_list); [...] rc = roof_write(pCtx, pCtx->ctrl, pCtx->iobuf, sz); [...] return 0; } // roof_send_cmd |
| rc = roof_send_cmd(pCtx, "PASV\r\n"); |
| rc = roof_send_cmd(pCtx, "TYPE %c %c\r\n", type, code); |
| int
roof_get_reply( roof_ctx_t *pContext, // External context const char **reply // Response ) { roof_context_t *pCtx = ROOF_CTX(pContext); fd_set fdset; int rc; struct timeval to; unsigned int i; int first = 1; unsigned int offset = 0; unsigned int lreply; char code[4]; *reply = NULL; lreply = pCtx->l_iobuf; one_more_time: FD_ZERO(&fdset); FD_SET(pCtx->ctrl, &fdset); if (pCtx->timeout_ms) { to.tv_sec = pCtx->timeout_ms / 1000; to.tv_usec = (pCtx->timeout_ms % 1000) * 1000; rc = select(pCtx->ctrl + 1, &fdset, NULL, NULL, &to); } else { rc = select(pCtx->ctrl + 1, &fdset, NULL, NULL, NULL); } switch(rc) { case -1: // Error or signal { if (EINTR == errno) { goto one_more_time; } ROOF_ERR(pCtx, "Error '%s' (%d) on select()\n", strerror(errno), errno); return -1; } break; case 0: // Timeout { ROOF_ERR(pCtx, "Timeout on read\n"); errno = ETIMEDOUT; return -1; } break; case 1 : // Data from the connection { char *p; rc = roof_read_line(pCtx, pCtx->ctrl, pCtx->iobuf + offset, lreply); [...] // If the connection is closed if (0 == rc) { // Overwrite the buffer with a dummy code strcpy(pCtx->iobuf, "600"); lreply = pCtx->l_iobuf - 3; // Add additional information if enough room strncat(pCtx->iobuf, " End of connection", lreply); pCtx->iobuf[pCtx->l_iobuf - 1] = '\0'; *reply = pCtx->iobuf; return 0; } [...] // Look for the end of line p = pCtx->iobuf + offset; i = 0; while (p < (pCtx->iobuf + offset + rc)) { if (('\r' == *p) && ('\n' == *(p + 1))) { // This is the first line if (first) { // We must have 3 digits at the beginning of the line [...] for (i = 0; i < 3; i ++) { [...] code[i] = pCtx->iobuf[i]; } // End for // Is it a multipline answer ? if ('-' == pCtx->iobuf[i]) { first = 0; // Remaining space in the input buffer lreply -= rc; // New beginning of buffer when the response // is multiline offset += rc; [...] // Read the following line goto one_more_time; } // This is a single line response // Terminate the line by overwriting the last <CR> with NUL *p = '\0'; *reply = pCtx->iobuf; return 0; } else // This is a new line of a multiline response { // According to the specification, a line may begin with a // number. So, to check if it is the last line, we check // the code value if ((rc > 3) && (' ' == ((pCtx->iobuf + offset)[3])) && (code[0] == (pCtx->iobuf + offset)[0]) && (code[1] == (pCtx->iobuf + offset)[1]) && (code[2] == (pCtx->iobuf + offset)[2])) { // End of multiline answer // Terminate the line by overwriting the last <CR> with NUL *p = '\0'; *reply = pCtx->iobuf; return 0; } else // This is not the end of a multiline response { // Remaining space in the input buffer lreply -= rc; // New beginning of buffer for a multiline response offset += rc; // Read the following line goto one_more_time; } } } i ++; p ++; } // End while [...] } break; default : // Impossible ???!!!??? { return -1; } } // End switch return -1; } // roof_get_reply |
| int
roof_open_ctrl( roof_ctx_t *pContext, // External context const char *host, // Server's address (name or IP address) unsigned int port // Server's port number ) { roof_context_t *pCtx = ROOF_CTX(pContext); int rc; int sd = -1; struct sockaddr_in addr; struct hostent *pHost; char *code; struct linger opt_linger; int err_sav; [...] // Get a TCP socket descriptor sd = socket(PF_INET, SOCK_STREAM, 0); [...] // Set SO_LINGER option to make the caller of close() on the socket // wait until the remote part has got all its data opt_linger.l_onoff = 1; // Activate the LINGER opt_linger.l_linger = 2; // Persistence time in 100ms units rc = setsockopt(sd, SOL_SOCKET, SO_LINGER, &opt_linger, sizeof(opt_linger)); [...] // Translate the hostname into address pHost = gethostbyname(host); [...] // Populate the address memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = (in_addr_t)(*(unsigned long *)(pHost->h_addr_list[0])); // Connection to the remote host rc = connect(sd, (struct sockaddr *)&addr, sizeof(addr)); [...] // We populate the context right now bacause roof_get_reply() need the ctrl field pCtx->ctrl = sd; // Loop until we receive a 2yz response or timeout do { // Wait for a response from the server rc = roof_get_reply(pContext, &code); [...] if ((code[0] != '1') && (code[0] != '2')) { ROOF_ERR(pCtx, "Negative reply code '%s'\n", pCtx->iobuf); errno = EIO; rc = -1; goto error; } } while (code[0] != '2'); rc = sd; goto end; error: err_sav = errno; if (sd >= 0) { close(sd); sd = -1; } pCtx->ctrl = -1; errno = err_sav; end: return rc; } // roof_open_ctrl |

| int
roof_login( roof_ctx_t *pContext, // External context const char *login, // Login name const char *passwd, // Password const char *account // Account information ) { roof_context_t *pCtx = ROOF_CTX(pContext); int rc; const char *code; assert(NULL != pCtx); assert(pCtx->busy); if (!login || !(login[0])) { ROOF_ERR(pCtx, "NULL login parameter\n"); errno = EINVAL; return -1; } rc = roof_send_cmd(pCtx, "USER %s\r\n", login); [...] rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...] switch(code[0]) { case '1' : // Preliminary positive response case '4' : // Transient negative completion case '5' : // Permanent negative completion { ROOF_ERR(pCtx, "Error '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; case '3' : // Intermediate positive reponse { goto send_passwd; } break; case '2' : // Positive completion { goto end; } break; default : // Normally impossible { ROOF_ERR(pCtx, "Unexpected reply code '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; } // End switch send_passwd: if (!passwd || !(passwd[0])) { ROOF_ERR(pCtx, "Password parameter is required by server\n"); errno = EINVAL; return -1; } rc = roof_send_cmd(pCtx, "PASS %s\r\n", passwd); [...] rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...] switch(code[0]) { case '1' : // Positive Preliminary reply case '4' : // Transient negative completion case '5' : // Permanent negative completion { ROOF_ERR(pCtx, "Error '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; case '3' : // Intermediate positive response { goto send_account; } break; case '2' : // Positive termination { goto end; } break; default : // Normally impossible { ROOF_ERR(pCtx, "Unexpected reply code '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; } // End switch send_account: if (!account || !(account[0])) { ROOF_ERR(pCtx, "Account parameter is required by server\n"); errno = EINVAL; return -1; } rc = roof_send_cmd(pCtx, "ACCT %s\r\n", account); [...] rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...] switch(code[0]) { case '1' : // Positive preliminary reply case '3' : // Intermediate positive response case '4' : // Transient negative completion case '5' : // Permanent negative completion { ROOF_ERR(pCtx, "Error '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; case '2' : // Positive completion { goto end; } break; default : // Normally impossible { ROOF_ERR(pCtx, "Unexpected reply code '%s'\n", pCtx->iobuf); errno = EIO; return -1; } break; } // End switch end: return 0; } // roof_login |


| static
int roof_open_data(roof_context_t *pCtx) // Internal context { int rc; const char *code; char *p; int port_lsb, port_msb, port; struct sockaddr_in addr; int data; int err_sav; rc = roof_send_cmd(pCtx, "PASV\r\n"); [...] rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...] // Make sure the response is OK if (code[0] != '2') { ROOF_ERR(pCtx, "Error '%s'\n", pCtx->iobuf); errno = EIO; return -1; } // Parsing of the response from the server to get the port number as // well as the server's address p = pCtx->iobuf; while (*p && (*p != ')')) { p ++; } if (*p != ')') { ROOF_ERR(pCtx, "Expected a terminating ')' in '%s'\n", pCtx->iobuf); errno = EIO; return -1; } *p = '\0'; while ((p != pCtx->iobuf) && (*p != ',')) { p --; } if ((*p != ',') && (!isdigit(*(p+1)))) { ROOF_ERR(pCtx, "Expected a ',' followed by a digit in '%s'\n", pCtx->iobuf); errno = EIO; return -1; } *p = '\0'; port_lsb = atoi(p+1); while ((p != pCtx->iobuf) && (*p != ',')) { p --; } if ((*p != ',') && (!isdigit(*(p+1)))) { ROOF_ERR(pCtx, "Expected a ',' followed by a digit in '%s'\n", pCtx->iobuf); errno = EIO; return -1; } *p = '\0'; port_msb = atoi(p+1); port = (port_msb << 8) | port_lsb; // Convert the address into dotted notation p--; while ((p != pCtx->iobuf) && (*p != '(')) { if (',' == *p) { *p = '.'; } else { if (!isdigit(*p)) { ROOF_ERR(pCtx, "Expected a digit in the server's address'%s'\n", pCtx->iobuf); errno = EIO; return -1; } } p --; } if (*p != '(') { ROOF_ERR(pCtx, "Expected a '(' in '%s'\n", pCtx->iobuf); errno = EIO; return -1; } // Populate the server's address memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(p+1); // Creation of the socket for the data channel data = socket(PF_INET, SOCK_STREAM, 0); [...] // Connection to the server rc = connect(data, (struct sockaddr *)&addr, sizeof(addr)); [...] return data; } // roof_open_data |

| #include
<stdio.h> #include <libgen.h> #include <assert.h> #include <unistd.h> // By default installed in '/usr/local/include' #include <roof.h> // Callback to display the content of the directory // // Parameters: ctx = parameter 'ctx' passed to roof_list() // buf = Directory's data // lbuf = Size of the data in 'buf' static int display(roof_ctx_t *ctx, const char *buf, unsigned int lbuf) { int rc; (void)ctx; // Unused parameter (suppress compiler's warning) rc = write(1, buf, lbuf); assert(lbuf == (unsigned)rc); // Return OK to the library return lbuf; } // affiche // Program's entry point // // Parameters: av[1] = destination host // av[2] = login name // av[3] = password int main(int ac, char *av[]) { roof_ctx_t *pCtx; // ROOF object int ctrl; // Socket on control channel char *p; int rc; // Check the parameters passed to the program if (ac != 4) { fprintf(stderr, "Usage: %s host login passwd\n", basename(av[0])); return 1; } // Create the ROOF object: // . Timeout 10 seconds // . Allocation of I/O buffer by the library // . No user context pCtx = roof_new(10000, NULL, 0, NULL); assert(pCtx); // Open the control channel ctrl = roof_open_ctrl(pCtx, av[1], ROOF_DEF_PORT); assert(ctrl >= 0); // Authentication on remote host rc = roof_login(pCtx, av[2], av[3], NULL); assert(0 == rc); // Display the remote system's type rc = roof_syst(pCtx, &p); assert(0 == rc); printf("Remote system's type: %s\n", p); // Display the pathname of the working directory rc = roof_pwd(pCtx, &p); assert(0 == rc); printf("Working directory: %s\n", p); // Display the content of the working directory printf("Content of working directory:\n"); rc = roof_list(pCtx, NULL, display); assert(0 == rc); // Close the control channel rc = roof_close_ctrl(pCtx); assert(0 == rc); // Deallocation of the ROOF object roof_delete(pCtx); return 0; } // main |
| $ gcc test_ftp.c -o test_ftp -lroof |
| $
./test_ftp localhost foo bar Remote system's type: 215 UNIX Type: L8 (Linux) Working directory: 257 "/home/foo" is current directory. Content of working directory: total 2256 -rw-r--r-- 1 foo foo 60 Jan 16 08:55 file1 -rw------- 1 foo foo 203 Jan 16 08:55 file2 drwx------ 2 foo foo 4096 Mar 11 2007 directory $ |
|
|