Implementing a Minimal Passive-Mode FTP Client in C
Implementing a client for the FTP control and data channels requires speaking a simple, line-oriented, text protocol over TCP. Commands are ASCII lines terminated by CRLF (\r\n). Replies are lines beginning with a three-digit code followed by text and CRLF. Only the numeric code is needed for flow control.
Protocol essentials
- Control connnection
- Default TCP port: 21 (configurable on the server).
- Client sends: COMMAND SP PARAMS CRLF.
- Server replies: DDD SP text CRLF. Parse the first three digits.
- Authentication
- Anonymous access uses USER anonymous and a password (often any email string). Case is significant.
- Binary transfers
- TYPE I switches to binary mode so file bytes are unmodified.
Typical download sequence (passive mode)
- Connect to <server, port> on the control socket; expect 220.
- USER → typically 331; PASS → 230 on success (530 on failure).
- Example
- Send: USER anonymous
- Reply: 331 Anonymous login ok, send your complete email address as your password
- Send: PASS anonymous
- Reply: 230 Anonymous access granted
- Example
- TYPE I → 200 to enforce binary transfers.
- Optional: SIZE /path/file → 213 or an error like 550 if missing.
- PASV → 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
- Data endpoint: IP = h1.h2.h3.h4, Port = p1*256 + p2.
- Connect the data socket to the PASV endpoint.
- RETR /path/file → expect 150/125, then read file bytes from the data socket. Data socket closes when finished; final reply 226 on the control channel.
Typical upload sequence (passive mode)
- PASV → 227 and parse endpoint.
- Connect the data socket to the reported IP:port.
- STOR /path/file → expect 150/125, then write file bytes to the data socket.
- After sending all data, close the data socket; expect 226 on the control channel.
Frequently used reply codes
- 1xx: Preliminary replies
- 125 Data connection open; transfer starting
- 150 File status okay; about to open data connection
- 2xx: Success
- 200 Command okay
- 213 Status (used by SIZE)
- 220 Service ready
- 226 Closing data connection; transfer complete
- 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
- 230 User logged in
- 3xx: Further information needed
- 331 User name okay, need password
- 4xx: Transient errors
- 421 Service not available
- 425 Can’t open data connection
- 450 Requested action not taken (e.g., file busy)
- 5xx: Permanent errors
- 500 Syntax eror, command unrecognized
- 530 Not logged in
- 550 Requested action not taken (e.g., file not found)
C implementation (passive-mode FTP, upload/download/SIZE)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "socket.h"
#include "log.h"
#include "ftp.h"
// Control and data sockets
static int g_ctrl_sock;
static int g_data_sock;
// Reusable I/O buffers
static char g_tx[1024];
static char g_rx[1024];
// Send a raw command line over the control channel (must include CRLF)
static int ctrl_send_line(const char *line)
{
int n = (int)strlen(line);
LOG_INFO("CTRL >> %s\r\n", line);
if (socket_send(g_ctrl_sock, line, n) < 0)
return 0;
return 1;
}
// Read a single reply line from the control socket; return numeric code
static int ctrl_read_reply(char *line, int cap)
{
int off = 0;
if (cap < 2) return 0;
// Read until '\n' OR buffer is almost full
while (off < cap - 1) {
int r = socket_recv(g_ctrl_sock, &line[off], 1);
if (r <= 0) return 0;
if (line[off] == '\n') { off++; break; }
off += r;
}
line[off] = '\0';
LOG_INFO("CTRL << %s", line);
// Parse first three digits
if (off >= 3 && line[0] >= '0' && line[0] <= '9') {
int code = (line[0]-'0')*100 + (line[1]-'0')*10 + (line[2]-'0');
return code;
}
return 0;
}
// Convenience: format and send a command, ensuring CRLF suffix
static int ctrl_sendf(const char *fmt, ...)
{
va_list ap;
int n;
va_start(ap, fmt);
n = vsnprintf(g_tx, sizeof(g_tx)-3, fmt, ap);
va_end(ap);
if (n < 0) return 0;
// Ensure CRLF termination
if (n > (int)sizeof(g_tx) - 3) n = (int)sizeof(g_tx) - 3;
g_tx[n++] = '\r';
g_tx[n++] = '\n';
g_tx[n] = '\0';
LOG_INFO("CTRL >> %s", g_tx);
return socket_send(g_ctrl_sock, g_tx, n) >= 0;
}
// Enter passive mode; parse 227 and extract IP:port
static int ctrl_enter_pasv(char *ip_out, int *port_out)
{
int code;
char *beg, *end;
int h1,h2,h3,h4,p1,p2;
if (!ctrl_send_line("PASV\r\n")) return 0;
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 227) return 0;
beg = strchr(g_rx, '(');
end = strchr(g_rx, ')');
if (!beg || !end || end <= beg) return 0;
*end = '\0';
if (sscanf(beg+1, "%d,%d,%d,%d,%d,%d", &h1,&h2,&h3,&h4,&p1,&p2) != 6) return 0;
snprintf(ip_out, 32, "%d.%d.%d.%d", h1, h2, h3, h4);
*port_out = (p1 << 8) | p2;
return 1;
}
// Upload a buffer as a file (STOR)
int ftp_upload(char *remote_name, void *data, int length)
{
int code;
char ip[32];
int port;
int sent = 0;
if (!ctrl_enter_pasv(ip, &port))
return 0;
if (socket_connect(g_data_sock, ip, port) != 1)
return 0;
if (!ctrl_sendf("STOR %s", remote_name)) {
socket_close(g_data_sock);
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 150 && code != 125) {
socket_close(g_data_sock);
return 0;
}
// Stream data to data socket
while (sent < length) {
int chunk = socket_send(g_data_sock, (char*)data + sent, length - sent);
if (chunk <= 0) {
LOG_INFO("upload: send error\r\n");
socket_close(g_data_sock);
return 0;
}
sent += chunk;
}
socket_close(g_data_sock);
code = ctrl_read_reply(g_rx, sizeof(g_rx));
return (code == 226);
}
// Download a file into a buffer (RETR)
int ftp_download(char *remote_name, void *buf, int maxlen)
{
int code;
char ip[32];
int port;
int total = 0;
if (!ctrl_enter_pasv(ip, &port))
return 0;
if (socket_connect(g_data_sock, ip, port) != 1) {
LOG_INFO("data connect failed\r\n");
return 0;
}
if (!ctrl_sendf("RETR %s", remote_name)) {
socket_close(g_data_sock);
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 150 && code != 125) {
socket_close(g_data_sock);
return 0;
}
// Read until EOF or buffer is full
while (total < maxlen) {
int r = socket_recv(g_data_sock, (char*)buf + total, maxlen - total);
if (r <= 0) break;
total += r;
LOG_INFO("download %d/%d\r\n", total, maxlen);
}
LOG_INFO("download complete: %d/%d bytes\r\n", total, maxlen);
socket_close(g_data_sock);
code = ctrl_read_reply(g_rx, sizeof(g_rx));
return (code == 226);
}
// Query server for file size using SIZE
int ftp_filesize(char *remote_name)
{
int code;
int size = -1;
if (!ctrl_sendf("SIZE %s", remote_name))
return -1;
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 213)
return -1;
// Expect "213 <size>..."; parse after code and space
char *p = g_rx + 3;
while (*p == ' ') p++;
size = atoi(p);
return size;
}
// Login and prepare binary mode
int ftp_login(char *addr, int port, char *username, char *password)
{
int code;
LOG_INFO("connecting...\r\n");
if (socket_connect(g_ctrl_sock, addr, port) != 1) {
LOG_INFO("connect failed\r\n");
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 220) {
socket_close(g_ctrl_sock);
return 0;
}
if (!ctrl_sendf("USER %s", username)) {
socket_close(g_ctrl_sock);
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 331 && code != 230) {
socket_close(g_ctrl_sock);
return 0;
}
if (code == 331) {
if (!ctrl_sendf("PASS %s", password)) {
socket_close(g_ctrl_sock);
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 230) {
socket_close(g_ctrl_sock);
return 0;
}
}
// Binary mode
if (!ctrl_send_line("TYPE I\r\n")) {
socket_close(g_ctrl_sock);
return 0;
}
code = ctrl_read_reply(g_rx, sizeof(g_rx));
if (code != 200 && code != 250) {
socket_close(g_ctrl_sock);
return 0;
}
return 1;
}
void ftp_quit(void)
{
ctrl_send_line("QUIT\r\n");
socket_close(g_ctrl_sock);
}
void ftp_init(void)
{
g_ctrl_sock = socket_create();
g_data_sock = socket_create();
}