Files
digFTP/src/plugins/filer_local/filer_local.cpp

443 lines
14 KiB
C++

#include "plugin.h"
#include "filer.h"
#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#include <system_error>
#include <sys/stat.h>
#include <time.h>
namespace fs = std::filesystem;
class LocalFiler : public IPlugin, public Filer {
public:
LocalFiler() {}
bool initialize(const std::map<std::string, std::string>& config) {
auto symreslv_it = config.find("resolve_external_symlinks");
this->resolve_ext_symlinks = (symreslv_it != config.end()) ?
(symreslv_it->second == "on" || symreslv_it->second == "true" || symreslv_it->second == "yes") :
false;
return true;
}
file_data setRoot(std::string _root) {
struct file_data fd;
try {
// Always convert to absolute path
fs::path new_root = fs::absolute(fs::weakly_canonical(_root));
fd.path = strdup(new_root.c_str());
// Create directory if it doesn't exist
if (!fs::exists(new_root)) {
if (!fs::create_directories(new_root)) {
fd.error = {FilerStatusCodes::NoPermission, "Failed to create root directory"};
return fd;
}
}
// Set the absolute root path
root = new_root;
// Reset CWD to root-relative path
cwd = "/";
} catch (const fs::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, "setRoot error: {}", ex.what());
fd.error = {FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data setCWD(std::string _cwd) {
struct file_data fd;
try {
// Always convert to absolute path
fs::path new_cwd = fs::weakly_canonical(_cwd);
fd.path = strdup(new_cwd.c_str());
fd.relpath = strdup(fs::relative(new_cwd, root).c_str());
// Create directory if it doesn't exist
if (!fs::exists(root / new_cwd)) {
if (!fs::create_directories(root / new_cwd)) {
fd.error = {FilerStatusCodes::NoPermission, "Failed to create cwd directory"};
return fd;
}
}
cwd = new_cwd;
} catch (const fs::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, "setCWD error: {}", ex.what());
fd.error = {FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
fs::path getRoot() {
return this->root;
}
fs::path getCWD() {
return this->cwd;
}
bool resolvePath(const std::string& path, fs::path* target) {
return this->resolvePath(path, target, true);
}
bool resolvePath(const std::string& path, fs::path* target, bool resolve_symlink) {
try {
if (path.empty() || path == ".") {
*target = root / cwd.relative_path();
return true;
}
if (path[0] == '/') {
// Absolute path relative to FTP root
*target = root / path.substr(1);
} else {
// Relative to current directory
*target = root / cwd.relative_path() / path;
}
*target = fs::path(*target).lexically_normal();
if (fs::is_symlink(*target) && resolve_symlink) {
fs::path resolved_sym = fs::weakly_canonical(*target);
logger->print(LOGLEVEL_DEBUG, "resolved symlink path: {}", resolved_sym.string());
return resolve_ext_symlinks ? true : resolved_sym.string().starts_with(root.string());
} else if (!(*target).string().starts_with(root.string()))
return false;
logger->print(LOGLEVEL_DEBUG, "resolved path: {}", target->string());
return true;
} catch (const std::exception& e) {
logger->print(LOGLEVEL_ERROR, "path resolution error: {}", e.what());
return false;
}
}
file_data traverse(std::string dir) {
struct file_data fd;
try {
fs::path requested_path;
if (!resolvePath(dir, &requested_path, false)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
if (dir.empty() || dir == ".") {
fd.path = strdup(requested_path.c_str());
fd.relpath = strdup(fs::relative(requested_path, root).c_str());
return fd;
}
// Verify the requested path exists and is within root
if (!fs::exists(requested_path)) {
fd.error = file_error{FilerStatusCodes::NotFound, "Directory Not Found"};
return fd;
}
if (!fs::is_directory(requested_path)) {
fd.error = file_error{FilerStatusCodes::NotFound, "Not a Directory"};
return fd;
}
// Update current working directory relative to root
cwd = "/" + fs::path(requested_path).lexically_relative(root).string();
fd.path = strdup(requested_path.c_str());
fd.relpath = strdup(fs::relative(requested_path, root).c_str());
} catch (const fs::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, "traversal error: {}", ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data createDirectory(std::string dir) {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(dir, &resolved)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
if (fs::exists(resolved)) {
fd.error = file_error{FilerStatusCodes::FileExists, "Directory Already Exists"};
return fd;
}
fs::create_directory(resolved);
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data fileSize(std::string name) {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(name, &resolved)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
if (!fs::exists(resolved)) {
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
return fd;
}
if (type == 'A') {
fd.error = file_error{FilerStatusCodes::InvalidTransferMode, "Refusing to transfer in ASCII mode"};
return fd;
}
std::ifstream infile(resolved, std::ios::in|std::ios::binary|std::ios::ate);
if (infile.is_open()) {
fd.size = infile.tellg();
} else {
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
}
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data deleteFile(std::string name) {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(name, &resolved, false)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
std::error_code err;
if (!fs::remove(resolved, err)) {
if (err == std::errc::no_such_file_or_directory)
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
else if (err == std::errc::permission_denied)
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
else if (err == std::errc::directory_not_empty)
fd.error = file_error{FilerStatusCodes::DirectoryNotEmpty, "Directory not empty"};
else if (err == std::errc::is_a_directory)
fd.error = file_error{FilerStatusCodes::IsDirectory, "Is A Directory"};
else
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to delete file"};
}
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data readFile(std::string name) {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(name, &resolved)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
if (!fs::exists(resolved)) {
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
return fd;
}
if (fs::is_directory(resolved)) {
return list(std::string(fd.relpath));
}
if (type != 'A') {
// Create a shared_ptr to manage the ifstream
fd.stream = std::make_shared<std::ifstream>(resolved, std::ios::in|std::ios::binary);
if (fd.stream && fd.stream->is_open()) {
// Get file size
fd.stream->seekg(0, std::ios::end);
fd.size = fd.stream->tellg();
fd.stream->seekg(0, std::ios::beg);
} else {
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
}
} else {
fd.error = file_error{FilerStatusCodes::InvalidTransferMode, "Refusing to transfer in ASCII mode"};
}
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
file_data writeFile(std::string name, unsigned char* data, int size, bool append = false) {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(name, &resolved)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
std::ios_base::openmode omode = std::ios::out|std::ios::binary;
if (append) omode |= std::ios::app;
std::ofstream outfile(resolved, omode);
if (outfile.is_open()) {
outfile.write((char *)data, size);
outfile.close();
fd.size = size;
} else {
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
}
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
struct file_data renameFile(const std::string& from, const std::string& to) {
struct file_data fd;
fs::path src_path;
if (!resolvePath(from, &src_path)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Source Permissions"};
return fd;
}
fs::path dst_path;
if (!resolvePath(to, &dst_path)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Destination Permissions"};
return fd;
}
try {
// Check if destination already exists
if (std::filesystem::exists(dst_path)) {
fd.error = {FilerStatusCodes::FileExists, "Destination file already exists"};
return fd;
}
// Perform the rename operation
std::filesystem::rename(src_path, dst_path);
fd.path = strdup(dst_path.c_str());
fd.relpath = strdup(fs::relative(dst_path, root).c_str());
fd.error = {0, "OK"};
} catch (const std::filesystem::filesystem_error& e) {
fd.error = {FilerStatusCodes::AccessDenied, e.what()};
}
return fd;
}
file_data list(std::string path = ".") {
struct file_data fd;
try {
fs::path resolved;
if (!resolvePath(path, &resolved)) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
return fd;
}
fd.path = strdup(resolved.c_str());
fd.relpath = strdup(fs::relative(resolved, root).c_str());
if (!fs::exists(resolved)) {
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
return fd;
}
std::ostringstream listStream;
for(const auto& p : fs::directory_iterator(resolved,
fs::directory_options::skip_permission_denied)) {
struct stat fstat;
struct tm *time;
time_t rawtime;
char timebuff[80];
if (lstat(p.path().c_str(), &fstat) == -1) {
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to stat file"};
break;
}
/* Convert time_t to tm struct */
rawtime = fstat.st_mtime;
time = localtime(&rawtime);
strftime(timebuff, 80, "%b %d %H:%M", time);
// God should've smitten me before I wrote such attrocities.
char* line;
fs::perms fperms = fs::symlink_status(p).permissions();
// Handle symlink info along with special cases
bool is_link = false; std::string link_path;
if ((is_link = S_ISLNK(fstat.st_mode))) {
fs::path sym_resolved;
if (resolvePath(fs::read_symlink(p), &sym_resolved))
link_path = sym_resolved.lexically_relative(resolved).string();
else is_link = false;
}
asprintf(
&line,
"%c%c%c%c%c%c%c%c%c%c %4u %4u %4u %12u %s %s%s\r\n",
is_link?'l':(p.is_directory()?'d':'-'),
(fperms & fs::perms::owner_read) != fs::perms::none?'r':'-',
(fperms & fs::perms::owner_write) != fs::perms::none?'w':'-',
(fperms & fs::perms::owner_exec) != fs::perms::none?'x':'-',
(fperms & fs::perms::group_read) != fs::perms::none?'r':'-',
(fperms & fs::perms::group_write) != fs::perms::none?'w':'-',
(fperms & fs::perms::group_exec) != fs::perms::none?'x':'-',
(fperms & fs::perms::others_read) != fs::perms::none?'r':'-',
(fperms & fs::perms::others_write) != fs::perms::none?'w':'-',
(fperms & fs::perms::others_exec) != fs::perms::none?'x':'-',
fstat.st_nlink,
fstat.st_uid,
fstat.st_gid,
fstat.st_size,
timebuff,
p.path().filename().c_str(),
is_link?(" -> " + link_path).c_str():""
);
listStream << std::string(line);
free(line);
}
fd.size = listStream.tellp();
fd.bin = new char[fd.size];
std::strncpy(fd.bin, listStream.str().c_str(), fd.size);
} catch (const std::filesystem::filesystem_error& ex) {
logger->print(LOGLEVEL_ERROR, ex.what());
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
}
return fd;
}
private:
fs::path root;
fs::path cwd;
bool resolve_ext_symlinks = false;
char type = 'I';
};
IMPLEMENT_PLUGIN(LocalFiler, Filer, "local", "Local filesystem implementation", "1.0.0")