Files
etaHEN/Source Code/util/source/ftp.c
LightningMods 0fe0407b3a etaHEN 2.4B
etaHEN 2.4B

etaHEN 2.4B Change log

- Updated to support the latest PS5 Payload SDK
- Fixed etaHEN and Cheats support for 8.40-10.01
- Added a Game Overlay menu to show CPU/GPU Temp and utilization, Local IP Address and other future states
- Added a Kstuff menu for options like downloading the latest kstuff from github, turning off kstufff autoload and more
- Added a Custom Background Package Installer for installing PKGs from internal storage from any directory (Requires DPIv2 enabled for 5.50+)
- DPIv2 can now download local files via url example http://192.xxx.xxx.xxx:12800/data/etaHEN/etaHEN.log
- Improved Cheats support, cheats with or without 0 sections are now supported
- Added Fix by TheFlow to Improve 2.xx PS4 PKG speeds
- Replaced the donation links in the etaHEN credits menu with ones to github sponsers
- Removed the non-whitelist app jailbreak option and moved it to an optional Legacy CMD Server option in the etaHEN Settings off by default
- Game Decryptor has been updated for the Itemzflow Dumper
- Updated the Plugin loader System
- The Payload SDK ELFLDR is now REQUIRED for etaHEN to load
- Replaced HTTP2 with Curl for better compatibility
- Added timeout for ShellUI to receive a response (will stop it from freezing if no response is given)

small fix
2025-12-01 20:31:16 -05:00

2533 lines
76 KiB
C

/*
* Copyright (c) 2015 Sergi Granell (xerpi)
*/
#include <arpa/inet.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <iso646.h>
#include <pthread.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include "common_utils.h"
#include <unistd.h>
bool ftp_started = false;
void notify(bool error, const char *fmt, ...);
extern atomic_bool rest_mode_action;
extern char ip_address[];
int close(int fd);
void* malloc(size_t size);
void etaHEN_log(const char *fmt, ...);
void free(void *ptr);
bool if_exists(const char *path);
int sceNetSocketAbort(int socket, unsigned int flags);
int sceNetCtlInit(void);
int sceNetCtlGetInfo(int size, SceNetCtlInfo *info);
/// Shared preprocessor directives for all systems -----------------------------
#define PS4 1
#define PATH_MAX 1024
#define CMD_LINE_BUF_SIZE PATH_MAX + 255
#define CRLF "\r\n"
#define FILE_BUF_SIZE 8192
#define DEFAULT_PATH "/"
#define DEFAULT_PORT 1337
#define SCE_NET_SOCKET_ABORT_FLAG_RCV_PRESERVATION 0x00000001
#define SCE_NET_SOCKET_ABORT_FLAG_SND_PRESERVATION 0x00000002
#define SCE_NET_SO_REUSEADDR 0x00000004
#define SCE_NET_ERROR_EINTR 0x80410104
// Default FTP reply codes -----------------------------------------------------
// Commented reply codes mean their string needs to be generated dynamically.
// Syntax (x0z)
#define RC_200 "200 Command okay." CRLF
#define RC_500 "500 Syntax error, command unrecognized." CRLF
#define RC_501 "501 Syntax error in arguments." CRLF
#define RC_202 "202 Command not implemented, superfluous at this site." CRLF
#define RC_502 "502 Command not implemented." CRLF
#define RC_503 "503 Bad sequence of commands." CRLF
#define RC_504 "504 Command not implemented for that parameter." CRLF
#define MAX(a,b) ((a) > (b) ? (a) : (b))
// Information (x1z)
// 110 Restart marker reply.
// 211 System status, or system help reply.
// 212 Directory status reply.
// 213 File status reply.
// 214 Help message.
// 215 NAME system type.
// Connections (x2z)
// 120 Service ready in nnn minutes.
#define RC_220 "220 Service ready for new user." CRLF
#define RC_221 "221 Service closing control connection." CRLF
#define RC_421 "421 Service not available, closing control connection." CRLF
#define RC_125 "125 Data connection already open; transfer starting." CRLF
#define RC_225 "225 Data connection open; no transfer in progress." CRLF
#define RC_425 "425 Can't open data connection." CRLF
#define RC_226 "226 Closing data connection." \
" Requested file action successful." CRLF
#define RC_426 "426 Connection closed; transfer aborted." CRLF
// 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
// Authentication and accounting (x3z)
#define RC_230 "230 User logged in, proceed." CRLF
#define RC_530 "530 Not logged in." CRLF
#define RC_331 "331 User name okay, need password." CRLF
#define RC_332 "332 Need account for login." CRLF
#define RC_532 "532 Need account for storing files." CRLF
// File system (x5z)
#define RC_150 "150 File status okay; about to open data connection." CRLF
#define RC_250 "250 Requested file action okay, completed." CRLF
// 257 "PATHNAME" created.
#define RC_350 "350 Requested file action pending further information." CRLF
#define RC_450 "450 Requested file action not taken." \
" File temporarily unavailable." CRLF
#define RC_550 "550 Requested action not taken. File unavailable." CRLF
#define RC_451 "451 Requested action aborted." \
" Local error in processing." CRLF
#define RC_551 "551 Requested action aborted. Page type unknown." CRLF
#define RC_452 "452 Requested action not taken." \
" Insufficient storage space in system." CRLF
#define RC_552 "552 Requested file action aborted." \
" Exceeded storage allocation." CRLF
#define RC_553 "553 Requested file action aborted." \
" File name not allowed." CRLF
/// Preprocessor directives for PS4 --------------------------------------------
#undef PATH_MAX
#define PATH_MAX 1024
#define FILE_PERM 0777
#define DIR_PERM 0777
// Uncomment this line or use compiler option -DDEBUG_PS4 to enable PS4 debug
// messages and send them to a computer.
//#define DEBUG_PS4
/// ----------------------------------------------------------------------------
// Describes the client's data connection.
enum data_connection_type {
FTP_DATA_CONNECTION_NONE, // No opened data connection
FTP_DATA_CONNECTION_ACTIVE, // Data connection socket (.data_sockfd) used
FTP_DATA_CONNECTION_PASSIVE, // Same as _ACTIVE, plus .pasv_sockfd used
};
// Stores the client's currently set MLST facts.
// When implementing a new fact, the following parts of ftp.c must be updated:
// cmd_FEAT(), cmd_OPTS_MLST, send_facts(), server_thread().
struct facts {
int modify; // Last modification time
int size; // Size in bytes
int type; // Entry type (dir, file, ...)
int unique; // Unique ID of file/directory
int unix_group; // Group ID
int unix_mode; // Unix file permissions
int unix_owner; // User ID
};
// Contains information about a connected client.
struct client_info {
char ipv4[16]; // Client's IPv4 address in text form;
pthread_t thid; // Client's thread UID
int ctrl_sockfd; // Control connection socket
int data_sockfd; // Data connection socket
int pasv_sockfd; // PASV connection socket
struct sockaddr_in ctrl_sockaddr; // Control socket information (IPv4, port)
struct sockaddr_in data_sockaddr; // Data socket information
enum data_connection_type data_con_type;
char cmd_line[CMD_LINE_BUF_SIZE]; // Command line sent by clients
char *cmd_args; // Command line arguments of .cmd_line
char cur_path[PATH_MAX]; // Current FTP directory (no trailing '/')
char rename_path[PATH_MAX]; // Rename path
long long restore_point; // Offset to resume outgoing files at
char binary_flag; // File mode for APPE, RETR, STOR, STOU
int umask; // Current file mode creation mask
struct facts facts; // Client's currently set MLST facts
struct client_info *next; // Next client in the client list
struct client_info *prev; // Previous client in the client list
};
#define SOCKETCLOSE(x) close(x)
// Describes whether an FTP command forbids, requires, or allows arguments.
enum cmd_args_flag {
ARGS_NONE,
ARGS_REQUIRED,
ARGS_OPTIONAL,
};
// Contains an FTP command's name and associated function pointer.
struct ftp_command {
const char *name; // The FTP command's name (in capital letters)
void (*function)(struct client_info *client); // The FTP command's function
enum cmd_args_flag args_flag;
};
// Initializes and starts the FTP server.
// *ip: a valid IPv4 address
// port: a valid port number (1-65535)
int init(const char *ip, unsigned short port, const char *default_directory);
// Terminates the FTP server.
void fini(void);
void check_ftp_addr_change(void);
bool StartFTP(void);
void ShutdownFTP(void);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat"
// libPS4/include/elf64.h
_Atomic int run = 0;
struct tm *gmtime_r(const time_t *timep, struct tm *result) {
// Number of days in each month for non-leap and leap years
static const int days_in_months[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static const int days_in_months_leap[] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
time_t t = *timep;
int year = 1970;
// Calculate number of days, hours, minutes, and seconds
int seconds = t % 60;
t /= 60;
int minutes = t % 60;
t /= 60;
int hours = t % 24;
t /= 24;
int days = t;
// Calculate the year
while (days >= 365) {
// Check for leap year
int leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
if (days >= 365 + leap) {
days -= 365 + leap;
year++;
} else {
break;
}
}
// Calculate the month and day
const int *days_in_current_months = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) ? days_in_months_leap : days_in_months;
int month = 0;
while (days >= days_in_current_months[month]) {
days -= days_in_current_months[month];
month++;
}
// Fill in the result struct
result->tm_sec = seconds;
result->tm_min = minutes;
result->tm_hour = hours;
result->tm_mday = days + 1;
result->tm_mon = month;
result->tm_year = year - 1900;
result->tm_wday = (t + 4) % 7; // 1970-01-01 was a Thursday
result->tm_yday = t;
result->tm_isdst = 0;
return result;
}
static struct in_addr server_ip; // Server's IPv4 address in binary form
static unsigned short server_port; // Server's port
static pthread_t server_thid; // Server's thread ID
static _Atomic int server_sockfd; // Server's socket file descriptor
static struct client_info *client_list; // Linked list used for SHUTDOWN
static pthread_mutex_t client_list_mtx; // Linked list's lock
static struct ftp_command *ftp_commands; // Points to available FTP commands
/// Debug functions ------------------------------------------------------------
#if (defined(PS4) && defined(DEBUG_PS4)) || (!defined(PS4) && defined(DEBUG))
// Sends a debug message containing a return value's error information.
// In some cases, return values for PS4 functions are < 0 instead of exactly -1.
#define debug_retval(ret_val) \
do { \
etaHEN_log("Line %d: Return value: %d, errno: %d (\"%s\").", __LINE__, \
ret_val, errno, strerror(errno)); \
errno = 0; \
} while (0)
// Same, but can be wrapped around functions of return type int to debug
// non-zero return values. Useful if the return value does not need to be saved.
#define debug_func(ret_val) \
do { \
if (ret_val) { \
etaHEN_log("Line %d: " #ret_val ": Return value: %d, errno:" \
" %d (\"%s\").", \
__LINE__, ret_val, errno, strerror(errno)); \
errno = 0; \
} \
} while (0)
#else
#define debug_retval(x)
#define debug_func(x) x
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/elf_common.h>
#include <sys/elf64.h>
#include <sys/elf32.h>
#include <sys/mman.h>
#define SELF_PROSPERO_MAGIC 0xEEF51454
typedef struct elf32_hdr_new {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry; /* Entry point */
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf32_Ehdr_new;
// Checks if a file or path exists on the FTP server.
// Used in FTP commands LIST and RNFR.
static int ftp_file_exists(const char *path) {
struct stat s;
return (stat(path, &s) >= 0);
}
typedef struct elf64_hdr_new {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry; /* Entry point virtual address */
uint64_t e_phoff; /* Program header table file offset */
uint64_t e_shoff; /* Section header table file offset */
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf64_Ehdr_new;
typedef struct elf32_phdr_new {
uint32_t p_type;
uint32_t p_offset;
uint32_t p_vaddr;
uint32_t p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} Elf32_Phdr_new;
typedef struct elf64_phdr_new {
uint32_t p_type;
uint32_t p_flags;
uint64_t p_offset; /* Segment file offset */
uint64_t p_vaddr; /* Segment virtual address */
uint64_t p_paddr; /* Segment physical address */
uint64_t p_filesz; /* Segment size in file */
uint64_t p_memsz; /* Segment size in memory */
uint64_t p_align; /* Segment alignment, file & memory */
} Elf64_Phdr_new;
struct sce_self_header
{
uint32_t magic; // 0x00
uint8_t version; // 0x04
uint8_t mode; // 0x05
uint8_t endian; // 0x06
uint8_t attributes; // 0x07
uint32_t key_type; // 0x08
uint16_t header_size; // 0x0C
uint16_t metadata_size; // 0x0E
uint64_t file_size; // 0x10
uint16_t segment_count; // 0x18
uint16_t flags; // 0x1A
char pad_2[0x4]; // 0x1C
}; // Size: 0x20
struct sce_self_segment_header {
uint64_t flags; // 0x00
uint64_t offset; // 0x08
uint64_t compressed_size; // 0x10
uint64_t uncompressed_size; // 0x18
}; // Size: 0x20
int decrypt_self(const char* path, const char* out_path) {
int self_fd;
uint64_t final_file_size;
struct elf64_hdr_new* elf_header;
struct elf64_phdr_new* start_phdrs;
struct elf64_phdr_new* cur_phdr;
struct sce_self_header* header;
char* self_file_data;
char* out_file_data;
void* segment_data;
char note_buf[0x1000];
etaHEN_log("decrypt_self: path=[%s]", path);
// Open SELF file
self_fd = open(path, O_RDONLY);
if (self_fd < 0) {
etaHEN_log("Failed to open SELF file: %s", strerror(errno));
return self_fd;
}
self_file_data = (char *) mmap(NULL, 0x1000, PROT_READ, MAP_SHARED, self_fd, 0);
if (self_file_data == MAP_FAILED) {
etaHEN_log("Failed to map self file errno: %d : %s", errno, strerror(errno));
close(self_fd);
return -ENOMEM;
}
header = (struct sce_self_header*)self_file_data;
// Get ELF headers
elf_header = (struct elf64_hdr_new*)(
(char*)self_file_data + sizeof(struct sce_self_header) +
(sizeof(struct sce_self_segment_header) * header->segment_count)
);
start_phdrs = (struct elf64_phdr_new*)((char*)(elf_header)+sizeof(struct elf64_hdr_new));
// Allocate backing buffer for output file data. We'll get size by finding the NOTE program header which should be
// in most SELFs
cur_phdr = start_phdrs;
final_file_size = 0;
for (int i = 0; i < elf_header->e_phnum; i++) {
final_file_size = MAX(final_file_size, cur_phdr->p_offset + cur_phdr->p_filesz);
cur_phdr++;
}
if (final_file_size == 0) {
munmap(self_file_data, 0x1000);
close(self_fd);
return -EINVAL;
}
// Map buffer for output data
out_file_data = (char *) mmap(NULL, final_file_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (out_file_data == MAP_FAILED) {
etaHEN_log("Failed to map out_file_data errno: %d : %s", errno, strerror(errno));
munmap(self_file_data, 0x1000);
close(self_fd);
return -12;
}
// Copy ELF headers over
memcpy(out_file_data, elf_header, sizeof(struct elf64_hdr_new));
memcpy(out_file_data + sizeof(struct elf64_hdr_new), start_phdrs, elf_header->e_phnum * sizeof(struct elf64_phdr_new));
// Decrypt and copy segments
cur_phdr = start_phdrs;
for (uint64_t i = 0; i < elf_header->e_phnum; i++) {
if (cur_phdr->p_type == PT_LOAD || cur_phdr->p_type == 0x61000000) {
//etaHEN_log("decrypt_self: seg=0x%lx", i);
segment_data = mmap(NULL, cur_phdr->p_filesz, PROT_READ, MAP_SHARED | 0x80000, self_fd, (i << 32));
if (segment_data == MAP_FAILED) {
etaHEN_log("Failed to map segment_data errno: %d : %s", errno, strerror(errno));
munmap(self_file_data, 0x1000);
close(self_fd);
return -EIO;
}
//etaHEN_log("decrypt_self: copying %p (size = 0x%lx)", segment_data, cur_phdr->p_filesz);
//DumpHex(segment_data, 0x100);
memcpy(out_file_data + cur_phdr->p_offset, segment_data, cur_phdr->p_filesz);
//etaHEN_log("decrypt_self: unmap %p", segment_data);
munmap(segment_data, cur_phdr->p_filesz);
//etaHEN_log("decrypt_self: done");
}
if (cur_phdr->p_type == 0x6FFFFF00) {
lseek(self_fd, cur_phdr->p_offset, SEEK_SET);
read(self_fd, &note_buf, cur_phdr->p_filesz);
memcpy(out_file_data + cur_phdr->p_offset, note_buf, cur_phdr->p_filesz);
}
cur_phdr++;
}
munmap(self_file_data, 0x1000);
close(self_fd);
// Write decrypted SELF to the specified USB path
int out_fd = open(out_path, O_RDWR | O_CREAT | O_TRUNC, 0666);
if (out_fd < 0) {
munmap(out_file_data, final_file_size);
etaHEN_log("Failed to open output file: %s", strerror(errno));
return -EIO;
}
ssize_t written_bytes = write(out_fd, out_file_data, final_file_size);
if (written_bytes != final_file_size) {
munmap(out_file_data, final_file_size);
close(out_fd);
etaHEN_log("Failed to write entire output file: %s", strerror(errno));
return -EIO;
}
munmap(out_file_data, final_file_size);
close(out_fd);
etaHEN_log("Successfully decrypted and saved to %s", out_path);
return 0;
}
#define SELF_PROSPERO_MAGIC 0xEEF51454
#define SELF_ORBIS_MAGIC 0x4F153D1D
bool Check_ELF_Magic(const char* path, uint32_t FILE_MAGIC) {
// Check for empty or pure whitespace path
int magic = 0;
int file_fd = open(path, O_RDONLY, 0);
if (file_fd < 0) {
etaHEN_log("Error opening file: %s", path);
return false;
}
// Check if it's a Prospero SELF
read(file_fd, (void * ) & magic, sizeof(magic));
close(file_fd);
return magic == FILE_MAGIC;
}
bool is_self(const char * path){
return Check_ELF_Magic(path, SELF_PROSPERO_MAGIC) || Check_ELF_Magic(path, SELF_ORBIS_MAGIC);
}
typedef struct
{
uint64_t pad0;
char version_str[0x1C];
uint32_t version;
uint64_t pad1;
} OrbisKernelSwVersion;
bool is_2xx = false;
int sceKernelGetProsperoSystemSwVersion(OrbisKernelSwVersion* sw);
int decrypt_self_ftp(const char* input_file_path, const char* output_file_path);
/// Reimplementation of missing functions --------------------------------------
static int decrypt_temp(struct client_info *client, char *file_path, char *buf,
size_t bufsize)
{
char temp_path[bufsize];
if(strstr(file_path, "safemode.elf") != NULL){
etaHEN_log("safemode.elf CANNOT be decrypted");
return -1;
}
mkdir("/user/temp", 0777);
// Create a unique file name.
int ret = snprintf(temp_path, bufsize, "/user/temp/PS5_temp_file_%d_%ld_%d",
client->ctrl_sockfd, time(NULL), rand() % 10000);
if (ret < 0 || (unsigned int) ret > bufsize -1) {
debug_retval(ret);
return -1;
}
while (-ftp_file_exists(temp_path) && strlen(temp_path) + 1 < bufsize) {
etaHEN_log("Temporary file \"%s\" already exists.", temp_path);
strcat(temp_path, "_");
}
etaHEN_log("Decrypting file \"%s\", using temporary file \"%s\"...",
file_path, temp_path);
if (is_2xx)
decrypt_self(file_path, temp_path);
else
decrypt_self_ftp(file_path, temp_path);
strcpy(buf, temp_path);
return 0;
}
struct ioctl_C0105203_args
{
void* buffer;
int size;
int error;
};
int ioctl(int fd, unsigned long request, ...);
int rnps_decrypt_block(void* buffer, int size)
{
int handle = open("/dev/rnps", 2);
if (handle < 0)
{
return 0x800F1213;
}
struct ioctl_C0105203_args args;
args.buffer = buffer;
args.size = size;
args.error = 0x800F1225;
int error;
if (ioctl(handle, 0xC0105203, &args) < 0)
{
etaHEN_log("ioctl failed: %s", strerror(errno));
return -1;
}
else
{
error = args.error;
}
close(handle);
return error;
}
static int decrypt_rnps(struct client_info *client, char *file_path, char *buf,
size_t bufsize)
{
char temp_path[bufsize];
int fd_in = -1, fd_out = -1;
unsigned char* file_buf = NULL;
int result = 0;
// Open the input file for reading
fd_in = open(file_path, O_RDONLY);
if (fd_in < 0) {
etaHEN_log("Failed to open input file %s", file_path);
return -1;
}
// Allocate buffer (e.g., 16 MB buffer)
const size_t BUFFER_SIZE = 0x10000000; // 16 MB
file_buf = (unsigned char*)malloc(BUFFER_SIZE);
if (!file_buf) {
etaHEN_log("Failed to allocate memory %d", BUFFER_SIZE);
result = -2;
goto cleanup;
}
// Read the input file into the buffer
ssize_t bytes_read = read(fd_in, file_buf, BUFFER_SIZE);
if (bytes_read < 0) {
perror("Failed to read from input file");
result = -3;
goto cleanup;
}
// Decrypt the data
if (rnps_decrypt_block(file_buf, (unsigned int)bytes_read) != 0) {
// etaHEN_log("Failed DECRYPTION OF %S", file_buf);
result = -4;
goto cleanup;
}
mkdir("/user/temp", 0777);
// Create a unique file name.
int ret = snprintf(temp_path, bufsize, "/user/temp/PS5_temp_file_%d_%ld_%d",
client->ctrl_sockfd, time(NULL), rand() % 10000);
if (ret < 0 || (unsigned int) ret > bufsize -1) {
debug_retval(ret);
result = -9;
goto cleanup;
}
etaHEN_log("Decrypting file \"%s\", using temporary file \"%s\"...",
file_path, temp_path);
// Open the output file for writing
fd_out = open(temp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_out < 0) {
etaHEN_log("Failed to open output file %s, %s", temp_path, strerror(errno));
result = -5;
goto cleanup;
}
// Write the decrypted data to the output file
ssize_t bytes_written = write(fd_out, file_buf, bytes_read);
if (bytes_written < 0 || bytes_written != bytes_read) {
etaHEN_log("Failed to write to output file %s, %s", temp_path, strerror(errno));
result = -6;
goto cleanup;
}
strcpy(buf, temp_path);
cleanup:
// Clean up resources
if (fd_in >= 0) close(fd_in);
if (fd_out >= 0) close(fd_out);
if (file_buf) free(file_buf);
return result;
}
/// Message-sending functions --------------------------------------------------
// Sends a control message string to a connected client.
// Only complete messages, ending with CRLF, should be sent.
#define send_ctrl_msg(client, str) \
do { \
send(client->ctrl_sockfd, str, strlen(str), MSG_NOSIGNAL); \
} while (0)
// Sends a formatted control message string to a connected client.
#define sendf_ctrl_msg(client, fmt, ...) \
do { \
char msg[CMD_LINE_BUF_SIZE]; \
snprintf(msg, sizeof(msg), fmt, __VA_ARGS__); \
send_ctrl_msg(client, msg); \
} while (0)
// Same as sendf_ctrl_msg(), but uses the data connection.
#define sendf_data_msg(client, fmt, ...) \
do { \
char msg[CMD_LINE_BUF_SIZE]; \
snprintf(msg, sizeof(msg), fmt, __VA_ARGS__); \
send_data_msg(client, msg); \
} while (0)
/// Functions shared by FTP commands -------------------------------------------
char *strcasestr(const char *haystack, const char *needle)
{
if (*haystack == '\0' || *needle == '\0')
return NULL;
size_t haystack_len = strlen(haystack);
size_t needle_len = strlen(needle);
if (haystack_len < needle_len)
return NULL;
size_t diff_len = haystack_len - needle_len;
do {
if (tolower(*haystack) == tolower(*needle)) {
const char *a = haystack;
const char *b = needle;
do {
++a;
++b;
if (*b == '\0')
return (char *) haystack;
} while (tolower(*a) == tolower(*b));
}
++haystack;
} while (diff_len-- > 0);
return NULL;
}
// Opens a client's data connection, returning < 0 on error.
// Used in FTP commands RETR and STOR.
static int open_data_connection(struct client_info *client) {
int ret;
if (client->data_con_type == FTP_DATA_CONNECTION_ACTIVE)
ret =
connect(client->data_sockfd, (struct sockaddr *)&client->data_sockaddr,
sizeof(client->data_sockaddr));
else
ret = client->pasv_sockfd = accept(client->data_sockfd, NULL, NULL);
if (ret < 0) {
debug_retval(ret);
}
return ret;
}
// Closes a client's data connection.
// Used in FTP commands LIST, NLST, RETR, and STOR.
static void close_data_connection(struct client_info *client) {
if (client->data_con_type == FTP_DATA_CONNECTION_NONE)
return;
debug_func(SOCKETCLOSE(client->data_sockfd));
if (client->data_con_type == FTP_DATA_CONNECTION_PASSIVE)
debug_func(SOCKETCLOSE(client->pasv_sockfd));
client->data_con_type = FTP_DATA_CONNECTION_NONE;
}
// Sends a string via a client's data connection.
// Used in FTP commands LIST and NLST.
static inline void send_data_msg(struct client_info *client, char *str) {
if (client->data_con_type == FTP_DATA_CONNECTION_ACTIVE)
send(client->data_sockfd, str, strlen(str), MSG_NOSIGNAL);
else
send(client->pasv_sockfd, str, strlen(str), MSG_NOSIGNAL);
}
// Causes a client's thread to exit.
// Used in FTP command QUIT and function client_list_terminate().
static void client_thread_exit(struct client_info *client) {
// Abort any open data connections.
if (client->data_con_type != FTP_DATA_CONNECTION_NONE) {
#ifdef PS4
debug_func(sceNetSocketAbort(
client->data_sockfd, SCE_NET_SOCKET_ABORT_FLAG_RCV_PRESERVATION |
SCE_NET_SOCKET_ABORT_FLAG_SND_PRESERVATION));
#else
debug_func(shutdown(client->data_sockfd, SHUT_RDWR));
#endif
if (client->data_con_type == FTP_DATA_CONNECTION_PASSIVE) {
#ifdef PS4
debug_func(sceNetSocketAbort(
client->pasv_sockfd, SCE_NET_SOCKET_ABORT_FLAG_RCV_PRESERVATION |
SCE_NET_SOCKET_ABORT_FLAG_SND_PRESERVATION));
#else
debug_func(shutdown(client->pasv_sockfd, SHUT_RDWR));
#endif
}
}
// Unblock the client thread's blocking accept() to make the thread exit.
#ifdef PS4
debug_func(sceNetSocketAbort(client->ctrl_sockfd,
SCE_NET_SOCKET_ABORT_FLAG_RCV_PRESERVATION));
#else
debug_func(shutdown(client->ctrl_sockfd, SHUT_RD));
#endif
}
// Generates an absolute FTP path string from the current working directory and
// an absolute or relative pathname and stores it in the provided buffer. The
// buffer should be of size PATH_MAX in order not to get truncated.
// Used in FTP commands APPE, CWD, DELE, LIST, MDTM, MLSD, MLST, MKD, NLST,
// RETR, RMD, RNFR, RNTO, "SITE CHMOD", SIZE, and STOR.
static int gen_ftp_path(char *buf, size_t buf_size, struct client_info *client,
char *pathname) {
int n;
if (pathname == NULL)
n = snprintf(buf, buf_size, "%s", client->cur_path);
else if (pathname[0] == '/') // Path is already absolute.
n = snprintf(buf, buf_size, "%s", pathname);
else // Concatenate both paths.
n = snprintf(buf, buf_size, "%s%s%s", client->cur_path,
client->cur_path[1] == '\0' ? "" : "/", pathname);
if (n >= 0 && (unsigned int)n + 1 > buf_size) { // FTP path got truncated.
etaHEN_log("Generated path larger than buffer.");
return -1;
} else {
return 0;
}
}
// Copies a source string to a buffer, inserting additional double quote
// characters, as required by the FTP standard. The buffer must be large enough
// to hold the additional characters, e.g. strlen(source) * 2 + 1.
// Used in FTP commands MKD and PWD.
static int gen_quoted_path(char *buf, int buf_size, char *source) {
if (source == NULL) {
etaHEN_log("String is NULL.");
return -1;
}
int len = strlen(source);
int i = 0, buf_i = 0;
for (; i < len && buf_i < buf_size - 1; i++, buf_i++) {
buf[buf_i] = source[i];
if (source[i] == '"')
buf[++buf_i] = '"';
}
if (buf_i > buf_size - 1) {
etaHEN_log("Buffer too small.");
return -1;
}
buf[buf_i] = '\0';
return 0;
}
// Sends a single file's facts via a client's data/control connection.
// Used in FTP commands MLSD and MLST.
// *path: the file's absolute path
static int send_facts(struct client_info *client, char *path, char *filename,
bool use_data_con) {
struct stat statbuf;
if (stat(path, &statbuf) < 0)
return -1;
char *type;
if (client->facts.type) {
if (S_ISDIR(statbuf.st_mode)) {
if (filename[0] == '.' && filename[1] == '\0')
type = (char *)"type=cdir;";
else if (filename[0] == '.' && filename[1] == '.' && filename[2] == '\0')
type = (char *)"type=pdir;";
else
type = (char *)"type=dir;";
} else {
type = (char *)"type=file;";
}
} else {
type = (char *)"";
}
char size[26];
if (client->facts.size)
snprintf(size, sizeof(size), "size=%ld;", statbuf.st_size);
else
size[0] = '\0';
char unique[42];
if (client->facts.unique)
snprintf(unique, sizeof(unique), "unique=%lx-%lx;",
(unsigned long)statbuf.st_dev, (unsigned long)statbuf.st_ino);
else
unique[0] = '\0';
char modify[23];
if (client->facts.modify) {
struct tm t;
if (gmtime_r(&statbuf.st_mtim.tv_sec, &t) == NULL)
return -1;
snprintf(modify, sizeof(modify), "modify=%d%02d%02d%02d%02d%02d;",
1900 + t.tm_year, 1 + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min,
t.tm_sec);
} else {
modify[0] = '\0';
}
char owner[20];
if (client->facts.unix_owner)
snprintf(owner, sizeof(owner), "unix.owner=%d;", statbuf.st_uid);
else
owner[0] = '\0';
char group[20];
if (client->facts.unix_group)
snprintf(group, sizeof(group), "unix.group=%d;", statbuf.st_gid);
else
group[0] = '\0';
char mode[16];
if (client->facts.unix_mode) {
int perm = statbuf.st_mode & (01111 | 02222 | 04444);
snprintf(mode, sizeof(mode), "unix.mode=%04o;", perm);
} else {
mode[0] = '\0';
}
if (use_data_con)
sendf_data_msg(client, "%s%s%s%s%s%s%s %s" CRLF, type, size, unique, modify,
owner, group, mode, filename);
else
sendf_ctrl_msg(client, " %s%s%s%s%s%s%s %s" CRLF, // Leading space req.
type, size, unique, modify, owner, group, mode, filename);
return 0;
}
// Sets a client's current directory to its parent directory.
// Used in FTP commands CWD and CDUP.
static int dir_up(struct client_info *client) {
#ifdef PS4 // Does not have the function access().
char *slash = strrchr(client->cur_path, '/');
if (slash == NULL)
return -1;
if (slash == client->cur_path)
strcpy(client->cur_path, "/");
else
*slash = '\0';
return 0;
#else
char temp_path[sizeof(client->cur_path)];
strcpy(temp_path, client->cur_path);
char *slash = strrchr(temp_path, '/');
if (slash == NULL)
return -1;
if (slash == temp_path)
strcpy(temp_path, "/");
else
*slash = '\0';
// Check if directory is accessible.
int ret;
if ((ret = access(temp_path, R_OK)) < 0) {
debug_retval(ret);
return -1;
}
strcpy(client->cur_path, temp_path);
return 0;
#endif
}
// Receives a file from the client and stores it on the server.
// Used in FTP commands APPE and STOR.
static void recv_file(struct client_info *client, const char *path) {
// Set file flags for open().
int flags = O_CREAT | O_RDWR; // Create file if necessary, open in r/w mode.
if (client->restore_point == -1)
flags = flags | O_APPEND; // Append new data to the end of the file.
else if (client->restore_point == 0)
flags = flags | O_TRUNC; // Reset the file to length 0.
// Open local file, creating it if necessary.
int fd;
if ((fd = open(path, flags, ~client->umask & FILE_PERM)) < 0) {
debug_retval(fd);
send_ctrl_msg(client, RC_550);
return;
}
// Apply restore point.
if (client->restore_point > 0) {
struct stat sb;
int ret;
if ((ret = fstat(fd, &sb)) < 0 || client->restore_point > sb.st_size ||
(ret = ftruncate(fd, client->restore_point)) < 0 ||
(ret = lseek(fd, 0, SEEK_END)) < 0) {
debug_retval(ret);
close(fd);
send_ctrl_msg(client, RC_451);
return;
}
}
// Open data connection.
send_ctrl_msg(client, RC_150);
if (open_data_connection(client) < 0) {
close(fd);
send_ctrl_msg(client, RC_425);
return;
}
// Receive remote file data and write it to local file.
int sockfd;
if (client->data_con_type == FTP_DATA_CONNECTION_ACTIVE)
sockfd = client->data_sockfd;
else
sockfd = client->pasv_sockfd;
unsigned char buffer[FILE_BUF_SIZE];
int n_received;
int n_written;
while ((n_received = recv(sockfd, buffer, sizeof(buffer), 0)) > 0) {
if ((n_written = write(fd, buffer, n_received)) != n_received) {
debug_retval(n_written);
close(fd);
close_data_connection(client);
send_ctrl_msg(client, RC_451);
return;
}
}
close(fd);
close_data_connection(client);
if (n_received == 0) { // Success.
send_ctrl_msg(client, RC_226);
} else {
debug_retval(n_received);
if (errno == 28)
send_ctrl_msg(client, RC_452);
else
send_ctrl_msg(client, RC_426);
}
}
/// FTP commands ---------------------------------------------------------------
// APPE (Append or create) "APPE <SP> <pathname> <CRLF>" -----------------------
static void cmd_APPE(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_553);
return;
}
client->restore_point = -1; // Tell recv_file() to use the O_APPEND flag.
recv_file(client, path);
}
// CDUP (Change to parent directory) "CDUP <CRLF>" -----------------------------
static void cmd_CDUP(struct client_info *client) {
if (dir_up(client))
send_ctrl_msg(client, RC_550);
else
send_ctrl_msg(client, RC_200);
}
// CWD (Change working directory) "CWD <SP> <pathname <CRLF>" ------------------
static void cmd_CWD(struct client_info *client) {
if (strcmp(client->cmd_args, "..") == 0) {
if (dir_up(client))
send_ctrl_msg(client, RC_550);
else
send_ctrl_msg(client, RC_250);
return;
}
if (strcmp(client->cmd_args, ".") == 0)
client->cmd_args = client->cur_path;
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
#ifdef PS4
if (ftp_file_exists(path)) {
#else
if (access(path, R_OK) == 0) {
#endif
strncpy(client->cur_path, path, sizeof(client->cur_path));
send_ctrl_msg(client, RC_250);
} else {
send_ctrl_msg(client, RC_550);
}
}
// DELE (Delete) "DELE <SP> <pathname> <CRLF>" ---------------------------------
static void cmd_DELE(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
int ret;
if ((ret = unlink(path)) < 0) {
debug_retval(ret);
send_ctrl_msg(client, RC_550);
} else {
send_ctrl_msg(client, RC_250);
}
}
// FEAT (Feature) "FEAT <CRLF>" ------------------------------------------------
static void cmd_FEAT(struct client_info *client) {
send_ctrl_msg(client, "211-Extensions:" CRLF);
send_ctrl_msg(client, " MDTM" CRLF);
sendf_ctrl_msg(
client,
" MLST type%s;size%s;unique%s;modify%s;"
"unix.owner%s;unix.group%s;unix.mode%s;" CRLF,
client->facts.type ? "*" : "", client->facts.size ? "*" : "",
client->facts.unique ? "*" : "", client->facts.modify ? "*" : "",
client->facts.unix_owner ? "*" : "", client->facts.unix_group ? "*" : "",
client->facts.unix_mode ? "*" : "");
send_ctrl_msg(client, " REST STREAM" CRLF);
send_ctrl_msg(client, " SITE CHMOD" CRLF);
send_ctrl_msg(client, " SITE UMASK" CRLF);
send_ctrl_msg(client, " SIZE" CRLF);
send_ctrl_msg(client, "211 END" CRLF);
}
// LIST (List) "LIST [<SP> <pathname>] <CRLF>" ---------------------------------
static int gen_list_format(char *out, int n, struct stat *st, char *file_name,
char *link_name, int cur_year) {
static const char num_to_month[][4] = {"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec"};
struct tm file_tm;
gmtime_r(&st->st_mtim.tv_sec, &file_tm);
char yt[6];
if (file_tm.tm_year == cur_year)
snprintf(yt, sizeof(yt), "%02d:%02d", file_tm.tm_hour, file_tm.tm_min);
else
snprintf(yt, sizeof(yt), "%d", 1900 + cur_year);
#define LIST_FMT "%c%c%c%c%c%c%c%c%c%c %ld %d %d %ld %s %2d %s %s"
#define LIST_ARGS \
S_ISREG(st->st_mode) ? '-' \
: S_ISDIR(st->st_mode) ? 'd' \
: S_ISLNK(st->st_mode) ? 'l' \
: S_ISBLK(st->st_mode) ? 'b' \
: S_ISCHR(st->st_mode) ? 'c' \
: S_ISFIFO(st->st_mode) ? 'p' \
: S_ISSOCK(st->st_mode) ? 's' \
: '?', \
st->st_mode & 0400 ? 'r' : '-', st->st_mode & 0200 ? 'w' : '-', \
st->st_mode & 0100 ? (st->st_mode & 04000 ? 's' : 'x') \
: st->st_mode & 04000 ? 'S' \
: '-', \
st->st_mode & 040 ? 'r' : '-', st->st_mode & 020 ? 'w' : '-', \
st->st_mode & 010 ? (st->st_mode & 02000 ? 's' : 'x') \
: st->st_mode & 02000 ? 'S' \
: '-', \
st->st_mode & 04 ? 'r' : '-', st->st_mode & 02 ? 'w' : '-', \
st->st_mode & 01 ? (st->st_mode & 01000 ? 't' : 'x') \
: st->st_mode & 01000 ? 'T' \
: '-', \
st->st_nlink, st->st_uid, st->st_gid, st->st_size, \
num_to_month[file_tm.tm_mon], file_tm.tm_mday, yt, file_name
if (link_name)
return snprintf(out, n, LIST_FMT " -> %s" CRLF, LIST_ARGS, link_name);
else
return snprintf(out, n, LIST_FMT CRLF, LIST_ARGS);
#undef LIST_FMT
#undef LIST_ARGS
}
// Sends list information about a file or directory via the data connection.
static void send_list_item(struct client_info *client, char *path,
char *file_name, int cur_year) {
char buffer[CMD_LINE_BUF_SIZE];
struct stat st;
int ret = stat(path, &st);
if (ret < 0) {
debug_retval(ret);
return;
}
// Get symbolic link.
char link_path[PATH_MAX];
link_path[0] = '\0';
if (S_ISLNK(st.st_mode)) {
ssize_t n = readlink(path, link_path, sizeof(link_path));
if (n > 0)
link_path[n] = '\0';
else {
debug_retval((int)n);
}
}
gen_list_format(buffer, sizeof(buffer), &st, file_name,
link_path[0] ? link_path : NULL, cur_year);
send_data_msg(client, buffer);
}
static void send_list(struct client_info *client, char *path) {
DIR *dfd;
struct dirent *dp;
struct stat st;
// Check if path is a directory.
int is_dir;
int ret = stat(path, &st);
if (ret < 0) {
debug_retval(ret);
return;
}
is_dir = S_ISDIR(st.st_mode);
if (is_dir) {
// Open directory.
if ((dfd = opendir(path)) == NULL) {
send_ctrl_msg(client, RC_550);
return;
}
}
// Open data connection.
send_ctrl_msg(client, RC_150);
if (open_data_connection(client) < 0) {
if (is_dir)
closedir(dfd);
send_ctrl_msg(client, RC_425);
return;
}
// Get current year.
time_t cur_time;
struct tm cur_tm;
time(&cur_time);
gmtime_r(&cur_time, &cur_tm);
if (is_dir) { // Send directory content list.
while ((dp = readdir(dfd)) != NULL) {
char file_path[PATH_MAX];
snprintf(file_path, sizeof(file_path), "%s/%s", path, dp->d_name);
send_list_item(client, file_path, dp->d_name, cur_tm.tm_year);
}
closedir(dfd);
} else { // Send single-file list.
send_list_item(client, path, path, cur_tm.tm_year);
}
send_ctrl_msg(client, RC_226);
close_data_connection(client);
}
static void cmd_LIST(struct client_info *client) {
if (client->cmd_args) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
if (ftp_file_exists(path))
send_list(client, path);
else if (strcmp("-a", client->cmd_args) == 0) // "Show all files".
send_list(client, client->cur_path);
else
send_ctrl_msg(client, RC_550);
} else {
send_list(client, client->cur_path);
}
}
// MDTM (Modification time) "MDTM <SP> <pathname> <CRLF>" ----------------------
static void cmd_MDTM(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
struct stat st;
if (stat(path, &st) < 0) {
send_ctrl_msg(client, RC_550);
return;
}
struct tm t;
if (gmtime_r(&st.st_mtim.tv_sec, &t) == NULL) {
send_ctrl_msg(client, RC_451);
return;
}
sendf_ctrl_msg(client, "213 %d%02d%02d%02d%02d%02d" CRLF, 1900 + t.tm_year,
1 + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
}
// MKD (Make directory) "MKD <SP> <pathname> <CRLF>" ---------------------------
static void cmd_MKD(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_553);
return;
}
int ret = mkdir(path, ~client->umask & DIR_PERM);
if (ret < 0) {
debug_retval(ret);
send_ctrl_msg(client, RC_550);
} else {
char quoted_path[strlen(path) * 2 + 1];
if (gen_quoted_path(quoted_path, sizeof(quoted_path), path) < 0) {
send_ctrl_msg(client, RC_550);
return;
}
sendf_ctrl_msg(client, "257 \"%s\" created." CRLF, quoted_path);
}
}
// MLSD (Machine-readable list directory) "MLSD [<SP> <pathname>] CRLF" --------
static void cmd_MLSD(struct client_info *client) {
struct dirent *dp;
DIR *dfd;
char dir_path[PATH_MAX];
char *dir_p;
if (client->cmd_args) {
if (gen_ftp_path(dir_path, sizeof(dir_path), client, client->cmd_args) ||
!ftp_file_exists(dir_path)) {
send_ctrl_msg(client, RC_550);
return;
}
dir_p = dir_path;
} else {
dir_p = client->cur_path;
}
if ((dfd = opendir(dir_p)) == NULL) {
send_ctrl_msg(client, "501 Argument is not a directory." CRLF);
return;
}
// Open data connection.
send_ctrl_msg(client, RC_150);
if (open_data_connection(client) < 0) {
send_ctrl_msg(client, RC_425);
closedir(dfd);
return;
}
// Send directory items.
char path[PATH_MAX + 1]; // +1 because dir can be '/'.
while ((dp = readdir(dfd)) != NULL) {
if (snprintf(path, sizeof(path), "%s/%s", dir_p, dp->d_name) > PATH_MAX ||
send_facts(client, path, dp->d_name, 1)) {
send_ctrl_msg(client, RC_451);
closedir(dfd);
close_data_connection(client);
return;
}
}
send_ctrl_msg(client, RC_226);
closedir(dfd);
close_data_connection(client);
}
// MLST (Machine-readable list) "MLST [<SP> <pathname>] CRLF" ------------------
static void cmd_MLST(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args) ||
!ftp_file_exists(path)) {
send_ctrl_msg(client, RC_550);
return;
}
sendf_ctrl_msg(client, "250-Listing %s" CRLF,
client->cmd_args ? client->cmd_args : client->cur_path);
if (send_facts(client, path,
client->cmd_args ? client->cmd_args : client->cur_path, 0))
send_ctrl_msg(client, RC_451);
else
send_ctrl_msg(client, RC_250);
}
// MODE (Transfer mode) "MODE <SP> <structure-code> <CRLF>" --------------------
static void cmd_MODE(struct client_info *client) {
if (client->cmd_args[0] == 'S' || client->cmd_args[0] == 's')
send_ctrl_msg(client, RC_200);
else
send_ctrl_msg(client, RC_504);
}
// NLST (Name list) "NLST [<SP> <pathname>] <CRLF>" ----------------------------
#ifdef PS4
#define str_swap(a, b) \
{ \
char *temp = a; \
a = b; \
b = temp; \
}
void dumbsort(char **arr, ssize_t n) {
for (ssize_t i = 0; i < n - 1; i++) {
ssize_t lowest = i;
for (ssize_t j = i; j < n; j++)
if (strcasecmp(arr[j], arr[lowest]) < 0)
lowest = j;
str_swap(arr[i], arr[lowest]);
}
}
#else
static int qsort_strcasecmp(const void *a, const void *b) {
return strcasecmp(*(const char **)a, *(const char **)b);
}
#endif
static void cmd_NLST(struct client_info *client) {
struct dirent *dp;
DIR *dfd;
char dir_path[PATH_MAX];
char *dir_p;
if (client->cmd_args) {
if (gen_ftp_path(dir_path, sizeof(dir_path), client, client->cmd_args) ||
!ftp_file_exists(dir_path)) {
send_ctrl_msg(client, RC_550);
return;
}
dir_p = dir_path;
} else {
dir_p = client->cur_path;
}
if ((dfd = opendir(dir_p)) == NULL) {
send_ctrl_msg(client, "501 Argument is not a directory." CRLF);
return;
}
// Open data connection.
send_ctrl_msg(client, RC_150);
if (open_data_connection(client) < 0) {
send_ctrl_msg(client, RC_425);
return;
}
int flist_size = 0;
char **flist = (char **)malloc(0);
if (flist == NULL)
goto out_of_memory;
// Create file list.
while ((dp = readdir(dfd)) != NULL) {
if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0)
continue;
char *file_name = (char *)malloc(strlen(dp->d_name) + 3);
if (file_name == NULL)
goto out_of_memory;
sprintf(file_name, "%s" CRLF, dp->d_name);
flist = (char **)realloc(flist, sizeof(*flist) * ++flist_size);
if (flist == NULL)
goto out_of_memory;
flist[flist_size - 1] = file_name;
}
// Send sorted file list.
#ifdef PS4
dumbsort(flist, flist_size);
#else
qsort(flist, flist_size, sizeof(*flist), qsort_strcasecmp);
#endif
for (int i = 0; i < flist_size; i++)
send_data_msg(client, flist[i]);
send_ctrl_msg(client, RC_226);
close_data_connection(client);
clean_up:
for (int i = 0; i < flist_size; i++)
free(flist[i]);
free(flist);
closedir(dfd);
return;
out_of_memory:
etaHEN_log("Could not allocate memory.");
close_data_connection(client);
send_ctrl_msg(client, RC_451);
goto clean_up;
}
// NOOP (No operation) "NOOP <CRLF>" -------------------------------------------
static void cmd_NOOP(struct client_info *client) {
send_ctrl_msg(client, RC_200);
}
// OPTS "OPTS" <SP> <subcommand> <CRLF> ----------------------------------------
// "MLST" [<SP> factname";"[factname";"...]] <CRLF>
static void cmd_OPTS_MLST(struct client_info *client, char *subcmd_args) {
client->facts = (struct facts){0}; // Disable all facts.
char reply[CMD_LINE_BUF_SIZE];
reply[0] = '\0';
if (subcmd_args == NULL)
goto done;
else
subcmd_args++;
if (strcasestr(subcmd_args, "type;")) {
client->facts.type = 1;
strcat(reply, "type;");
}
if (strcasestr(subcmd_args, "size;")) {
client->facts.size = 1;
strcat(reply, "size;");
}
if (strcasestr(subcmd_args, "unique;")) {
client->facts.unique = 1;
strcat(reply, "unique;");
}
if (strcasestr(subcmd_args, "modify;")) {
client->facts.modify = 1;
strcat(reply, "modify;");
}
if (strcasestr(subcmd_args, "unix.owner;")) {
client->facts.unix_owner = 1;
strcat(reply, "unix.owner;");
}
if (strcasestr(subcmd_args, "unix.group;")) {
client->facts.unix_group = 1;
strcat(reply, "unix.group;");
}
if (strcasestr(subcmd_args, "unix.mode;")) {
client->facts.unix_mode = 1;
strcat(reply, "unix.mode;");
}
done:
sendf_ctrl_msg(client, "200 MLST OPTS %s" CRLF, reply);
}
static void cmd_OPTS(struct client_info *client) {
if (client->cmd_args == NULL) {
send_ctrl_msg(client, RC_501);
return;
}
if (strstr(client->cmd_args, "MLST") == client->cmd_args)
cmd_OPTS_MLST(client, strchr(client->cmd_args, ' '));
else
send_ctrl_msg(client, RC_504);
}
// PASS (Password) "PASS <SP> <password> <CRLF>" -------------------------------
static void cmd_PASS(struct client_info *client) {
send_ctrl_msg(client, RC_202);
}
// PASV (Passive) "PASV <CRLF>" ------------------------------------------------
static void cmd_PASV(struct client_info *client) {
close_data_connection(client); // Drop any connections already made.
if ((client->data_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
send_ctrl_msg(client, RC_451);
return;
}
int ret;
socklen_t socklen = sizeof(struct sockaddr_in);
// To ensure the PASV IP is reachable, use the server IP the client uses for
// its control connection.
if ((ret = getsockname(client->ctrl_sockfd,
(struct sockaddr *)&client->data_sockaddr, &socklen)) <
0)
goto error;
client->data_sockaddr.sin_port = htons(0); // But we need a different port.
if ((ret =
bind(client->data_sockfd, (struct sockaddr *)&client->data_sockaddr,
sizeof(client->data_sockaddr))) < 0)
goto error;
if ((ret = listen(client->data_sockfd, 128)) < 0)
goto error;
struct sockaddr_in pasv_addr;
if ((ret = getsockname(client->data_sockfd, (struct sockaddr *)&pasv_addr,
&socklen)) < 0)
goto error;
client->data_con_type = FTP_DATA_CONNECTION_PASSIVE;
char reply[CMD_LINE_BUF_SIZE];
snprintf(reply, sizeof(reply),
"227 Entering Passive Mode (%hhu,%hhu,%hhu,%hhu,%hhu,%hhu)." CRLF,
(pasv_addr.sin_addr.s_addr >> 0) & 0xFF,
(pasv_addr.sin_addr.s_addr >> 8) & 0xFF,
(pasv_addr.sin_addr.s_addr >> 16) & 0xFF,
(pasv_addr.sin_addr.s_addr >> 24) & 0xFF,
(pasv_addr.sin_port >> 0) & 0xFF, (pasv_addr.sin_port >> 8) & 0xFF);
send_ctrl_msg(client, reply);
return;
error:
debug_retval(ret);
SOCKETCLOSE(client->data_sockfd);
send_ctrl_msg(client, RC_451);
}
// PORT (Data port) "SP> <host-port> <CRLF>" -----------------------------------
static void cmd_PORT(struct client_info *client) {
unsigned char data_ip[4];
unsigned char porthi = 0;
unsigned char portlo = 0;
unsigned short data_port;
char ip_str[16];
struct in_addr data_addr;
int n;
n = sscanf(client->cmd_args, "%hhu,%hhu,%hhu,%hhu,%hhu,%hhu", &data_ip[0],
&data_ip[1], &data_ip[2], &data_ip[3], &porthi, &portlo);
if (n != 6) {
send_ctrl_msg(client, RC_501);
return;
}
data_port = portlo + porthi * 256;
sprintf(ip_str, "%d.%d.%d.%d", data_ip[0], data_ip[1], data_ip[2],
data_ip[3]);
inet_pton(AF_INET, ip_str, &data_addr);
client->data_sockfd = socket(AF_INET, SOCK_STREAM, 0);
#ifdef PS4
client->data_sockaddr.sin_len = sizeof(client->data_sockaddr);
#endif
client->data_sockaddr.sin_family = AF_INET;
client->data_sockaddr.sin_addr = data_addr;
client->data_sockaddr.sin_port = htons(data_port);
client->data_con_type = FTP_DATA_CONNECTION_ACTIVE;
send_ctrl_msg(client, RC_200);
}
// PWD (Print working directory) "PWD <CRLF>" ----------------------------------
static void cmd_PWD(struct client_info *client) {
char path[strlen(client->cur_path) * 2 + 1];
if (gen_quoted_path(path, sizeof(path), client->cur_path) < 0)
send_ctrl_msg(client, RC_550);
else
sendf_ctrl_msg(client, "257 \"%s\" is the current directory." CRLF, path);
}
// QUIT (Close connection) "QUIT <CRLF>" ---------------------------------------
static void cmd_QUIT(struct client_info *client) {
send_ctrl_msg(client, RC_221);
client_thread_exit(client);
}
// REST (Restart) "REST <SP> <marker> <CRLF> -----------------------------------
static void cmd_REST(struct client_info *client) {
long long marker;
if (sscanf(client->cmd_args, "%lld", &marker) != 1) {
send_ctrl_msg(client, RC_501);
return;
}
client->restore_point = marker;
sendf_ctrl_msg(client, "350 Resuming at %lld." CRLF, client->restore_point);
}
// RETR (Retreive) "RETR <SP> <pathname> <CRLF>" -------------------------------
// Sends a local file to a client.
static void send_file(struct client_info *client, const char *path) {
int fd, ret;
// Open local file.
if ((fd = open(path, O_RDONLY, 0)) < 0) {
debug_retval(fd);
send_ctrl_msg(client, RC_550);
return;
}
// Skip to a previous REST command's offset.
if (client->restore_point > 0 &&
(ret = lseek(fd, client->restore_point, SEEK_SET)) < 0) {
debug_retval(ret);
send_ctrl_msg(client, RC_550);
close(fd);
return;
}
// Open data connection.
send_ctrl_msg(client, RC_150);
if (open_data_connection(client) < 0) {
send_ctrl_msg(client, RC_425);
close(fd);
return;
}
// Send the file.
int sockfd;
if (client->data_con_type == FTP_DATA_CONNECTION_ACTIVE)
sockfd = client->data_sockfd;
else
sockfd = client->pasv_sockfd;
unsigned char buffer[FILE_BUF_SIZE];
int n_read;
while ((n_read = read(fd, buffer, FILE_BUF_SIZE)) > 0) {
if (n_read != send(sockfd, buffer, n_read, MSG_NOSIGNAL)) {
close(fd);
close_data_connection(client);
send_ctrl_msg(client, RC_426);
return;
}
}
close(fd);
close_data_connection(client);
if (n_read < 0) {
debug_retval(n_read);
send_ctrl_msg(client, RC_451);
return;
}
send_ctrl_msg(client, RC_226);
}
static void cmd_RETR(struct client_info *client) {
char path[PATH_MAX];
char temp_path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
#if 0
if (client->binary_flag)
send_file(client, path);
else
send_text_file(client, path);
#else
if (is_self(path)) {
if ( decrypt_temp(client, path, temp_path, sizeof(temp_path))) {
send_ctrl_msg(client, RC_451);
return;
}
send_file(client, temp_path);
debug_func(unlink(temp_path));
}
else if (strstr(path, ".ps.bundle") != NULL || strstr(path, ".jsbundle") != NULL) {
if ( decrypt_rnps(client, path, temp_path, sizeof(temp_path))) {
send_ctrl_msg(client, RC_451);
return;
}
send_file(client, temp_path);
debug_func(unlink(temp_path));
}
else{
// for now lets treat everything as a binary unless a user has issues with text files later on
send_file(client, path);
}
#endif
}
// RMD (Remove directory) "RMD <SP> <pathname> <CRLF>" -------------------------
static void delete_dir(struct client_info *client, const char *path) {
if (rmdir(path) >= 0)
send_ctrl_msg(client, RC_250);
else if (errno == 66) // ENOTEMPTY
send_ctrl_msg(client, "550 Directory not empty." CRLF);
else
send_ctrl_msg(client, RC_550);
}
static void cmd_RMD(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
delete_dir(client, path);
}
// RNFR (Rename from) "RNFR <SP> <pathname> <CRLF>" ----------------------------
static void cmd_RNFR(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
if (!ftp_file_exists(path)) {
send_ctrl_msg(client, RC_550);
return;
}
strcpy(client->rename_path, path);
send_ctrl_msg(client, RC_350);
}
// RNTO (Rename to) "RNTO <SP> <pathname> <CRLF>" ------------------------------
static void cmd_RNTO(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_553);
return;
}
int ret;
if ((ret = rename(client->rename_path, path)) < 0) {
debug_retval(ret);
send_ctrl_msg(client, RC_550);
} else {
send_ctrl_msg(client, RC_250);
}
}
// SIZE (Size of file) "SIZE <SP> <pathname> <CRLF>" ---------------------------
static void cmd_SIZE(struct client_info *client) {
struct stat s;
char path[PATH_MAX];
char temp_path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_550);
return;
}
int ret;
if ((ret = stat(path, &s)) < 0) {
debug_retval(ret);
send_ctrl_msg(client, RC_550);
return;
}
// If file is a SELF, decrypt it to retrieve the correct file size.
if (is_2xx && is_self(path)) {
if (decrypt_temp(client, path, temp_path, sizeof(temp_path))) {
send_ctrl_msg(client, RC_451);
return;
}
debug_func(stat(temp_path, &s));
debug_func(unlink(temp_path));
}
else if (strstr(path, ".ps.bundle") != NULL || strstr(path, ".jsbundle") != NULL) {
if ( decrypt_rnps(client, path, temp_path, sizeof(temp_path))) {
send_ctrl_msg(client, RC_451);
return;
}
send_file(client, temp_path);
debug_func(unlink(temp_path));
}
sendf_ctrl_msg(client, "213 %ld" CRLF, s.st_size);
}
// STRU (File structure) "STRU <SP> <structure-code> <CRLF>" -------------------
static void cmd_STRU(struct client_info *client) {
if (client->cmd_args[0] == 'F' || client->cmd_args[0] == 'f')
send_ctrl_msg(client, RC_200);
else
send_ctrl_msg(client, RC_504);
}
// STOR (Store) "STOR <SP> <pathname> <CRLF>" ----------------------------------
static void cmd_STOR(struct client_info *client) {
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, client->cmd_args)) {
send_ctrl_msg(client, RC_553);
return;
}
recv_file(client, path);
}
// SYST (System) "SYST <CRLF>" -------------------------------------------------
static void cmd_SYST(struct client_info *client) {
send_ctrl_msg(client, "215 UNIX Type: L8" CRLF);
}
// TYPE (Representation type) "TYPE <SP> <type-code> <CRLF>" -------------------
static void cmd_TYPE(struct client_info *client) {
switch (strlen(client->cmd_args)) {
case 1:
switch (client->cmd_args[0]) {
case 'a':
case 'A':
client->binary_flag = 0;
break;
case 'i':
case 'I':
client->binary_flag = 1;
break;
default:
send_ctrl_msg(client, RC_504);
return;
}
break;
case 3:
if (strcasecmp(client->cmd_args, "a n") == 0) {
client->binary_flag = 0;
} else if (strcasecmp(client->cmd_args, "l 8") == 0) {
client->binary_flag = 1;
} else {
send_ctrl_msg(client, RC_504);
return;
}
break;
default:
send_ctrl_msg(client, RC_504);
return;
}
send_ctrl_msg(client, RC_200);
}
// USER (User name) "USER <SP> <username> <CRLF>" ------------------------------
static void cmd_USER(struct client_info *client) {
send_ctrl_msg(client, RC_230);
}
// Custom SITE commands (not part of the FTP standard, but commonly used) ------
// Converts a string into a file mode, returning -1 on error.
// Used in FTP commands "SITE CHMOD" and "SITE UMASK".
static long string_to_mode(char *str) {
if (str == NULL || strlen(str) > 4)
return -1;
char modebuf[6] = "0";
strncat(modebuf, str, 4);
long mode = strtol(modebuf, NULL, 8);
if (mode < 0 || mode > 07777)
return -1;
return mode;
}
// "SITE CHMOD <SP> <mode> <SP> <filename> <CRLF>"
static void cmd_SITE_CHMOD(struct client_info *client, char *args) {
if (!args) {
send_ctrl_msg(client, RC_501);
return;
}
char *mode_string = args;
char *filename = strchr(args, ' ');
if (!filename) {
send_ctrl_msg(client, RC_501);
return;
}
*filename++ = '\0';
// Check if argument is a valid mode number.
long mode = string_to_mode(mode_string);
if (mode == -1) {
send_ctrl_msg(client, RC_501);
return;
}
// Set mode.
char path[PATH_MAX];
if (gen_ftp_path(path, sizeof(path), client, filename)) {
send_ctrl_msg(client, RC_550);
return;
}
if (chmod(path, mode) != 0)
send_ctrl_msg(client, RC_451);
else
send_ctrl_msg(client, RC_250);
}
// "SITE UMASK <SP> <mask> <CRLF>"
static void cmd_SITE_UMASK(struct client_info *client, char *args) {
long mode;
if (!args || (mode = string_to_mode(args)) == -1) {
send_ctrl_msg(client, RC_501);
return;
}
client->umask = mode;
send_ctrl_msg(client, RC_200);
}
// Launches SITE commands ("SITE COMMAND [<SP> <parameters>]").
static void cmd_SITE(struct client_info *client) {
char *command = client->cmd_args;
char *args = strchr(client->cmd_args, ' ');
if (args)
*args++ = '\0';
if (strcasecmp(command, "CHMOD") == 0)
cmd_SITE_CHMOD(client, args);
else if (strcasecmp(command, "UMASK") == 0)
cmd_SITE_UMASK(client, args);
else
send_ctrl_msg(client, RC_504);
}
// Custom FTP commands (not part of the FTP standard) --------------------------
// Obsolete, kept for compatibility with older scripts.
static void cmd_KILL(struct client_info *client) {
send_ctrl_msg(client, RC_202);
}
// Causes the FTP server to exit.
static void cmd_SHUTDOWN(struct client_info *client) {
send_ctrl_msg(client, "200 Shutting down..." CRLF);
run = 0;
}
#ifndef PS4
// Dummy command for read-only mode, used in place of commands that would write.
static void cmd_BLOCKED(struct client_info *client) {
send_ctrl_msg(client, RC_502);
}
#endif
// -----------------------------------------------------------------------------
// Creates a list of commands that are available to FTP clients.
static int create_command_list(void) {
struct ftp_command command_list[] = {
// Standard FTP commands:
{"APPE", cmd_APPE, ARGS_REQUIRED},
{"CDUP", cmd_CDUP, ARGS_NONE},
{"CWD", cmd_CWD, ARGS_REQUIRED},
{"DELE", cmd_DELE, ARGS_REQUIRED},
{"FEAT", cmd_FEAT, ARGS_NONE},
{"LIST", cmd_LIST, ARGS_OPTIONAL},
{"MDTM", cmd_MDTM, ARGS_REQUIRED},
{"MLSD", cmd_MLSD, ARGS_OPTIONAL},
{"MLST", cmd_MLST, ARGS_OPTIONAL},
{"MKD", cmd_MKD, ARGS_REQUIRED},
{"MODE", cmd_MODE, ARGS_REQUIRED},
{"NLST", cmd_NLST, ARGS_OPTIONAL},
{"NOOP", cmd_NOOP, ARGS_NONE},
{"OPTS", cmd_OPTS, ARGS_REQUIRED},
{"PASS", cmd_PASS, ARGS_REQUIRED},
{"PASV", cmd_PASV, ARGS_NONE},
{"PORT", cmd_PORT, ARGS_REQUIRED},
{"PWD", cmd_PWD, ARGS_NONE},
{"QUIT", cmd_QUIT, ARGS_NONE},
{"REST", cmd_REST, ARGS_REQUIRED},
{"RETR", cmd_RETR, ARGS_REQUIRED},
{"RMD", cmd_RMD, ARGS_REQUIRED},
{"RNFR", cmd_RNFR, ARGS_REQUIRED},
{"RNTO", cmd_RNTO, ARGS_REQUIRED},
{"SIZE", cmd_SIZE, ARGS_REQUIRED},
{"STOR", cmd_STOR, ARGS_REQUIRED},
{"STRU", cmd_STRU, ARGS_REQUIRED},
{"SYST", cmd_SYST, ARGS_NONE},
{"TYPE", cmd_TYPE, ARGS_REQUIRED},
{"USER", cmd_USER, ARGS_REQUIRED},
// Custom FTP commands:
{"KILL", cmd_KILL, ARGS_NONE},
{"SHUTDOWN", cmd_SHUTDOWN, ARGS_NONE},
{"SITE", cmd_SITE, ARGS_REQUIRED},
{NULL, NULL, ARGS_NONE} // Marks the end of the array.
};
size_t list_size = sizeof(command_list);
ftp_commands = (struct ftp_command *)malloc(list_size);
if (ftp_commands == NULL) {
etaHEN_log("Could not allocate memory.");
return -1;
}
memcpy(ftp_commands, command_list, list_size);
return 0;
}
// Runs the function associated with a client's received FTP command line.
static void run_cmd(struct client_info *client) {
struct ftp_command *cmd = ftp_commands;
while (cmd->name) {
if (strcasecmp(client->cmd_line, cmd->name) == 0) {
if (client->cmd_args) {
if (cmd->args_flag == ARGS_NONE) {
send_ctrl_msg(client, RC_501);
return;
}
} else {
if (cmd->args_flag == ARGS_REQUIRED) {
send_ctrl_msg(client, RC_501);
return;
}
}
cmd->function(client);
// Restore points are meant to be used only right after REST.
if (cmd->function != cmd_REST)
client->restore_point = 0;
return;
}
cmd++;
}
send_ctrl_msg(client, RC_502);
}
/// Threads --------------------------------------------------------------------
// Adds a client thread to the thread list.
static void client_list_add(struct client_info *client) {
debug_func(pthread_mutex_lock(&client_list_mtx));
if (client_list == NULL) { // List is empty.
client_list = client;
client->prev = NULL;
client->next = NULL;
} else {
client->next = client_list;
client->next->prev = client;
client->prev = NULL;
client_list = client;
}
debug_func(pthread_mutex_unlock(&client_list_mtx));
}
// Deletes a client from the client list.
static void client_list_delete(struct client_info *client) {
debug_func(pthread_mutex_lock(&client_list_mtx));
if (client->prev)
client->prev->next = client->next;
if (client->next)
client->next->prev = client->prev;
if (client == client_list)
client_list = client->next;
debug_func(pthread_mutex_unlock(&client_list_mtx));
}
// Causes all client threads to exit.
// The server thread should have exited before calling this function.
static void client_list_terminate(void) {
// Loop until the client list is empty.
while (1) {
debug_func(pthread_mutex_lock(&client_list_mtx));
struct client_info *client = client_list;
if (client == NULL) {
debug_func(pthread_mutex_unlock(&client_list_mtx));
break;
}
// Cause all remaining client threads to try to exit.
do {
client_thread_exit(client);
client = client->next;
} while (client);
// The exiting threads need access to the list to be able to exit.
debug_func(pthread_mutex_unlock(&client_list_mtx));
usleep(1000);
}
debug_func(pthread_mutex_destroy(&client_list_mtx));
}
static void set_socket_timeout(int socket, int seconds) {
struct timeval t = {.tv_sec = seconds};
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(t));
}
// This function is used as a separate thread for each new client.
static void *client_thread(void *arg) {
debug_func(pthread_detach(pthread_self()));
struct client_info *client = (struct client_info *)arg;
etaHEN_log("Client %s connects to socket %d.", client->ipv4,
client->ctrl_sockfd);
send_ctrl_msg(client, "220 FTP server 1.0A for PS5 by LM." CRLF);
// Disconnect after a period of inactivity.
set_socket_timeout(client->ctrl_sockfd, 300);
while (1) {
// Receive a command line string from the client.
ssize_t n = recv(client->ctrl_sockfd, client->cmd_line,
sizeof(client->cmd_line) - 1, 0);
if (n > 0) {
// Remove the string's "end-of-line" (CRLF aka \r).
client->cmd_line[n] = '\0';
char *cmd_end = strrchr(client->cmd_line, '\n');
if (cmd_end) {
*cmd_end = '\0';
if (*--cmd_end == '\r') // Some FTP clients only use ''.
*cmd_end = '\0';
} else {
if (n == sizeof(client->cmd_line) - 1) {
etaHEN_log("Received command line too long.");
} else {
etaHEN_log("Received command line not terminated.");
}
send_ctrl_msg(client, RC_500);
break; // Kick client for sending garbage.
}
// Isolate arguments.
if ((client->cmd_args = strchr(client->cmd_line, ' '))) {
*client->cmd_args = '\0';
client->cmd_args++;
if (*client->cmd_args == '\0')
client->cmd_args = NULL; // Treat empty-arg as no-arg.
}
run_cmd(client);
} else if (n == 0) { // FTP client disconnected (PS4 and non-PS4), or
break; // fini() has been called (non-PS4).
} else if (n < 0) {
#ifdef PS4
if (n != (int)SCE_NET_ERROR_EINTR) { // Would happen on fini().
etaHEN_log("Error %d in client %s@%d's thread.", n, client->ipv4,
client->ctrl_sockfd);
}
#else
etaHEN_log("Error in client %s@%d's thread: %s.", client->ipv4,
client->ctrl_sockfd, strerror(errno));
#endif
break;
}
}
etaHEN_log("Client %s@%d disconnects.", client->ipv4, client->ctrl_sockfd);
// Clean up.
client_list_delete(client);
debug_func(SOCKETCLOSE(client->ctrl_sockfd));
close_data_connection(client);
etaHEN_log("Client %s@%d's thread exits.", client->ipv4,
client->ctrl_sockfd);
free(client);
return NULL;
}
int sceNetSetsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
typedef uint8_t SceNetSaFamily_t;
typedef struct SceNetSockaddr {
uint8_t sa_len;
SceNetSaFamily_t sa_family;
char sa_data[14];
} SceNetSockaddr;
int sceNetBind(
int s,
const SceNetSockaddr *addr,
uint32_t addrlen
);
// Listens for new clients and starts new client threads.
static void *server_thread(void *arg) {
// Set up the FTP server's default directory.
const char *default_directory = (char *)arg;
if (default_directory == NULL)
default_directory = "/";
// Create the FTP command list.
if (create_command_list()) {
run = 0;
return NULL;
}
// Create server socket.
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(!server_sockfd) {
notify(true, "FTP failed to start!\nError %s", strerror(errno));
etaHEN_log("FTP failed to start! Error %s", strerror(errno));
run = 0; // On error, trigger server shutdown.
return NULL;
}
int option_value = 1;
int ret = sceNetSetsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &option_value,
sizeof(option_value));
etaHEN_log("sceNetSetsockopt: %d", ret);
sceNetSetsockopt(server_sockfd, SOL_SOCKET, SO_REUSEPORT, &option_value,
sizeof(option_value));
// Fill in the server's IPv4 socket address.
struct sockaddr_in serveraddr;
#ifdef PS4
serveraddr.sin_len = sizeof(serveraddr); // This member only exists on PS4.
#endif
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(server_port);
// Bind the server's address to the socket.
if (sceNetBind(server_sockfd, (const SceNetSockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
// printf_notification("Port %u already in use", server_port);
notify(true, "FTP failed to bind to the required port!\nError %s", strerror(errno));
etaHEN_log("Port %u already in use", server_port);
run = 0; // On error, trigger server shutdown.
return NULL;
}
// Start listening.
if (listen(server_sockfd, 128) < 0) {
notify(true, "FTP failed to start!\n(listen) Error %s", strerror(errno));
etaHEN_log("FTP failed to start! Error %s", strerror(errno));
run = 0; // On error, trigger server shutdown.
return NULL;
}
ftp_started = true;
// Accept clients.
while (1) {
struct sockaddr_in clientaddr;
int client_sockfd;
socklen_t addrlen = sizeof(clientaddr);
client_sockfd =
accept(server_sockfd, (struct sockaddr *)&clientaddr, &addrlen);
if (client_sockfd < 0) {
#ifdef PS4
if (client_sockfd != -2143223548 /* SHUTDOWN command on PS4 */) {
#else
if (errno != 22 /* SHUTDOWN command on non-PS4 */) {
#endif
debug_retval(client_sockfd);
}
break;
} else {
// Allocate the new client struct (all values are set to 0).
struct client_info *client =
(struct client_info *)calloc(sizeof(*client), 1);
if (client == NULL) {
etaHEN_log("Could not allocate memory.");
debug_func(SOCKETCLOSE(client_sockfd));
continue;
}
// Set up the new client.
client->ctrl_sockfd = client_sockfd;
client->data_con_type = FTP_DATA_CONNECTION_NONE;
strncpy(client->cur_path, default_directory, sizeof(client->cur_path));
memcpy(&client->ctrl_sockaddr, &clientaddr,
sizeof(client->ctrl_sockaddr));
client->facts.modify = 1;
client->facts.size = 1;
client->facts.type = 1;
client->facts.unique = 0;
client->facts.unix_group = 1;
client->facts.unix_mode = 1;
client->facts.unix_owner = 1;
// Add the client to the client list.
client_list_add(client);
// Create a new thread for the client.
if (pthread_create(&client->thid, NULL, client_thread, client)) {
etaHEN_log("Could not create a client thread.");
free(client);
debug_func(SOCKETCLOSE(client_sockfd));
}
}
}
debug_func(SOCKETCLOSE(server_sockfd));
etaHEN_log("Server thread exits.");
ftp_started = false;
return NULL;
}
// Initializes the program and starts the server thread.
int init(const char *ip, unsigned short port, const char *default_directory) {
int ret;
// Store server port and server IPv4 address globally.
server_port = port;
if (inet_pton(AF_INET, ip, &server_ip) == 0) {
etaHEN_log("Invalid IPv4 address: \"%s\"", ip);
return -1;
}
// Create client list mutex.
ret = pthread_mutex_init(&client_list_mtx, NULL);
if (ret) {
etaHEN_log("Could not create the client list mutex (error %d).", ret);
return -1;
}
// Create server thread.
ret = pthread_create(&server_thid, NULL, server_thread,
(void *)default_directory);
if (ret) {
etaHEN_log("Could not create the server thread (error %d).", ret);
pthread_mutex_destroy(&client_list_mtx);
return -1;
}
return 0;
}
// Cleans up and exits the program.
void fini(void) {
// Exit server thread.
// Make the server thread's blocking accept() fail before attempting
// to close the server socket. Simply closing a socket does not
// unblock on PS4 and some other systems.
debug_func(sceNetSocketAbort(server_sockfd, 0));
etaHEN_log("Waiting for server thread to exit...");
debug_func(pthread_join(server_thid, NULL));
// Exit client threads.
etaHEN_log("Waiting for client threads to exit...");
client_list_terminate();
free(ftp_commands);
}
bool StartFTP(void) {
OrbisKernelSwVersion sw;
sceKernelGetProsperoSystemSwVersion(&sw);
is_2xx = (sw.version < 0x3000000);
srand(time(NULL) ^ getpid());
ftp_started = false;
if (get_ip_address(&ip_address[0]) < 0){
etaHEN_log("[FTP Module] Failed to get IP address");
notify(true, "FTP failed to start!\nFailed to get IP address");
return false;
}
return (ftp_started = init(ip_address, 1337, "/") == 0);
}
void ShutdownFTP(void) {
if(!ftp_started){
etaHEN_log("[FTP Module] FTP server not started");
return;
}
fini();
ftp_started = false;
}
void check_ftp_addr_change(void) {
char func_ip_address[16];
if (get_ip_address(&func_ip_address[0]) < 0)
return;
if (strcmp(&ip_address[0], &func_ip_address[0]) != 0) {
etaHEN_log("[FTP Module] IP Address changed, restarting FTP server");
ShutdownFTP();
StartFTP();
}
}
#pragma clang diagnostic pop
// NOLINTEND(*)