diff --git a/src/client.cpp b/src/client.cpp index a56d42b..92794bb 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -156,32 +156,32 @@ public: if (state >= FTP_STATE_AUTHED) { struct file_data fd = filer->traverse(argstr); if (fd.error.code == 0) submit(250, "OK"); - else submit(431, std::string(fd.error.msg)); + else submit(431, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "CDUP") { if (state >= FTP_STATE_AUTHED) { struct file_data fd = filer->traverse(".."); if (fd.error.code == 0) submit(250, "OK"); - else submit(431, std::string(fd.error.msg)); + else submit(431, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "MKD") { if (state >= FTP_STATE_AUTHED) { struct file_data fd = filer->createDirectory(argstr); if (fd.error.code == 0) submit(257, "\""+std::string(fd.path)+"\" directory created"); - else if (fd.error.code == FilerStatusCodes::FileExists) submit(521, std::string(fd.error.msg)); - else submit(550, std::string(fd.error.msg)); + else if (fd.error.code == FilerStatusCodes::FileExists) submit(521, fd.error.msg); + else submit(550, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "SIZE") { if (state >= FTP_STATE_AUTHED) { struct file_data fd = filer->fileSize(argstr); if (fd.error.code == 0) submit(213, std::to_string(fd.size)); - else submit(550, std::string(fd.error.msg)); + else submit(550, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "DELE" || cmd == "RMD") { if (state >= FTP_STATE_AUTHED) { struct file_data fd = filer->deleteFile(argstr); if (fd.error.code == 0) submit(250, "OK"); - else submit(550, std::string(fd.error.msg)); + else submit(550, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "PASV") { if (state >= FTP_STATE_AUTHED) { @@ -270,7 +270,7 @@ public: // Check if source file exists struct file_data fd = filer->fileSize(argstr); if (fd.error.code != 0) { - submit(550, std::string(fd.error.msg)); + submit(550, fd.error.msg); rename_pending = false; return 0; } @@ -298,7 +298,7 @@ public: rename_pending = false; // Reset rename state if (fd.error.code == 0) submit(250, "Rename successful"); - else submit(550, std::string(fd.error.msg)); + else submit(550, fd.error.msg); } else submit(530, "Not logged in"); } else if (cmd == "LIST" || cmd == "NLST") { if (state == FTP_STATE_ONDATA && data_sock > 0) { @@ -341,7 +341,7 @@ public: } } if (transfer_ok) submit(226, "OK"); - } else submit(550, std::string(fd.error.msg)); + } else submit(550, fd.error.msg); data_close(); } else submit(425, "No data connection"); } else if (cmd == "STOR") { @@ -368,7 +368,7 @@ public: fd = filer->writeFile(argstr, inbuf, psize, true); if (fd.error.code != 0) { - submit(550, "Access Denied: "+std::string(fd.error.msg)); + submit(550, "Access Denied: "+fd.error.msg); data_close(); break; } diff --git a/src/filer.h b/src/filer.h index 216def6..9007df2 100644 --- a/src/filer.h +++ b/src/filer.h @@ -7,7 +7,7 @@ struct file_error { uint8_t code = 0; - const char* msg = {}; + std::string msg; }; struct file_data { diff --git a/src/plugins/auth_sql/CMakeLists.txt b/src/plugins/auth_sql/CMakeLists.txt new file mode 100644 index 0000000..640b71d --- /dev/null +++ b/src/plugins/auth_sql/CMakeLists.txt @@ -0,0 +1,4 @@ +target_link_libraries(${plugin} + PRIVATE + ${CMAKE_DL_LIBS} +) \ No newline at end of file diff --git a/src/plugins/auth_sql/auth_sql.cpp b/src/plugins/auth_sql/auth_sql.cpp new file mode 100644 index 0000000..a4eb678 --- /dev/null +++ b/src/plugins/auth_sql/auth_sql.cpp @@ -0,0 +1,375 @@ +#include "plugin.h" +#include "filer.h" +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +class LocalFiler : public IPlugin, public Filer { +public: + LocalFiler() {} + + 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 = new_root.string().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: %s", 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 = new_cwd.string().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: %s", ex.what()); + fd.error = {FilerStatusCodes::Exception, ex.what()}; + } + + return fd; + } + + fs::path getRoot() { + return this->root; + } + + fs::path getCWD() { + return this->cwd; + } + + fs::path resolvePath(const std::string& path) { + try { + if (path.empty() || path == ".") { + return root / cwd.relative_path(); + } + + fs::path target_path; + if (path[0] == '/') { + // Absolute path relative to FTP root + target_path = root / path.substr(1); + } else { + // Relative to current directory + target_path = root / cwd.relative_path() / path; + } + + // Remove .. and . components + target_path = fs::weakly_canonical(target_path); + + // Make sure we haven't escaped the root + if (!target_path.string().starts_with(root.string())) { + return root / cwd.relative_path(); + } + + return target_path; + } catch (const std::exception& e) { + logger->print(LOGLEVEL_ERROR, "Path resolution error: %s", e.what()); + return root / cwd.relative_path(); + } + } + + file_data traverse(std::string dir) { + struct file_data fd; + try { + // Handle special cases + if (dir.empty() || dir == ".") { + fd.path = resolvePath(".").string().c_str(); + return fd; + } + + fs::path requested_path = resolvePath(dir); + + // 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; + } + + // Make sure path is within root directory + fs::path rel_path = fs::relative(requested_path, root); + if (rel_path.string().find("..") == 0) { + fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"}; + return fd; + } + + // Update current working directory relative to root + cwd = "/" + rel_path.string(); + fd.path = requested_path.string().c_str(); + + } catch (const fs::filesystem_error& ex) { + logger->print(LOGLEVEL_ERROR, "Path fallback: %s", 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 = resolvePath(dir); + fd.path = resolved.string().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 = resolvePath(name); + fd.path = resolved.string().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 = resolvePath(name); + fd.path = resolved.string().c_str(); + + if (!fs::exists(resolved)) { + fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"}; + return fd; + } + + if (fs::is_directory(resolved) && !fs::is_empty(resolved)) { + fd.error = file_error{FilerStatusCodes::DirectoryNotEmpty, "Directory not empty"}; + return fd; + } + + if (!fs::remove(resolved)) { + 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 = resolvePath(name); + fd.path = resolved.string().c_str(); + + if (!fs::exists(resolved)) { + fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"}; + return fd; + } + + if (type != 'A') { + // Create a shared_ptr to manage the ifstream + fd.stream = std::make_shared(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 = resolvePath(name); + fd.path = resolved.string().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; + std::filesystem::path src_path = resolvePath(from); + std::filesystem::path dst_path = resolvePath(to); + + 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.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 = resolvePath(path); + fd.path = resolved.string().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::follow_directory_symlink | + fs::directory_options::skip_permission_denied)) { + + struct stat fstat; + struct tm *time; + time_t rawtime; + char timebuff[80]; + + if (stat(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::status(p).permissions(); + asprintf( + &line, + "%c%c%c%c%c%c%c%c%c%c %4u %4u %4u %12u %s %s\r\n", + 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':'-', + fs::hard_link_count(p), + fstat.st_uid, + fstat.st_gid, + fstat.st_size, + timebuff, + p.path().filename().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; + char type = 'I'; +}; + +IMPLEMENT_PLUGIN(LocalFiler, Filer, "local", "Local filesystem implementation", "1.0.0") \ No newline at end of file