Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing a Minimal Passive-Mode FTP Client in C

Tech 4

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)

  1. Connect to <server, port> on the control socket; expect 220.
  2. 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
  3. TYPE I → 200 to enforce binary transfers.
  4. Optional: SIZE /path/file → 213 or an error like 550 if missing.
  5. PASV → 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
    • Data endpoint: IP = h1.h2.h3.h4, Port = p1*256 + p2.
  6. Connect the data socket to the PASV endpoint.
  7. 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)

  1. PASV → 227 and parse endpoint.
  2. Connect the data socket to the reported IP:port.
  3. STOR /path/file → expect 150/125, then write file bytes to the data socket.
  4. 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();
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.