/** * @file ftp.c * @brief Simple FTP client for ClaudeOS. * * Connects to an FTP server and provides a minimal interactive * interface for sending FTP commands and viewing responses. * * Usage: * ftp [:] * * Examples: * ftp 10.0.2.2 * ftp 192.168.1.1:2121 * * Once connected, type FTP commands directly: * USER anonymous * PASS user@ * LIST * QUIT * * The client handles the control connection. PASV data connections * are not supported in this minimal version. */ #include "syscalls.h" typedef unsigned char uint8_t; /** * Parse a decimal number from a string. */ static uint32_t parse_uint(const char **s) { uint32_t val = 0; while (**s >= '0' && **s <= '9') { val = val * 10 + (uint32_t)(**s - '0'); (*s)++; } return val; } /** * Parse an IPv4 address. */ static uint32_t parse_ip(const char **s) { uint32_t ip = 0; for (int i = 0; i < 4; i++) { uint32_t octet = parse_uint(s); if (octet > 255) return 0; ip = (ip << 8) | octet; if (i < 3) { if (**s != '.') return 0; (*s)++; } } return ip; } /** * Print a decimal number. */ static void print_dec(uint32_t val) { char buf[12]; int i = 0; if (val == 0) { putchar('0'); return; } while (val > 0) { buf[i++] = '0' + (char)(val % 10); val /= 10; } while (i > 0) putchar(buf[--i]); } /** * Format IP address. */ static void ip_to_str(uint32_t ip, char *buf) { int pos = 0; for (int i = 3; i >= 0; i--) { uint32_t octet = (ip >> (i * 8)) & 0xFF; char tmp[4]; int ti = 0; if (octet == 0) { tmp[ti++] = '0'; } else { while (octet > 0) { tmp[ti++] = '0' + (char)(octet % 10); octet /= 10; } } while (ti > 0) buf[pos++] = tmp[--ti]; if (i > 0) buf[pos++] = '.'; } buf[pos] = '\0'; } /** * Read a line from stdin with echo. * Returns length (excluding newline). */ static int readline(char *buf, int maxlen) { int pos = 0; while (pos < maxlen - 1) { char c; int32_t n = read(0, &c, 1); if (n <= 0) { yield(); continue; } if (c == '\n' || c == '\r') { putchar('\n'); break; } if (c == '\b' || c == 127) { if (pos > 0) { pos--; puts("\b \b"); } } else if (c >= 32) { buf[pos++] = c; putchar(c); } } buf[pos] = '\0'; return pos; } /** * Receive and print FTP server response. * Reads until we get a line starting with a 3-digit code followed by space. * Returns the response code, or -1 on error. */ static int32_t recv_response(int32_t sockfd) { char buf[512]; int total = 0; int timeout = 500; int code = -1; while (timeout > 0) { int32_t n = net_recv(sockfd, buf + total, (uint32_t)(sizeof(buf) - 1 - (uint32_t)total)); if (n > 0) { total += n; buf[total] = '\0'; /* Print received data */ write(1, buf + total - n, (uint32_t)n); /* Check if we have a complete response. * FTP response ends when we get "NNN " (3 digits + space) at start of line. */ int got_complete = 0; for (int i = 0; i < total; i++) { /* Check for line start */ if (i == 0 || (i > 0 && buf[i-1] == '\n')) { /* Check for "NNN " pattern */ if (i + 3 < total && buf[i] >= '0' && buf[i] <= '9' && buf[i+1] >= '0' && buf[i+1] <= '9' && buf[i+2] >= '0' && buf[i+2] <= '9' && buf[i+3] == ' ') { code = (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0'); got_complete = 1; } } } if (got_complete) break; timeout = 200; } else if (n < 0) { return -1; } else { yield(); timeout--; } } return code; } /** * Send an FTP command (appends \r\n). */ static int32_t send_cmd(int32_t sockfd, const char *cmd) { char buf[256]; uint32_t len = 0; while (cmd[len] && len < sizeof(buf) - 3) { buf[len] = cmd[len]; len++; } buf[len++] = '\r'; buf[len++] = '\n'; return net_send(sockfd, buf, len); } int main(void) { char arg[256]; if (getenv("ARG1", arg, sizeof(arg)) < 0 || arg[0] == '\0') { puts("Usage: ftp [:]\n"); puts(" e.g. ftp 10.0.2.2\n"); return 1; } /* Parse IP and port */ const char *p = arg; uint32_t ip = parse_ip(&p); if (ip == 0) { puts("ftp: invalid IP address\n"); return 1; } uint32_t port = 21; if (*p == ':') { p++; port = parse_uint(&p); if (port == 0 || port > 65535) { puts("ftp: invalid port\n"); return 1; } } char ip_str[64]; ip_to_str(ip, ip_str); puts("Connecting to "); puts(ip_str); putchar(':'); print_dec(port); puts("...\n"); /* Create TCP socket */ int32_t sockfd = socket(SOCK_TCP); if (sockfd < 0) { puts("ftp: failed to create socket\n"); return 1; } /* Connect */ if (connect(sockfd, ip, port) < 0) { puts("ftp: connect failed\n"); return 1; } /* Wait for connection */ int timeout = 500; while (timeout > 0) { int32_t state = sockstate(sockfd); if (state == TCP_STATE_ESTABLISHED) break; if (state == TCP_STATE_CLOSED) { puts("ftp: connection refused\n"); return 1; } yield(); timeout--; } if (timeout <= 0) { puts("ftp: connection timed out\n"); return 1; } puts("Connected to "); puts(ip_str); puts("\n"); /* Read server welcome banner */ int32_t code = recv_response(sockfd); if (code < 0 || code >= 400) { puts("ftp: server rejected connection\n"); return 1; } /* Interactive command loop */ char cmd[256]; for (;;) { puts("ftp> "); int len = readline(cmd, sizeof(cmd)); if (len == 0) continue; /* Check for local quit command */ if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0 || strcmp(cmd, "bye") == 0) { send_cmd(sockfd, "QUIT"); recv_response(sockfd); puts("Goodbye.\n"); break; } /* Check for help */ if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) { puts("ClaudeOS FTP Client\n"); puts("Send raw FTP commands:\n"); puts(" USER - specify username\n"); puts(" PASS - specify password\n"); puts(" PWD - print working directory\n"); puts(" CWD - change directory\n"); puts(" LIST - list files (control channel only)\n"); puts(" SYST - show system type\n"); puts(" STAT - show server status\n"); puts(" QUIT - disconnect\n"); puts(" quit/exit/bye - disconnect and exit\n"); continue; } /* Send the command to server */ if (send_cmd(sockfd, cmd) < 0) { puts("ftp: send failed\n"); break; } /* Receive response */ code = recv_response(sockfd); if (code < 0) { puts("ftp: connection lost\n"); break; } } return 0; }