diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88cb8cc..21357a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: needs: clang-format steps: - uses: actions/checkout@v4 + with: + submodules: true - name: create version.h run: | git_hash=$(git rev-parse --short "$GITHUB_SHA") diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ec3d98b..60dd664 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,6 +15,8 @@ jobs: needs: clang-format steps: - uses: actions/checkout@v4 + with: + submodules: true - name: create version.h run: | git_hash=$(git rev-parse --short "${{ github.event.pull_request.head.sha }}") diff --git a/.gitignore b/.gitignore index 348bf56..3ff4ba2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.nso *.pfs0 *.smdh +*~ .gdb_history 3ds/build 3ds-classic/build @@ -24,7 +25,7 @@ switch-classic/build switch/romfs/*.zst switch/romfs/shaders/*.dksh .idea/ -build/ +build*/ *.rpx *.wuhb *.wps diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dc160a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3rd/gls"] + path = 3rd/gls + url = git@github.com:microsoft/GSL.git diff --git a/3rd/gls b/3rd/gls new file mode 160000 index 0000000..a353456 --- /dev/null +++ b/3rd/gls @@ -0,0 +1 @@ +Subproject commit a3534567187d2edc428efd3f13466ff75fe5805c diff --git a/Makefile b/Makefile index 910799f..4d9fa62 100644 --- a/Makefile +++ b/Makefile @@ -22,9 +22,9 @@ WUPS_ROOT := $(DEVKITPRO)/wups #------------------------------------------------------------------------------- TARGET := ftpiiu BUILD := build -SOURCES := source source/wiiu +SOURCES := source source/wiiu source/posix DATA := data -INCLUDES := source include +INCLUDES := source include 3rd/gls/include #------------------------------------------------------------------------------- # options for code generation diff --git a/include/fs.h b/include/fs.h index 81c0200..2fbbc5d 100644 --- a/include/fs.h +++ b/include/fs.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,6 +22,8 @@ #include "ioBuffer.h" +#include + #include #include @@ -29,6 +31,7 @@ #include #include #include +#include namespace fs { @@ -69,7 +72,7 @@ public: /// \brief Open file /// \param path_ Path to open /// \param mode_ Access mode (\sa std::fopen) - bool open (char const *path_, char const *mode_ = "rb"); + bool open (gsl::not_null path_, gsl::not_null mode_ = "rb"); /// \brief Close file void close (); @@ -77,13 +80,13 @@ public: /// \brief Seek to file position /// \param pos_ File position /// \param origin_ Reference position (\sa std::fseek) - std::make_signed_t seek (std::size_t pos_, int origin_); + std::make_signed_t seek (std::make_signed_t pos_, int origin_); /// \brief Read data /// \param buffer_ Output buffer /// \param size_ Size to read /// \note Can return partial reads - std::make_signed_t read (void *buffer_, std::size_t size_); + std::make_signed_t read (gsl::not_null buffer_, std::size_t size_); /// \brief Read data /// \param buffer_ Output buffer @@ -97,13 +100,13 @@ public: /// \param buffer_ Output buffer /// \param size_ Size to read /// \note Fails on partial reads and errors - bool readAll (void *buffer_, std::size_t size_); + bool readAll (gsl::not_null buffer_, std::size_t size_); /// \brief Write data /// \param buffer_ Input data /// \param size_ Size to write /// \note Can return partial writes - std::make_signed_t write (void const *buffer_, std::size_t size_); + std::make_signed_t write (gsl::not_null buffer_, std::size_t size_); /// \brief Write data /// \param buffer_ Input data @@ -114,20 +117,17 @@ public: /// \param buffer_ Input data /// \param size_ Size to write /// \note Fails on partials writes and errors - bool writeAll (void const *buffer_, std::size_t size_); + bool writeAll (gsl::not_null buffer_, std::size_t size_); private: /// \brief Underlying std::FILE* std::unique_ptr m_fp{nullptr, nullptr}; /// \brief Buffer - std::unique_ptr m_buffer; - - /// \brief Buffer size - std::size_t m_bufferSize = 0; + std::vector m_buffer; /// \brief Line buffer - char *m_lineBuffer = nullptr; + gsl::owner m_lineBuffer = nullptr; /// \brief Line buffer size std::size_t m_lineBufferSize = 0; @@ -161,14 +161,14 @@ public: /// \brief Open directory /// \param path_ Path to open - bool open (char const *const path_); + bool open (gsl::not_null path_); /// \brief Close directory void close (); /// \brief Read a directory entry /// \note Returns nullptr on end-of-directory or error; check errno - struct dirent *read (); + dirent *read (); private: /// \brief Underlying DIR* diff --git a/include/ftpConfig.h b/include/ftpConfig.h index d3c021d..57fef91 100644 --- a/include/ftpConfig.h +++ b/include/ftpConfig.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,10 +22,13 @@ #include "platform.h" +#include + #include #include #include #include +#include class FtpConfig; using UniqueFtpConfig = std::unique_ptr; @@ -41,15 +44,15 @@ public: /// \brief Load config /// \param path_ Path to config file - static UniqueFtpConfig load (char const *path_); + static UniqueFtpConfig load (gsl::not_null path_); -#ifndef NDS +#ifndef __NDS__ std::scoped_lock lockGuard (); #endif /// \brief Save config /// \param path_ Path to config file - bool save (char const *path_); + bool save (gsl::not_null path_); /// \brief Get user std::string const &user () const; @@ -57,6 +60,9 @@ public: /// \brief Get password std::string const &pass () const; + /// \brief Get hostname + std::string const &hostname () const; + /// \brief Get port std::uint16_t port () const; @@ -79,15 +85,19 @@ public: /// \brief Set user /// \param user_ User - void setUser (std::string const &user_); + void setUser (std::string user_); /// \brief Set password /// \param pass_ Password - void setPass (std::string const &pass_); + void setPass (std::string pass_); + + /// \brief Set hostname + /// \param hostname_ Hostname + void setHostname (std::string hostname_); /// \brief Set listen port /// \param port_ Listen port - bool setPort (std::string const &port_); + bool setPort (std::string_view port_); /// \brief Set listen port /// \param port_ Listen port @@ -106,17 +116,17 @@ public: /// \brief Set access point SSID /// \param ssid_ SSID - void setSSID (std::string const &ssid_); + void setSSID (std::string_view ssid_); /// \brief Set access point passphrase /// \param passphrase_ Passphrase - void setPassphrase (std::string const &passphrase_); + void setPassphrase (std::string_view passphrase_); #endif private: FtpConfig (); -#ifndef NDS +#ifndef __NDS__ /// \brief Mutex mutable platform::Mutex m_lock; #endif @@ -127,6 +137,9 @@ private: /// \brief Password std::string m_pass; + /// \brief Hostname + std::string m_hostname; + /// \brief Listen port std::uint16_t m_port; diff --git a/include/ftpServer.h b/include/ftpServer.h index 2822309..a866d7e 100644 --- a/include/ftpServer.h +++ b/include/ftpServer.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -48,6 +48,9 @@ public: /// \brief Draw server and all of its sessions void draw (); + /// \brief Whether server wants to quit + bool quit (); + /// \brief Create server static UniqueFtpServer create (); @@ -60,6 +63,11 @@ public: /// \brief Server start time static std::time_t startTime (); +#ifdef __3DS__ + /// \brief Get timezone offset in seconds (only used on 3DS) + static int tzOffset (); +#endif + private: /// \brief Paramterized constructor /// \param config_ FTP config @@ -88,7 +96,7 @@ private: /// \brief Thread entry point void threadFunc (); -#ifndef NDS +#ifndef __NDS__ /// \brief Thread platform::Thread m_thread; @@ -102,6 +110,11 @@ private: /// \brief Listen socket UniqueSocket m_socket; +#ifndef __NDS__ + /// \brief mDNS socket + UniqueSocket m_mdnsSocket; +#endif + /// \brief ImGui window name std::string m_name; @@ -109,7 +122,7 @@ private: std::vector m_sessions; /// \brief Whether thread should quit - std::atomic m_quit; + std::atomic_bool m_quit = false; #ifndef CLASSIC /// \brief Log upload cURL context @@ -143,8 +156,11 @@ private: /// \brief Password setting std::string m_passSetting; + /// \brief Hostname setting + std::string m_hostnameSetting; + /// \brief Port setting - std::uint16_t m_portSetting; + std::uint16_t m_portSetting = 0; #ifdef __3DS__ /// \brief getMTime setting @@ -153,7 +169,7 @@ private: #ifdef __SWITCH__ /// \brief Whether an error occurred enabling access point - std::atomic m_apError = false; + std::atomic_bool m_apError = false; /// \brief Enable access point setting bool m_enableAPSetting; diff --git a/include/ftpSession.h b/include/ftpSession.h index 807803c..c4996d2 100644 --- a/include/ftpSession.h +++ b/include/ftpSession.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2023 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -26,8 +26,20 @@ #include "platform.h" #include "socket.h" +#if __has_include() +#include +#define FTPD_HAS_GLOB 1 +#else +#define FTPD_HAS_GLOB 0 +#endif + +#include +using stat_t = struct stat; + #include +#include #include +#include #include #include #include @@ -47,6 +59,9 @@ public: /// \brief Draw session status void draw (); + /// \brief Draw session connections + void drawConnections (); + /// \brief Create session /// \param config_ FTP config /// \param commandSocket_ Command socket @@ -60,7 +75,7 @@ private: /// \brief Command buffer size constexpr static auto COMMAND_BUFFERSIZE = 4096; -#ifdef NDS +#ifdef __NDS__ /// \brief Response buffer size constexpr static auto RESPONSE_BUFFERSIZE = 4096; @@ -77,7 +92,7 @@ private: /// \brief File buffersize constexpr static auto FILE_BUFFERSIZE = 4 * XFER_BUFFERSIZE; -#if defined(NDS) +#if defined(__NDS__) /// \brief Socket buffer size constexpr static auto SOCK_BUFFERSIZE = 4096; @@ -156,11 +171,21 @@ private: /// \brief Connect data socket bool dataConnect (); + /// \brief Perform stat and apply tz offset to mtime + /// \param path_ Path to stat + /// \param st_ Output stat + int tzStat (char const *const path_, stat_t *st_); + + /// \brief Perform lstat and apply tz offset to mtime + /// \param path_ Path to lstat + /// \param st_ Output stat + int tzLStat (char const *const path_, stat_t *st_); + /// \brief Fill directory entry /// \param st_ Entry status /// \param path_ Path name /// \param type_ MLST type - int fillDirent (struct stat const &st_, std::string_view path_, char const *type_ = nullptr); + int fillDirent (stat_t const &st_, std::string_view path_, char const *type_ = nullptr); /// \brief Fill directory entry /// \param path_ Path name @@ -199,13 +224,18 @@ private: /// \brief Transfer directory list bool listTransfer (); +#if FTPD_HAS_GLOB + /// \brief Transfer glob list + bool globTransfer (); +#endif + /// \brief Transfer download bool retrieveTransfer (); /// \brief Transfer upload bool storeTransfer (); -#ifndef NDS +#ifndef __NDS__ /// \brief Mutex platform::Mutex m_lock; #endif @@ -285,10 +315,41 @@ private: /// \brief Directory being transferred fs::Dir m_dir; +#if FTPD_HAS_GLOB + /// \brief Glob wrappre + class Glob + { + public: + ~Glob () noexcept; + Glob () noexcept; + + /// \brief Perform glob + /// \param pattern_ Glob pattern + bool glob (char const *pattern_) noexcept; + + /// \brief Get next glob result + /// \note returns nullptr when no more entries exist + char const *next () noexcept; + + private: + /// \brief Clear glob + void clear () noexcept; + + /// \brief Glob result + std::optional m_glob = std::nullopt; + + /// \brief Result counter + unsigned m_offset = 0; + }; + + /// \brief Glob + Glob m_glob; +#endif + /// \brief Directory transfer mode XferDirMode m_xferDirMode; - /// \brief Last command timestamp + /// \brief Last activity timestamp time_t m_timestamp; /// \brief Whether user has been authorized diff --git a/include/log.h b/include/log.h index 29122fa..5fa2956 100644 --- a/include/log.h +++ b/include/log.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2023 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -24,6 +24,10 @@ #include #include +#ifdef DEBUG +#undef DEBUG +#endif + /// \brief Log level enum LogLevel { diff --git a/include/mdns.h b/include/mdns.h new file mode 100644 index 0000000..4fd8ce5 --- /dev/null +++ b/include/mdns.h @@ -0,0 +1,40 @@ + +// ftpd is a server implementation based on the following: +// - RFC 959 (https://datatracker.ietf.org/doc/html/rfc959) +// - RFC 3659 (https://datatracker.ietf.org/doc/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// ftpd implements mdns based on the following: +// - RFC 1035 (https://datatracker.ietf.org/doc/html/rfc1035) +// - RFC 6762 (https://datatracker.ietf.org/doc/html/rfc6762) +// +// Copyright (C) 2024 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "sockAddr.h" +#include "socket.h" + +#include + +namespace mdns +{ +void setHostname (std::string hostname_); + +UniqueSocket createSocket (); + +void handleSocket (Socket *socket_, SockAddr const &addr_); +} diff --git a/include/platform.h b/include/platform.h index bde8500..86238cb 100644 --- a/include/platform.h +++ b/include/platform.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ #include "sockAddr.h" -#if defined(NDS) +#if defined(__NDS__) #include #elif defined(__3DS__) #include <3ds.h> @@ -37,6 +37,7 @@ #include #include #include +#include #if defined(CLASSIC) && !defined(__WIIU__) extern PrintConsole g_statusConsole; @@ -74,6 +75,9 @@ bool networkVisible (); /// \param[out] addr_ Network address bool networkAddress (SockAddr &addr_); +/// \brief Get hostname +std::string const &hostname (); + /// \brief Platform loop bool loop (); @@ -110,7 +114,7 @@ struct steady_clock using steady_clock = std::chrono::steady_clock; #endif -#ifndef NDS +#ifndef __NDS__ /// \brief Platform thread class Thread { diff --git a/include/sockAddr.h b/include/sockAddr.h index b3af3c6..7f36ba5 100644 --- a/include/sockAddr.h +++ b/include/sockAddr.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,24 +23,55 @@ #include #include +#include #include -#ifdef NDS -struct sockaddr_storage -{ - unsigned short ss_family; - char ss_data[sizeof (struct sockaddr_in) - sizeof (unsigned short)]; -}; -#endif - /// \brief Socket address class SockAddr { public: + enum class Domain + { + IPv4 = AF_INET, +#ifndef NO_IPV6 + IPv6 = AF_INET6, +#endif + }; + + /// \brief 0.0.0.0 + static SockAddr const AnyIPv4; + +#ifndef NO_IPV6 + /// \brief :: + static SockAddr const AnyIPv6; +#endif + ~SockAddr (); SockAddr (); + /// \brief Parameterized constructor + /// \param domain_ Socket domain + /// \note Initial address is INADDR_ANY/in6addr_any + SockAddr (Domain domain_); + + /// \brief Parameterized constructor + /// \param addr_ Socket address (network byte order) + /// \param port_ Socket port (host byte order) + SockAddr (in_addr_t addr_, std::uint16_t port_ = 0); + + /// \brief Parameterized constructor + /// \param addr_ Socket address (network byte order) + /// \param port_ Socket port (host byte order) + SockAddr (in_addr const &addr_, std::uint16_t port_ = 0); + +#ifndef NO_IPV6 + /// \brief Parameterized constructor + /// \param addr_ Socket address + /// \param port_ Socket port (host byte order) + SockAddr (in6_addr const &addr_, std::uint16_t port_ = 0); +#endif + /// \brief Copy constructor /// \param that_ Object to copy SockAddr (SockAddr const &that_); @@ -57,46 +88,69 @@ public: /// \param that_ Object to move from SockAddr &operator= (SockAddr &&that_); - /// \param Parameterized constructor - /// \param addr_ Address - SockAddr (struct sockaddr const &addr_); + /// \brief Parameterized constructor + /// \param addr_ Address (network byte order) + SockAddr (sockaddr_in const &addr_); - /// \param Parameterized constructor - /// \param addr_ Address - SockAddr (struct sockaddr_in const &addr_); - -#ifndef __3DS__ - /// \param Parameterized constructor - /// \param addr_ Address - SockAddr (struct sockaddr_in6 const &addr_); +#ifndef NO_IPV6 + /// \brief Parameterized constructor + /// \param addr_ Address (network byte order) + SockAddr (sockaddr_in6 const &addr_); #endif - /// \param Parameterized constructor - /// \param addr_ Address - SockAddr (struct sockaddr_storage const &addr_); + /// \brief Parameterized constructor + /// \param addr_ Address (network byte order) + SockAddr (sockaddr_storage const &addr_); - /// \param sockaddr_in cast operator - operator struct sockaddr_in const & () const; + /// \brief sockaddr_in cast operator (network byte order) + operator sockaddr_in const & () const; -#ifndef __3DS__ - /// \param sockaddr_in6 cast operator - operator struct sockaddr_in6 const & () const; +#ifndef NO_IPV6 + /// \brief sockaddr_in6 cast operator (network byte order) + operator sockaddr_in6 const & () const; #endif - /// \param sockaddr_storage cast operator - operator struct sockaddr_storage const & () const; + /// \brief sockaddr_storage cast operator (network byte order) + operator sockaddr_storage const & () const; - /// \param sockaddr* cast operator - operator struct sockaddr * (); - /// \param sockaddr const* cast operator - operator struct sockaddr const * () const; + /// \brief sockaddr* cast operator (network byte order) + operator sockaddr * (); - /// \brief Address port + /// \brief sockaddr const* cast operator (network byte order) + operator sockaddr const * () const; + + /// \brief Equality operator + bool operator== (SockAddr const &that_) const; + + /// \brief Comparison operator + std::strong_ordering operator<=> (SockAddr const &that_) const; + + /// \brief sockaddr domain + Domain domain () const; + + /// \brief sockaddr size + socklen_t size () const; + + /// \brief Set address + /// \param addr_ Address to set (network byte order) + void setAddr (in_addr_t addr_); + + /// \brief Set address + /// \param addr_ Address to set (network byte order) + void setAddr (in_addr const &addr_); + +#ifndef NO_IPV6 + /// \brief Set address + /// \param addr_ Address to set (network byte order) + void setAddr (in6_addr const &addr_); +#endif + + /// \brief Address port (host byte order) std::uint16_t port () const; /// \brief Set address port - /// \param port_ Port to set - bool setPort (std::uint16_t port_); + /// \param port_ Port to set (host byte order) + void setPort (std::uint16_t port_); /// \brief Address name /// \param buffer_ Buffer to hold name @@ -111,6 +165,6 @@ public: char const *name () const; private: - /// \brief Address storage - struct sockaddr_storage m_addr = {}; + /// \brief Address storage (network byte order) + sockaddr_storage m_addr = {}; }; diff --git a/include/socket.h b/include/socket.h index 7effc1d..f10f221 100644 --- a/include/socket.h +++ b/include/socket.h @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ #include #include -#ifdef NDS +#ifdef __NDS__ struct pollfd { int fd; @@ -34,10 +34,9 @@ struct pollfd int revents; }; -using socklen_t = int; -using nfds_t = unsigned int; +using nfds_t = unsigned int; -extern "C" int poll (struct pollfd *fds_, nfds_t nfds_, int timeout_); +extern "C" int poll (pollfd *fds_, nfds_t nfds_, int timeout_); #define POLLIN (1 << 0) #define POLLPRI (1 << 1) @@ -56,6 +55,12 @@ using SharedSocket = std::shared_ptr; class Socket { public: + enum Type + { + eStream = SOCK_STREAM, ///< Stream socket + eDatagram = SOCK_DGRAM, ///< Datagram socket + }; + /// \brief Poll info struct PollInfo { @@ -116,6 +121,18 @@ public: /// \param size_ Buffer size bool setSendBufferSize (std::size_t size_); +#ifndef __NDS__ + /// \brief Join multicast group + /// \param addr_ Multicast group address + /// \param iface_ Interface address + bool joinMulticastGroup (SockAddr const &addr_, SockAddr const &iface_); + + /// \brief Drop multicast group + /// \param addr_ Multicast group address + /// \param iface_ Interface address + bool dropMulticastGroup (SockAddr const &addr_, SockAddr const &iface_); +#endif + /// \brief Read data /// \param buffer_ Output buffer /// \param size_ Size to read @@ -127,6 +144,12 @@ public: /// \param oob_ Whether to read from out-of-band std::make_signed_t read (IOBuffer &buffer_, bool oob_ = false); + /// \brief Read data + /// \param buffer_ Output buffer + /// \param size_ Size to read + /// \param[out] addr_ Source address + std::make_signed_t readFrom (void *buffer_, std::size_t size_, SockAddr &addr_); + /// \brief Write data /// \param buffer_ Input buffer /// \param size_ Size to write @@ -137,13 +160,21 @@ public: /// \param size_ Size to write std::make_signed_t write (IOBuffer &buffer_); + /// \brief Write data + /// \param buffer_ Input buffer + /// \param size_ Size to write + /// \param[out] addr_ Destination address + std::make_signed_t + writeTo (void const *buffer_, std::size_t size_, SockAddr const &addr_); + /// \brief Local name SockAddr const &sockName () const; /// \brief Peer name SockAddr const &peerName () const; /// \brief Create socket - static UniqueSocket create (); + /// \param type_ Socket type + static UniqueSocket create (Type type_); /// \brief Poll sockets /// \param info_ Poll info diff --git a/source/fs.cpp b/source/fs.cpp index 98c6865..6bf39e7 100644 --- a/source/fs.cpp +++ b/source/fs.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,13 +20,26 @@ #include "fs.h" #include "IOAbstraction.h" +#include "ioBuffer.h" +#include +#include + +#include #include +#include #include +#include #include +#include +#include +#include #include +#include +#include +#include -#if defined(NDS) || defined(__3DS__) || defined(__SWITCH__) || defined(__WIIU__) +#if defined(__NDS__) || defined(__3DS__) || defined(__SWITCH__) || defined(__WIIU__) #define getline __getline #endif @@ -39,7 +52,7 @@ std::string fs::printSize (std::uint64_t const size_) constexpr std::uint64_t const PiB = 1024 * TiB; constexpr std::uint64_t const EiB = 1024 * PiB; - char buffer[64] = {}; + std::array buffer{}; for (auto const &[name, bin] : { // clang-format off @@ -57,30 +70,32 @@ std::string fs::printSize (std::uint64_t const size_) if (size_ >= 100 * bin) { // >= 100, print xxxXiB - std::sprintf (buffer, "%" PRIu64 "%s", whole, name); - return buffer; + std::size_t const size = std::sprintf (buffer.data (), "%" PRIu64 "%s", whole, name); + return {buffer.data (), size}; } // get the fractional portion of the number - auto const frac = size_ - whole * bin; + auto const frac = size_ - (whole * bin); if (size_ >= 10 * bin) { // >= 10, print xx.xXiB - std::sprintf (buffer, "%" PRIu64 ".%" PRIu64 "%s", whole, frac * 10 / bin, name); - return buffer; + std::size_t const size = std::sprintf ( + buffer.data (), "%" PRIu64 ".%" PRIu64 "%s", whole, frac * 10 / bin, name); + return {buffer.data (), size}; } if (size_ >= 1000 * (bin / KiB)) { // >= 1000 of lesser bin, print x.xxXiB - std::sprintf (buffer, "%" PRIu64 ".%02" PRIu64 "%s", whole, frac * 100 / bin, name); - return buffer; + std::size_t const size = std::sprintf ( + buffer.data (), "%" PRIu64 ".%02" PRIu64 "%s", whole, frac * 100 / bin, name); + return {buffer.data (), size}; } } // < 1KiB, just print the number - std::sprintf (buffer, "%" PRIu64 "B", size_); - return buffer; + std::size_t const size = std::sprintf (buffer.data (), "%" PRIu64 "B", size_); + return {buffer.data (), size}; } /////////////////////////////////////////////////////////////////////////// @@ -107,26 +122,24 @@ fs::File::operator FILE * () const void fs::File::setBufferSize (std::size_t const size_) { - if (m_bufferSize != size_) - { - m_buffer = std::make_unique (size_); - m_bufferSize = size_; - } + if (m_buffer.size () != size_) + m_buffer.resize (size_); if (m_fp) - std::setvbuf (m_fp.get (), m_buffer.get (), _IOFBF, m_bufferSize); + (void)std::setvbuf (m_fp.get (), m_buffer.data (), _IOFBF, m_buffer.size ()); } -bool fs::File::open (char const *const path_, char const *const mode_) +bool fs::File::open (gsl::not_null const path_, + gsl::not_null const mode_) { - auto const fp = IOAbstraction::fopen (path_, mode_); + gsl::owner fp = IOAbstraction::fopen (path_, mode_); if (!fp) return false; m_fp = std::unique_ptr (fp, &std::fclose); - if (m_buffer) - std::setvbuf (m_fp.get (), m_buffer.get (), _IOFBF, m_bufferSize); + if (!m_buffer.empty ()) + (void)std::setvbuf (m_fp.get (), m_buffer.data (), _IOFBF, m_buffer.size ()); return true; } @@ -136,17 +149,27 @@ void fs::File::close () m_fp.reset (); } -std::make_signed_t fs::File::seek (std::size_t const pos_, int const origin_) +std::make_signed_t fs::File::seek (std::make_signed_t const pos_, + int const origin_) { return IOAbstraction::fseek (m_fp.get (), pos_, origin_); } -std::make_signed_t fs::File::read (void *const buffer_, std::size_t const size_) +std::make_signed_t fs::File::read (gsl::not_null const buffer_, + std::size_t const size_) { assert (buffer_); assert (size_ > 0); - return IOAbstraction::fread (buffer_, 1, size_, m_fp.get ()); + auto const rc = IOAbstraction::fread (buffer_, 1, size_, m_fp.get ()); + if (rc == 0) + { + if (std::feof (m_fp.get ())) + return 0; + return -1; + } + + return gsl::narrow_cast> (rc); } std::make_signed_t fs::File::read (IOBuffer &buffer_) @@ -177,37 +200,41 @@ std::string_view fs::File::readLine () } if (rc > 0) - return std::string_view (m_lineBuffer, rc); + return {m_lineBuffer, gsl::narrow_cast (rc)}; } } -bool fs::File::readAll (void *const buffer_, std::size_t const size_) +bool fs::File::readAll (gsl::not_null const buffer_, std::size_t const size_) { assert (buffer_); assert (size_ > 0); - auto p = static_cast (buffer_); + auto const p = static_cast (buffer_.get ()); std::size_t bytes = 0; while (bytes < size_) { - auto const rc = read (p, size_ - bytes); + auto const rc = read (p + bytes, size_ - bytes); if (rc <= 0) return false; - p += rc; bytes += rc; } return true; } -std::make_signed_t fs::File::write (void const *const buffer_, std::size_t const size_) +std::make_signed_t fs::File::write (gsl::not_null const buffer_, + std::size_t const size_) { assert (buffer_); assert (size_ > 0); - return IOAbstraction::fwrite (buffer_, 1, size_, m_fp.get ()); + auto const rc = IOAbstraction::fwrite (buffer_, 1, size_, m_fp.get ()); + if (rc == 0) + return -1; + + return gsl::narrow_cast> (rc); } std::make_signed_t fs::File::write (IOBuffer &buffer_) @@ -221,21 +248,20 @@ std::make_signed_t fs::File::write (IOBuffer &buffer_) return rc; } -bool fs::File::writeAll (void const *const buffer_, std::size_t const size_) +bool fs::File::writeAll (gsl::not_null const buffer_, std::size_t const size_) { assert (buffer_); assert (size_ > 0); - auto p = static_cast (buffer_); + auto const p = static_cast (buffer_.get ()); std::size_t bytes = 0; while (bytes < size_) { - auto const rc = write (p, size_ - bytes); + auto const rc = write (p + bytes, size_ - bytes); if (rc <= 0) return false; - p += rc; bytes += rc; } @@ -261,7 +287,7 @@ fs::Dir::operator DIR * () const return m_dp.get (); } -bool fs::Dir::open (char const *const path_) +bool fs::Dir::open (gsl::not_null const path_) { auto const dp = IOAbstraction::opendir (path_); if (!dp) @@ -276,7 +302,7 @@ void fs::Dir::close () m_dp.reset (); } -struct dirent *fs::Dir::read () +dirent *fs::Dir::read () { errno = 0; return IOAbstraction::readdir (m_dp.get ()); diff --git a/source/ftpConfig.cpp b/source/ftpConfig.cpp index 0b4dd9d..a692ab6 100644 --- a/source/ftpConfig.cpp +++ b/source/ftpConfig.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,11 +22,23 @@ #include "fs.h" #include "log.h" +#include "platform.h" + +#include #include +using stat_t = struct stat; -#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include namespace { @@ -36,14 +48,14 @@ constexpr std::uint16_t DEFAULT_PORT = 21; constexpr std::uint16_t DEFAULT_PORT = 5000; #endif -bool mkdirParent (std::string const &path_) +bool mkdirParent (std::string_view const path_) { auto pos = path_.find_first_of ('/'); while (pos != std::string::npos) { - auto const dir = path_.substr (0, pos); + auto const dir = std::string (path_.substr (0, pos)); - struct stat st; + stat_t st{}; auto const rc = ::stat (dir.c_str (), &st); if (rc < 0 && errno != ENOENT) return false; @@ -61,14 +73,13 @@ bool mkdirParent (std::string const &path_) return true; } -std::string strip (std::string const &str_) +std::string_view strip (std::string_view const str_) { auto const start = str_.find_first_not_of (" \t"); if (start == std::string::npos) return {}; auto const end = str_.find_last_not_of (" \t"); - if (end == std::string::npos) return str_.substr (start); @@ -76,37 +87,21 @@ std::string strip (std::string const &str_) } template -bool parseInt (T &out_, std::string const &val_) +bool parseInt (T &out_, std::string_view const val_) { - T val = 0; - - for (auto const &c : val_) + auto const rc = std::from_chars (val_.data (), val_.data () + val_.size (), out_); + if (rc.ec != std::errc{}) { - if (!std::isdigit (c)) - { - errno = EINVAL; - return false; - } - - if (std::numeric_limits::max () / 10 < val) - { - errno = EOVERFLOW; - return false; - } - - val *= 10; - - auto const v = c - '0'; - if (std::numeric_limits::max () - v < val) - { - errno = EOVERFLOW; - return false; - } - - val += v; + errno = static_cast (rc.ec); + return false; + } + + if (rc.ptr != val_.data () + val_.size ()) + { + errno = EINVAL; + return false; } - out_ = val; return true; } } @@ -123,7 +118,7 @@ UniqueFtpConfig FtpConfig::create () return UniqueFtpConfig (new FtpConfig ()); } -UniqueFtpConfig FtpConfig::load (char const *const path_) +UniqueFtpConfig FtpConfig::load (gsl::not_null const path_) { auto config = create (); @@ -143,8 +138,8 @@ UniqueFtpConfig FtpConfig::load (char const *const path_) continue; } - auto const key = strip (line.substr (0, pos)); - auto const val = strip (line.substr (pos + 1)); + auto const key = strip (std::string_view (line).substr (0, pos)); + auto const val = strip (std::string_view (line).substr (pos + 1)); if (key.empty () || val.empty ()) { error ("Ignoring '%s'\n", line.c_str ()); @@ -165,7 +160,9 @@ UniqueFtpConfig FtpConfig::load (char const *const path_) else if (val == "1") config->m_getMTime = true; else - error ("Invalid value for mtime: %s\n", val.c_str ()); + error ("Invalid value for mtime: %.*s\n", + gsl::narrow_cast (val.size ()), + val.data ()); } #endif #ifdef __SWITCH__ @@ -176,7 +173,9 @@ UniqueFtpConfig FtpConfig::load (char const *const path_) else if (val == "1") config->m_enableAP = true; else - error ("Invalid value for ap: %s\n", val.c_str ()); + error ("Invalid value for ap: %.*s\n", + gsl::narrow_cast (val.size ()), + val.data ()); } else if (key == "ssid") config->m_ssid = val; @@ -190,16 +189,16 @@ UniqueFtpConfig FtpConfig::load (char const *const path_) return config; } -#ifndef NDS +#ifndef __NDS__ std::scoped_lock FtpConfig::lockGuard () { return std::scoped_lock (m_lock); } #endif -bool FtpConfig::save (char const *const path_) +bool FtpConfig::save (gsl::not_null const path_) { - if (!mkdirParent (path_)) + if (!mkdirParent (path_.get ())) return false; auto fp = fs::File (); @@ -207,21 +206,21 @@ bool FtpConfig::save (char const *const path_) return false; if (!m_user.empty ()) - std::fprintf (fp, "user=%s\n", m_user.c_str ()); + (void)std::fprintf (fp, "user=%s\n", m_user.c_str ()); if (!m_pass.empty ()) - std::fprintf (fp, "pass=%s\n", m_pass.c_str ()); - std::fprintf (fp, "port=%u\n", m_port); + (void)std::fprintf (fp, "pass=%s\n", m_pass.c_str ()); + (void)std::fprintf (fp, "port=%u\n", m_port); #ifdef __3DS__ - std::fprintf (fp, "mtime=%u\n", m_getMTime); + (void)std::fprintf (fp, "mtime=%u\n", m_getMTime); #endif #ifdef __SWITCH__ - std::fprintf (fp, "ap=%u\n", m_enableAP); + (void)std::fprintf (fp, "ap=%u\n", m_enableAP); if (!m_ssid.empty ()) - std::fprintf (fp, "ssid=%s\n", m_ssid.c_str ()); + (void)std::fprintf (fp, "ssid=%s\n", m_ssid.c_str ()); if (!m_passphrase.empty ()) - std::fprintf (fp, "passphrase=%s\n", m_passphrase.c_str ()); + (void)std::fprintf (fp, "passphrase=%s\n", m_passphrase.c_str ()); #endif return true; @@ -237,6 +236,11 @@ std::string const &FtpConfig::pass () const return m_pass; } +std::string const &FtpConfig::hostname () const +{ + return m_hostname; +} + std::uint16_t FtpConfig::port () const { return m_port; @@ -266,19 +270,24 @@ std::string const &FtpConfig::passphrase () const } #endif -void FtpConfig::setUser (std::string const &user_) +void FtpConfig::setUser (std::string user_) { - m_user = user_.substr (0, user_.find_first_of ('\0')); + m_user = std::move (user_); } -void FtpConfig::setPass (std::string const &pass_) +void FtpConfig::setPass (std::string pass_) { - m_pass = pass_.substr (0, pass_.find_first_of ('\0')); + m_pass = std::move (pass_); } -bool FtpConfig::setPort (std::string const &port_) +void FtpConfig::setHostname (std::string hostname_) { - std::uint16_t parsed; + m_hostname = std::move (hostname_); +} + +bool FtpConfig::setPort (std::string_view const port_) +{ + std::uint16_t parsed{}; if (!parseInt (parsed, port_)) return false; @@ -294,7 +303,7 @@ bool FtpConfig::setPort (std::uint16_t const port_) errno = EPERM; return false; } -#elif defined(NDS) || defined(__3DS__) +#elif defined(__NDS__) || defined(__3DS__) // 3DS is allowed < 1024, but not 0 // NDS is allowed < 1024, but 0 crashes the app if (port_ == 0) @@ -321,12 +330,12 @@ void FtpConfig::setEnableAP (bool const enable_) m_enableAP = enable_; } -void FtpConfig::setSSID (std::string const &ssid_) +void FtpConfig::setSSID (std::string_view const ssid_) { m_ssid = ssid_.substr (0, ssid_.find_first_of ('\0')); } -void FtpConfig::setPassphrase (std::string const &passphrase_) +void FtpConfig::setPassphrase (std::string_view const passphrase_) { m_passphrase = passphrase_.substr (0, passphrase_.find_first_of ('\0')); } diff --git a/source/ftpServer.cpp b/source/ftpServer.cpp index 49b238c..46b9970 100644 --- a/source/ftpServer.cpp +++ b/source/ftpServer.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2023 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -21,36 +21,62 @@ #include "ftpServer.h" #include "fs.h" -#ifndef __WIIU__ -#include "licenses.h" -#endif +#include "ftpConfig.h" +#include "ftpSession.h" #include "log.h" #include "platform.h" +#include "sockAddr.h" #include "socket.h" #ifndef __WIIU__ #include "imgui.h" +#include "licenses.h" +#endif +#ifndef __NDS__ +#include "mdns.h" #endif -#ifdef NDS +#ifdef __NDS__ #include #endif -#include +#ifdef __3DS__ +#include +#endif + +#ifndef CLASSIC +#include + +#include + +#include +#include +#ifndef NDEBUG +#include +#endif +#endif + #include -#include +using statvfs_t = struct statvfs; #include +#include +#include #include #include +#include #include #include +#include +#include #include +#include #include -#include +#include +#include using namespace std::chrono_literals; -#ifdef NDS +#ifdef __NDS__ #define LOCKED(x) x #else #define LOCKED(x) \ @@ -66,7 +92,12 @@ namespace /// \brief Application start time auto const s_startTime = std::time (nullptr); -#ifndef NDS +#ifdef __3DS__ +/// \brief Timezone offset in seconds (only used on 3DS) +int s_tzOffset = 0; +#endif + +#ifndef __NDS__ /// \brief Mutex for s_freeSpace platform::Mutex s_lock; #endif @@ -76,21 +107,33 @@ std::string s_freeSpace; #ifndef CLASSIC #ifndef NDEBUG -std::string printable (char *const data_, std::size_t const size_) +std::string printable (std::string_view const data_) { - std::string result; - result.reserve (size_); - - for (std::size_t i = 0; i < size_; ++i) + unsigned count = 0; + for (auto const &c : data_) { - if (std::isprint (data_[i]) || std::isspace (data_[i])) - result.push_back (data_[i]); + if (c != '%' && (std::isprint (c) || std::isspace (c))) + ++count; + else + count += 3; + } + + std::string result; + result.reserve (count); + + for (auto const &c : data_) + { + if (c != '%' && (std::isprint (c) || std::isspace (c))) + result.push_back (c); else { - char buffer[5]; - std::snprintf ( - buffer, sizeof (buffer), "%%%02u", static_cast (data_[i])); - result += buffer; + result.push_back ('%'); + + auto const upper = (static_cast (c) >> 4u) & 0xF; + auto const lower = (static_cast (c) >> 0u) & 0xF; + + result.push_back (gsl::narrow_cast (upper < 10 ? upper + '0' : upper + 'A' - 10)); + result.push_back (gsl::narrow_cast (lower < 10 ? lower + '0' : lower + 'A' - 10)); } } @@ -103,9 +146,10 @@ int curlDebug (CURL *const handle_, std::size_t const size_, void *const user_) { + (void)handle_; (void)user_; - auto const text = printable (data_, size_); + auto const text = printable (std::string_view (data_, size_)); switch (type_) { @@ -167,7 +211,7 @@ FtpServer::~FtpServer () { m_quit = true; -#ifndef NDS +#ifndef __NDS__ m_thread.join (); #endif @@ -184,23 +228,36 @@ FtpServer::~FtpServer () #endif } -FtpServer::FtpServer (UniqueFtpConfig config_) : m_config (std::move (config_)), m_quit (false) +FtpServer::FtpServer (UniqueFtpConfig config_) + : m_config (std::move (config_)) +#ifndef CLASSIC + , + m_hostnameSetting (m_config->hostname ()) +#endif { -#ifndef NDS +#ifndef __NDS__ + mdns::setHostname (m_config->hostname ()); + m_thread = platform::Thread (std::bind (&FtpServer::threadFunc, this)); #endif + +#ifdef __3DS__ + s64 tzOffsetMinutes; + if (R_SUCCEEDED (svcGetSystemInfo (&tzOffsetMinutes, 0x10000, 0x103))) + s_tzOffset = tzOffsetMinutes * 60; +#endif } void FtpServer::draw () { -#ifdef NDS +#ifdef __NDS__ loop (); #endif #ifdef CLASSIC { char port[7]; -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif if (m_socket) @@ -214,7 +271,7 @@ void FtpServer::draw () m_socket ? port : ""); #endif -#ifndef NDS +#ifndef __NDS__ char timeBuffer[16]; auto const now = std::time (nullptr); std::strftime (timeBuffer, sizeof (timeBuffer), "%H:%M:%S", std::localtime (&now)); @@ -227,7 +284,7 @@ void FtpServer::draw () } { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif if (!s_freeSpace.empty ()) @@ -244,7 +301,7 @@ void FtpServer::draw () } { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif #ifndef NO_CONSOLE @@ -274,17 +331,17 @@ void FtpServer::draw () ImGui::SetNextWindowSize (ImVec2 (width, height)); #endif { - char title[64]; + std::array title{}; { auto const serverLock = std::scoped_lock (m_lock); - std::snprintf (title, - sizeof (title), + std::snprintf (title.data (), + title.size (), STATUS_STRING " %s###ftpd", m_socket ? m_name.c_str () : "Waiting for WiFi..."); } - ImGui::Begin (title, + ImGui::Begin (title.data (), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize #ifndef __3DS__ @@ -332,6 +389,11 @@ void FtpServer::draw () #endif } +bool FtpServer::quit () +{ + return m_quit; +} + UniqueFtpServer FtpServer::create () { updateFreeSpace (); @@ -343,7 +405,7 @@ UniqueFtpServer FtpServer::create () std::string FtpServer::getFreeSpace () { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif return s_freeSpace; @@ -351,8 +413,8 @@ std::string FtpServer::getFreeSpace () void FtpServer::updateFreeSpace () { - struct statvfs st; -#if defined(NDS) || defined(__3DS__) || defined(__SWITCH__) + statvfs_t st = {}; +#if defined(__NDS__) || defined(__3DS__) || defined(__SWITCH__) if (::statvfs ("sdmc:/", &st) != 0) #else if (::statvfs ("/", &st) != 0) @@ -361,7 +423,7 @@ void FtpServer::updateFreeSpace () auto freeSpace = fs::printSize (static_cast (st.f_bsize) * st.f_bfree); -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif if (freeSpace != s_freeSpace) @@ -373,6 +435,13 @@ std::time_t FtpServer::startTime () return s_startTime; } +#ifdef __3DS__ +int FtpServer::tzOffset () +{ + return s_tzOffset; +} +#endif + void FtpServer::handleNetworkFound () { SockAddr addr; @@ -382,7 +451,7 @@ void FtpServer::handleNetworkFound () std::uint16_t port; { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config->lockGuard (); #endif port = m_config->port (); @@ -390,7 +459,7 @@ void FtpServer::handleNetworkFound () addr.setPort (port); - auto socket = Socket::create (); + auto socket = Socket::create (Socket::eStream); if (!socket) return; @@ -412,6 +481,14 @@ void FtpServer::handleNetworkFound () info ("Started server at %s\n", m_name.c_str ()); LOCKED (m_socket = std::move (socket)); + +#ifndef __NDS__ + socket = mdns::createSocket (); + if (!socket) + return; + + LOCKED (m_mdnsSocket = std::move (socket)); +#endif } void FtpServer::handleNetworkLost () @@ -423,9 +500,15 @@ void FtpServer::handleNetworkLost () } { - // destroy command socket UniqueSocket sock; + + // destroy command socket LOCKED (sock = std::move (m_socket)); + +#ifndef __NDS__ + // destroy mDNS socket + LOCKED (sock = std::move (m_mdnsSocket)); +#endif } info ("Stopped server at %s\n", m_name.c_str ()); @@ -450,7 +533,7 @@ void FtpServer::showMenu () if (ImGui::MenuItem ("Upload Log")) { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif if (!m_uploadLogCurlM) @@ -462,11 +545,6 @@ void FtpServer::showMenu () auto const handle = curl_easy_init (); -#ifdef __3DS__ - // 3DS CA fails peer verification, so add CA here - curl_easy_setopt (handle, CURLOPT_CAINFO, "romfs:/sni.cloudflaressl.com.ca"); -#endif - #ifndef NDEBUG curl_easy_setopt (handle, CURLOPT_DEBUGFUNCTION, &curlDebug); curl_easy_setopt (handle, CURLOPT_DEBUGDATA, nullptr); @@ -479,9 +557,9 @@ void FtpServer::showMenu () curl_easy_setopt (handle, CURLOPT_WRITEDATA, &m_uploadLogResult); // set headers - static char contentType[] = "Content-Type: multipart/form-data"; + static char contentType[] = "Content-Type: text/plain"; static curl_slist const headers = {contentType, nullptr}; - curl_easy_setopt (handle, CURLOPT_URL, "https://hastebin.com/documents"); + curl_easy_setopt (handle, CURLOPT_URL, "https://pastie.io/documents"); curl_easy_setopt (handle, CURLOPT_HTTPHEADER, &headers); // set form data @@ -500,9 +578,16 @@ void FtpServer::showMenu () } } + ImGui::Separator (); + if (ImGui::MenuItem ("About")) m_showAbout = true; + ImGui::Separator (); + + if (ImGui::MenuItem ("Quit")) + m_quit = true; + ImGui::EndMenu (); } ImGui::EndMenuBar (); @@ -512,7 +597,7 @@ void FtpServer::showMenu () { if (!prevShowSettings) { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config->lockGuard (); #endif @@ -522,6 +607,9 @@ void FtpServer::showMenu () m_passSetting = m_config->pass (); m_passSetting.resize (32); + m_hostnameSetting = m_config->hostname (); + m_hostnameSetting.resize (32); + m_portSetting = m_config->port (); #ifdef __3DS__ @@ -579,6 +667,11 @@ void FtpServer::showSettings () m_passSetting.size (), ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_Password); + ImGui::InputText ("Hostname", + m_hostnameSetting.data (), + m_hostnameSetting.size (), + ImGuiInputTextFlags_AutoSelectAll); + ImGui::InputScalar ("Port", ImGuiDataType_U16, &m_portSetting, @@ -645,12 +738,13 @@ void FtpServer::showSettings () m_showSettings = false; ImGui::CloseCurrentPopup (); -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config->lockGuard (); #endif m_config->setUser (m_userSetting); m_config->setPass (m_passSetting); + m_config->setHostname (m_hostnameSetting); m_config->setPort (m_portSetting); #ifdef __3DS__ @@ -666,11 +760,13 @@ void FtpServer::showSettings () UniqueSocket socket; LOCKED (socket = std::move (m_socket)); + + mdns::setHostname (m_hostnameSetting); } if (save) { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config->lockGuard (); #endif if (!m_config->save (FTPDCONFIG)) @@ -681,9 +777,10 @@ void FtpServer::showSettings () { static auto const defaults = FtpConfig::create (); - m_userSetting = defaults->user (); - m_passSetting = defaults->pass (); - m_portSetting = defaults->port (); + m_userSetting = defaults->user (); + m_passSetting = defaults->pass (); + m_hostnameSetting = defaults->hostname (); + m_portSetting = defaults->port (); #ifdef __3DS__ m_getMTimeSetting = defaults->getMTime (); #endif @@ -723,17 +820,31 @@ void FtpServer::showAbout () ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) { ImGui::TextUnformatted (STATUS_STRING); - ImGui::TextWrapped ("Copyright © 2023 Michael Theall, Dave Murphy, TuxSH"); + ImGui::TextWrapped ("Copyright © 2024 Michael Theall, Dave Murphy, TuxSH"); ImGui::Separator (); ImGui::Text ("Platform: %s", io.BackendPlatformName); ImGui::Text ("Renderer: %s", io.BackendRendererName); +#ifdef __3DS__ + ImGui::Text ("Command Buffer Usage: %.1f%%", 100.0f * C3D_GetCmdBufUsage ()); + ImGui::Text ("GPU Processing Usage: %.1f%%", 6.0f * C3D_GetProcessingTime ()); + ImGui::Text ("GPU Drawing Usage: %.1f%%", 6.0f * C3D_GetDrawingTime ()); +#endif + if (ImGui::Button ("OK", ImVec2 (100, 0))) { m_showAbout = false; ImGui::CloseCurrentPopup (); } + ImGui::Separator (); + if (ImGui::TreeNode ("Connections")) + { + for (auto const &session : m_sessions) + session->drawConnections (); + ImGui::TreePop (); + } + ImGui::Separator (); if (ImGui::TreeNode (g_dearImGuiVersion)) { @@ -743,7 +854,7 @@ void FtpServer::showAbout () ImGui::TreePop (); } -#if defined(NDS) +#if defined(__NDS__) #elif defined(__3DS__) if (ImGui::TreeNode (g_libctruVersion)) { @@ -782,7 +893,7 @@ void FtpServer::showAbout () { ImGui::TextWrapped ("%s", g_zstdCopyright); ImGui::Separator (); - ImGui::TextWrapped ("%s", g_bsdLicense); + ImGui::TextWrapped ("%s", g_zstdLicense); ImGui::TreePop (); } #else @@ -795,6 +906,24 @@ void FtpServer::showAbout () } #endif +#if defined(__NDS__) || defined(__3DS__) || defined(__SWITCH__) + if (ImGui::TreeNode (g_globVersion)) + { + ImGui::TextWrapped ("%s", g_globCopyright); + ImGui::Separator (); + ImGui::TextWrapped ("%s", g_globLicense); + ImGui::TreePop (); + } + + if (ImGui::TreeNode (g_collateVersion)) + { + ImGui::TextWrapped ("%s", g_collateCopyright); + ImGui::Separator (); + ImGui::TextWrapped ("%s", g_collateLicense); + ImGui::TreePop (); + } +#endif + ImGui::EndPopup (); } } @@ -854,11 +983,19 @@ void FtpServer::loop () if (msg->data.result != CURLE_OK) info ("cURL finished with status %d\n", msg->data.result); - if (m_uploadLogResult.starts_with ("{\"key\":\"")) + json_error_t err; + auto const root = json_loads (m_uploadLogResult.c_str (), 0, &err); + if (json_is_object (root)) { - auto const key = m_uploadLogResult.substr (8, 10); - info ("https://hastebin.com/%s\n", key.c_str ()); + auto const key = json_object_get (root, "key"); + if (json_is_string (key)) + info ( + "Log uploaded to https://pastie.io/%s\n", json_string_value (key)); } + else + error ("Failed to upload log\n"); + + json_decref (root); curl_multi_remove_handle (m_uploadLogCurlM, m_uploadLogCurl); curl_easy_cleanup (m_uploadLogCurl); @@ -898,11 +1035,17 @@ void FtpServer::loop () } } +#ifndef __NDS__ + // poll mDNS socket + if (m_socket && m_mdnsSocket) + mdns::handleSocket (m_mdnsSocket.get (), m_socket->sockName ()); +#endif + { std::vector deadSessions; { // remove dead sessions -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif auto it = std::begin (m_sessions); @@ -926,7 +1069,7 @@ void FtpServer::loop () if (!FtpSession::poll (m_sessions)) handleNetworkLost (); } -#ifndef NDS +#ifndef __NDS__ // avoid busy polling in background thread else platform::Thread::sleep (16ms); diff --git a/source/ftpSession.cpp b/source/ftpSession.cpp index 82fb927..23fa45a 100644 --- a/source/ftpSession.cpp +++ b/source/ftpSession.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2023 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,9 +23,10 @@ #include "IOAbstraction.h" #include "ftpServer.h" #include "log.h" +#include "mdns.h" #include "platform.h" -#ifndef __WIIU__ +#if !defined(__WIIU__) && !defined(CLASSIC) #include "imgui.h" #endif @@ -33,7 +34,12 @@ #include #include +#if FTPD_HAS_GLOB +#include +#endif + #include +#include #include #include #include @@ -44,13 +50,14 @@ #include #include #include +#include using namespace std::chrono_literals; -#if defined(NDS) || defined(__3DS__) || defined(__SWITCH__) +#if defined(__NDS__) || defined(__3DS__) || defined(__SWITCH__) #define lstat stat #endif -#ifdef NDS +#ifdef __NDS__ #define LOCKED(x) x #else #define LOCKED(x) \ @@ -63,6 +70,36 @@ using namespace std::chrono_literals; namespace { +/// \brief Idle timeout +constexpr auto IDLE_TIMEOUT = 60; + +/// \brief Check if string view is a C string +/// \param str_ String to check +bool isCString (std::string_view const str_) +{ + return str_.find_first_of ('\0') != std::string_view::npos; +} + +/// \brief Case-insensitive string compare +/// \param lhs_ Left string +/// \param rhs_ Right string +int compare (std::string_view const lhs_, std::string_view const rhs_) +{ + if (isCString (lhs_) && isCString (rhs_)) + return ::strcasecmp (lhs_.data (), rhs_.data ()); + + auto const maxLen = std::min (lhs_.size (), rhs_.size ()); + for (unsigned i = 0; i < maxLen; ++i) + { + auto const l = std::tolower (lhs_[i]); + auto const r = std::tolower (rhs_[i]); + if (l != r) + return l - r; + } + + return gsl::narrow_cast (lhs_.size ()) - gsl::narrow_cast (rhs_.size ()); +} + /// \brief Parse command /// \param buffer_ Buffer to parse /// \param size_ Size of buffer @@ -271,6 +308,66 @@ std::string buildResolvedPath (std::string_view const cwd_, std::string_view con } } +/////////////////////////////////////////////////////////////////////////// +#if FTPD_HAS_GLOB +FtpSession::Glob::~Glob () noexcept +{ + clear (); +} + +FtpSession::Glob::Glob () noexcept = default; + +bool FtpSession::Glob::glob (char const *const pattern_) noexcept +{ + if (!m_glob.has_value ()) + m_glob.emplace (); + else + ::globfree (&m_glob.value ()); + + std::memset (&m_glob.value (), 0, sizeof (glob_t)); + + auto const rc = ::glob (pattern_, GLOB_NOSORT, nullptr, &m_glob.value ()); + if (rc == GLOB_NOSPACE) + { + clear (); + errno = ENOMEM; + return false; + } + else if (rc != 0) + { + clear (); + errno = EIO; + return false; + } + + m_offset = 0; + return true; +} + +char const *FtpSession::Glob::next () noexcept +{ + if (!m_glob.has_value ()) + return nullptr; + + if (m_glob->gl_pathc <= 0 || m_offset >= static_cast (m_glob->gl_pathc)) + { + clear (); + return nullptr; + } + + return m_glob->gl_pathv[m_offset++]; +} + +void FtpSession::Glob::clear () noexcept +{ + if (!m_glob.has_value ()) + return; + + ::globfree (&m_glob.value ()); + m_glob.reset (); +} +#endif + /////////////////////////////////////////////////////////////////////////// FtpSession::~FtpSession () { @@ -300,7 +397,7 @@ FtpSession::FtpSession (FtpConfig &config_, UniqueSocket commandSocket_) m_devZero (false) { { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif if (m_config.user ().empty ()) @@ -323,7 +420,7 @@ FtpSession::FtpSession (FtpConfig &config_, UniqueSocket commandSocket_) bool FtpSession::dead () { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif if (m_commandSocket || m_pasvSocket || m_dataSocket) @@ -334,7 +431,7 @@ bool FtpSession::dead () void FtpSession::draw () { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif @@ -388,7 +485,8 @@ void FtpSession::draw () auto const timeDiff = now - m_filePositionTime; m_filePositionTime = now; - auto const rate = diff / std::chrono::duration (timeDiff).count (); + auto const rate = + gsl::narrow_cast (diff) / std::chrono::duration (timeDiff).count (); auto const alpha = 0.01f; m_xferRate = alpha * rate + (1.0f - alpha) * m_xferRate; } @@ -404,6 +502,60 @@ void FtpSession::draw () #endif } +void FtpSession::drawConnections () +{ +#ifndef CLASSIC +#ifdef NO_IPV6 + char peerName[INET_ADDRSTRLEN]; + char sockName[INET_ADDRSTRLEN]; +#else + char peerName[INET6_ADDRSTRLEN]; + char sockName[INET6_ADDRSTRLEN]; +#endif + + static char const *const stateStrings[] = { + "Command", + "Data Connect", + "Data Transfer", + }; + + ImGui::TextWrapped ("State: %s", stateStrings[static_cast (m_state)]); + if (m_commandSocket) + { + m_commandSocket->peerName ().name (peerName, sizeof (peerName)); + m_commandSocket->sockName ().name (sockName, sizeof (sockName)); + + if (m_commandSocket == m_dataSocket) + ImGui::TextWrapped ("Command/Data %s -> %s", peerName, sockName); + else + ImGui::TextWrapped ("Command %s -> %s", peerName, sockName); + } + + if (m_pasvSocket) + { + m_pasvSocket->sockName ().name (sockName, sizeof (sockName)); + ImGui::TextWrapped ("PASV %s", sockName); + } + + if (m_dataSocket && m_dataSocket != m_commandSocket) + { + m_dataSocket->peerName ().name (peerName, sizeof (peerName)); + m_dataSocket->sockName ().name (sockName, sizeof (sockName)); + ImGui::TextWrapped ("Data %s -> %s", peerName, sockName); + } + + for (auto const &sock : m_pendingCloseSocket) + { + if (!sock) + continue; + + sock->peerName ().name (peerName, sizeof (peerName)); + sock->sockName ().name (sockName, sizeof (sockName)); + ImGui::TextWrapped ("Closing %s -> %s", peerName, sockName); + } +#endif +} + UniqueFtpSession FtpSession::create (FtpConfig &config_, UniqueSocket commandSocket_) { return UniqueFtpSession (new FtpSession (config_, std::move (commandSocket_))); @@ -418,7 +570,7 @@ bool FtpSession::poll (std::vector const &sessions_) for (auto &pending : session->m_pendingCloseSocket) { assert (pending.unique ()); - pollInfo.emplace_back (Socket::PollInfo{*pending, POLLIN, 0}); + pollInfo.emplace_back (*pending, POLLIN, 0); } } @@ -462,8 +614,7 @@ bool FtpSession::poll (std::vector const &sessions_) { if (session->m_commandSocket) { - pollInfo.emplace_back ( - Socket::PollInfo{*session->m_commandSocket, POLLIN | POLLPRI, 0}); + pollInfo.emplace_back (*session->m_commandSocket, POLLIN | POLLPRI, 0); if (session->m_responseBuffer.usedSize () != 0) pollInfo.back ().events |= POLLOUT; } @@ -479,12 +630,12 @@ bool FtpSession::poll (std::vector const &sessions_) { assert (!session->m_port); // we are waiting for a PASV connection - pollInfo.emplace_back (Socket::PollInfo{*session->m_pasvSocket, POLLIN, 0}); + pollInfo.emplace_back (*session->m_pasvSocket, POLLIN, 0); } else { // we are waiting to complete a PORT connection - pollInfo.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLOUT, 0}); + pollInfo.emplace_back (*session->m_dataSocket, POLLOUT, 0); } break; @@ -493,12 +644,12 @@ bool FtpSession::poll (std::vector const &sessions_) if (session->m_recv) { assert (!session->m_send); - pollInfo.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLIN, 0}); + pollInfo.emplace_back (*session->m_dataSocket, POLLIN, 0); } else { assert (session->m_send); - pollInfo.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLOUT, 0}); + pollInfo.emplace_back (*session->m_dataSocket, POLLOUT, 0); } break; } @@ -515,16 +666,18 @@ bool FtpSession::poll (std::vector const &sessions_) return false; } - if (rc == 0) - return true; + auto const now = std::time (nullptr); for (auto &session : sessions_) { + bool handled = false; for (auto const &i : pollInfo) { if (!i.revents) continue; + handled = true; + // check command socket if (&i.socket.get () == session->m_commandSocket.get ()) { @@ -608,6 +761,13 @@ bool FtpSession::poll (std::vector const &sessions_) } } } + + if (!handled && now - session->m_timestamp >= IDLE_TIMEOUT) + { + session->closeCommand (); + session->closePasv (); + session->closeData (); + } } return true; @@ -620,7 +780,8 @@ bool FtpSession::authorized () const void FtpSession::setState (State const state_, bool const closePasv_, bool const closeData_) { - m_state = state_; + m_state = state_; + m_timestamp = std::time (nullptr); if (closePasv_) closePasv (); @@ -630,7 +791,7 @@ void FtpSession::setState (State const state_, bool const closePasv_, bool const if (state_ == State::COMMAND) { { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (m_lock); #endif @@ -702,8 +863,8 @@ bool FtpSession::changeDir (char const *const args_) if (path.empty ()) return false; - struct stat st; - if (IOAbstraction::stat (path.c_str (), &st) != 0) + stat_t st; + if (tzStat (path.c_str (), &st) != 0) return false; if (!S_ISDIR (st.st_mode)) @@ -763,7 +924,7 @@ bool FtpSession::dataConnect () m_port = false; - auto data = Socket::create (); + auto data = Socket::create (Socket::eStream); LOCKED (m_dataSocket = std::move (data)); if (!m_dataSocket) return false; @@ -791,7 +952,49 @@ bool FtpSession::dataConnect () return true; } -int FtpSession::fillDirent (struct stat const &st_, std::string_view const path_, char const *type_) +int FtpSession::tzStat (char const *const path_, stat_t *st_) +{ + auto const rc = IOAbstraction::stat (path_, st_); + if (rc != 0) + return rc; + +#ifdef __3DS__ + if (m_config.getMTime ()) + { + std::uint64_t mtime = 0; + auto const rc = archive_getmtime (path_, &mtime); + if (rc != 0) + error ("sdmc_getmtime %s 0x%lx\n", path_, rc); + else + st_->st_mtime = mtime - FtpServer::tzOffset (); + } +#endif + + return 0; +} + +int FtpSession::tzLStat (char const *const path_, stat_t *st_) +{ + auto const rc = IOAbstraction::lstat (path_, st_); + if (rc != 0) + return rc; + +#ifdef __3DS__ + if (m_config.getMTime ()) + { + std::uint64_t mtime = 0; + auto const rc = archive_getmtime (path_, &mtime); + if (rc != 0) + error ("sdmc_getmtime %s 0x%lx\n", path_, rc); + else + st_->st_mtime = mtime - FtpServer::tzOffset (); + } +#endif + + return 0; +} + +int FtpSession::fillDirent (stat_t const &st_, std::string_view const path_, char const *type_) { auto const buffer = m_xferBuffer.freeArea (); auto const size = m_xferBuffer.freeSize (); @@ -1090,8 +1293,8 @@ int FtpSession::fillDirent (struct stat const &st_, std::string_view const path_ int FtpSession::fillDirent (std::string const &path_, char const *type_) { - struct stat st; - if (IOAbstraction::stat (path_.c_str (), &st) != 0) + stat_t st; + if (tzStat (path_.c_str (), &st) != 0) return errno; return fillDirent (st, encodePath (path_), type_); @@ -1117,8 +1320,8 @@ void FtpSession::xferFile (char const *const args_, XferFileMode const mode_) else if (mode_ == XferFileMode::RETR) { // stat the file - struct stat st; - if (IOAbstraction::stat (path.c_str (), &st) != 0) + stat_t st; + if (tzStat (path.c_str (), &st) != 0) { sendResponse ("450 %s\r\n", std::strerror (errno)); return; @@ -1229,25 +1432,19 @@ void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool if (std::strlen (args_) > 0) { + // work around broken clients that think LIST -a/-l is valid + auto const needWorkaround = workaround_ && args_[0] == '-' && + (args_[1] == 'a' || args_[1] == 'l') && + (args_[2] == '\0' || args_[2] == ' '); + // an argument was provided auto const path = buildResolvedPath (m_cwd, args_); if (path.empty ()) { - // work around broken clients that think LIST -a/-l is valid - if (workaround_) + if (needWorkaround) { - if (args_[0] == '-' && (args_[1] == 'a' || args_[1] == 'l')) - { - char const *args = &args_[2]; - if (*args == '\0' || *args == ' ') - { - if (*args == ' ') - ++args; - - xferDir (args, mode_, false); - return; - } - } + xferDir (args_ + 2 + (args_[2] == ' '), mode_, false); + return; } sendResponse ("550 %s\r\n", std::strerror (errno)); @@ -1255,9 +1452,15 @@ void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool return; } - struct stat st; - if (IOAbstraction::stat (path.c_str (), &st) != 0) + stat_t st; + if (tzStat (path.c_str (), &st) != 0) { + if (needWorkaround) + { + xferDir (args_ + 2 + (args_[2] == ' '), mode_, false); + return; + } + sendResponse ("550 %s\r\n", std::strerror (errno)); setState (State::COMMAND, true, true); return; @@ -1405,7 +1608,7 @@ void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool void FtpSession::readCommand (int const events_) { -#ifndef NDS +#ifndef __NDS__ // check out-of-band data if (events_ & POLLPRI) { @@ -1426,6 +1629,8 @@ void FtpSession::readCommand (int const events_) auto const rc = m_commandSocket->read (m_commandBuffer); if (rc < 0 && errno != EWOULDBLOCK) closeCommand (); + else + m_timestamp = std::time (nullptr); return; } @@ -1438,8 +1643,13 @@ void FtpSession::readCommand (int const events_) // EWOULDBLOCK means out-of-band data is on the way if (errno != EWOULDBLOCK) closeCommand (); + else + m_timestamp = std::time (nullptr); + return; } + else + m_timestamp = std::time (nullptr); // reset the command buffer m_commandBuffer.clear (); @@ -1472,6 +1682,8 @@ void FtpSession::readCommand (int const events_) return; } + m_timestamp = std::time (nullptr); + if (m_urgent) { // look for telnet data mark @@ -1519,12 +1731,10 @@ void FtpSession::readCommand (int const events_) auto const it = std::lower_bound (std::begin (handlers), std::end (handlers), command, - [] (auto const &lhs_, auto const &rhs_) { - return ::strcasecmp (lhs_.first.data (), rhs_) < 0; - }); + [] (auto const &lhs_, auto const &rhs_) { return compare (lhs_.first, rhs_) < 0; }); m_timestamp = std::time (nullptr); - if (it == std::end (handlers) || ::strcasecmp (it->first.data (), command) != 0) + if (it == std::end (handlers) || compare (it->first, command) != 0) { std::string response = "502 Invalid command \""; response += encodePath (command); @@ -1542,9 +1752,9 @@ void FtpSession::readCommand (int const events_) else if (m_state != State::COMMAND) { // only some commands are available during data transfer - if (::strcasecmp (command, "ABOR") != 0 && ::strcasecmp (command, "NOOP") != 0 && - ::strcasecmp (command, "PWD") != 0 && ::strcasecmp (command, "QUIT") != 0 && - ::strcasecmp (command, "STAT") != 0 && ::strcasecmp (command, "XPWD") != 0) + if (compare (command, "ABOR") != 0 && compare (command, "NOOP") != 0 && + compare (command, "PWD") != 0 && compare (command, "QUIT") != 0 && + compare (command, "STAT") != 0 && compare (command, "XPWD") != 0) { sendResponse ("503 Invalid command during transfer\r\n"); setState (State::COMMAND, true, true); @@ -1559,7 +1769,7 @@ void FtpSession::readCommand (int const events_) else { // clear rename for all commands except RNTO - if (::strcasecmp (command, "RNTO") != 0) + if (compare (command, "RNTO") != 0) m_rename.clear (); auto const handler = it->second; @@ -1580,6 +1790,8 @@ void FtpSession::writeResponse () return; } + m_timestamp = std::time (nullptr); + m_responseBuffer.coalesce (); } @@ -1626,7 +1838,10 @@ void FtpSession::sendResponse (char const *fmt_, ...) closeCommand (); } else + { + m_timestamp = std::time (nullptr); m_responseBuffer.coalesce (); + } } void FtpSession::sendResponse (std::string_view const response_) @@ -1653,7 +1868,7 @@ void FtpSession::sendResponse (std::string_view const response_) bool FtpSession::listTransfer () { // check if we sent all available data - if (m_xferBuffer.empty ()) + while (m_xferBuffer.empty ()) { m_xferBuffer.clear (); @@ -1683,7 +1898,7 @@ bool FtpSession::listTransfer () // I think we are supposed to return entries for . and .. if (std::strcmp (dent->d_name, ".") == 0 || std::strcmp (dent->d_name, "..") == 0) - return true; + continue; // just skip it // check if this was NLST if (m_xferDirMode == XferDirMode::NLST) @@ -1741,10 +1956,68 @@ bool FtpSession::listTransfer () return false; } + m_timestamp = std::time (nullptr); + // we can try to send more data return true; } +bool FtpSession::globTransfer () +{ +#if FTPD_HAS_GLOB + // check if we sent all available data + if (m_xferBuffer.empty ()) + { + m_xferBuffer.clear (); + + auto const entry = m_glob.next (); + if (!entry) + { + // we have exhausted the glob listing + sendResponse ("226 OK\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // NLST gives the whole path name + auto const path = encodePath (entry) + "\r\n"; + if (m_xferBuffer.freeSize () < path.size ()) + { + sendResponse ("501 %s\r\n", std::strerror (ENOMEM)); + setState (State::COMMAND, true, true); + return false; + } + + std::memcpy (m_xferBuffer.freeArea (), path.data (), path.size ()); + m_xferBuffer.markUsed (path.size ()); + LOCKED (m_filePosition += path.size ()); + } + + // send any pending data + auto const rc = m_dataSocket->write (m_xferBuffer); + if (rc <= 0) + { + // error sending data + if (rc < 0 && errno == EWOULDBLOCK) + return false; + + sendResponse ("426 Connection broken during transfer\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + m_timestamp = std::time (nullptr); + + // we can try to send more data + return true; +#else + /// \todo error code? + sendResponse ("451 Glob unsupported\r\n"); + setState (State::COMMAND, true, true); + return false; +#endif +} + bool FtpSession::retrieveTransfer () { if (m_xferBuffer.empty ()) @@ -1794,6 +2067,8 @@ bool FtpSession::retrieveTransfer () return false; } + m_timestamp = std::time (nullptr); + // we can try to read/send more data LOCKED (m_filePosition += rc); return true; @@ -1825,6 +2100,8 @@ bool FtpSession::storeTransfer () setState (State::COMMAND, true, true); return false; } + + m_timestamp = std::time (nullptr); } if (!m_devZero) @@ -1854,6 +2131,8 @@ bool FtpSession::storeTransfer () /////////////////////////////////////////////////////////////////////////// void FtpSession::ABOR (char const *args_) { + (void)args_; + if (m_state == State::COMMAND) { sendResponse ("225 No transfer to abort\r\n"); @@ -1868,6 +2147,8 @@ void FtpSession::ABOR (char const *args_) void FtpSession::ALLO (char const *args_) { + (void)args_; + sendResponse ("202 Superfluous command\r\n"); setState (State::COMMAND, false, false); } @@ -1887,6 +2168,8 @@ void FtpSession::APPE (char const *args_) void FtpSession::CDUP (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); if (!authorized ()) @@ -1953,6 +2236,8 @@ void FtpSession::DELE (char const *args_) } void FtpSession::FEAT (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); sendResponse ("211-\r\n" " MDTM\r\n" @@ -1972,6 +2257,8 @@ void FtpSession::FEAT (char const *args_) void FtpSession::HELP (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); sendResponse ("214-\r\n" "The following commands are recognized\r\n" @@ -1996,6 +2283,8 @@ void FtpSession::LIST (char const *args_) void FtpSession::MDTM (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); if (!authorized ()) @@ -2067,7 +2356,7 @@ void FtpSession::MODE (char const *args_) setState (State::COMMAND, false, false); // we only accept S (stream) mode - if (::strcasecmp (args_, "S") == 0) + if (compare (args_, "S") == 0) { sendResponse ("200 OK\r\n"); return; @@ -2085,12 +2374,47 @@ void FtpSession::NLST (char const *args_) return; } - // open the path in NLST mode +#if FTPD_HAS_GLOB + if (std::strchr (args_, '*')) + { + if (::chdir (m_cwd.c_str ()) != 0 || !m_glob.glob (args_)) + { + sendResponse ("501 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, false, false); + return; + } + + m_transfer = &FtpSession::globTransfer; + + if (!m_port && !m_pasv) + { + // Prior PORT or PASV required + sendResponse ("503 Bad sequence of commands\r\n"); + setState (State::COMMAND, true, true); + return; + } + + setState (State::DATA_CONNECT, false, true); + m_send = true; + + // setup connection + if (m_port && !dataConnect ()) + { + sendResponse ("425 Can't open data connection\r\n"); + setState (State::COMMAND, true, true); + } + + return; + } +#endif + xferDir (args_, XferDirMode::NLST, false); } void FtpSession::NOOP (char const *args_) { + (void)args_; + sendResponse ("200 OK\r\n"); } @@ -2099,8 +2423,8 @@ void FtpSession::OPTS (char const *args_) setState (State::COMMAND, false, false); // check UTF8 options - if (::strcasecmp (args_, "UTF8") == 0 || ::strcasecmp (args_, "UTF8 ON") == 0 || - ::strcasecmp (args_, "UTF8 NLST") == 0) + if (compare (args_, "UTF8") == 0 || compare (args_, "UTF8 ON") == 0 || + compare (args_, "UTF8 NLST") == 0) { sendResponse ("200 OK\r\n"); return; @@ -2163,7 +2487,7 @@ void FtpSession::PASS (char const *args_) std::string pass; { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif user = m_config.user (); @@ -2188,6 +2512,8 @@ void FtpSession::PASS (char const *args_) void FtpSession::PASV (char const *args_) { + (void)args_; + if (!authorized ()) { setState (State::COMMAND, false, false); @@ -2201,7 +2527,7 @@ void FtpSession::PASV (char const *args_) m_port = false; // create a socket to listen on - auto pasv = Socket::create (); + auto pasv = Socket::create (Socket::eStream); LOCKED (m_pasvSocket = std::move (pasv)); if (!m_pasvSocket) { @@ -2217,8 +2543,8 @@ void FtpSession::PASV (char const *args_) m_pasvSocket->setSendBufferSize (SOCK_BUFFERSIZE); // create an address to bind - struct sockaddr_in addr = m_commandSocket->sockName (); -#if defined(NDS) || defined(__3DS__) + sockaddr_in addr = m_commandSocket->sockName (); +#if defined(__NDS__) || defined(__3DS__) static std::uint16_t ephemeralPort = 5001; if (ephemeralPort > 10000) ephemeralPort = 5001; @@ -2301,7 +2627,7 @@ void FtpSession::PORT (char const *args_) return; } - struct sockaddr_in addr = {}; + sockaddr_in addr{}; // parse the address if (!inet_aton (addrString.data (), &addr.sin_addr)) @@ -2354,6 +2680,8 @@ void FtpSession::PORT (char const *args_) void FtpSession::PWD (char const *args_) { + (void)args_; + if (!authorized ()) { sendResponse ("530 Not logged in\r\n"); @@ -2371,6 +2699,8 @@ void FtpSession::PWD (char const *args_) void FtpSession::QUIT (char const *args_) { + (void)args_; + sendResponse ("221 Disconnecting\r\n"); closeCommand (); } @@ -2408,7 +2738,7 @@ void FtpSession::REST (char const *args_) // set the restart offset m_restartPosition = pos; - sendResponse ("200 OK\r\n"); + sendResponse ("350 OK\r\n"); } void FtpSession::RETR (char const *args_) @@ -2472,8 +2802,8 @@ void FtpSession::RNFR (char const *args_) } // make sure the path exists - struct stat st; - if (IOAbstraction::lstat (path.c_str (), &st) != 0) + stat_t st; + if (tzLStat (path.c_str (), &st) != 0) { sendResponse ("450 %s\r\n", std::strerror (errno)); return; @@ -2529,19 +2859,22 @@ void FtpSession::SITE (char const *args_) { setState (State::COMMAND, false, false); - auto const str = std::string (args_); + auto const str = std::string_view (args_); auto const pos = str.find_first_of (' '); auto const command = str.substr (0, pos); - auto const arg = pos == std::string::npos ? std::string () : str.substr (pos + 1); + auto const arg = pos == std::string::npos ? std::string_view () : str.substr (pos + 1); - if (::strcasecmp (command.c_str (), "HELP") == 0) + if (compare (command.data (), "HELP") == 0) { sendResponse ("211-\r\n" " Show this help: SITE HELP\r\n" " Set username: SITE USER \r\n" " Set password: SITE PASS \r\n" " Set port: SITE PORT \r\n" +#ifndef __NDS__ + " Set hostname: SITE HOST \r\n" +#endif #ifdef __3DS__ " Set getMTime: SITE MTIME [0|1]\r\n" #endif @@ -2556,36 +2889,36 @@ void FtpSession::SITE (char const *args_) return; } - if (::strcasecmp (command.c_str (), "USER") == 0) + if (compare (command, "USER") == 0) { { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif - m_config.setUser (arg); + m_config.setUser (std::string (arg)); } sendResponse ("200 OK\r\n"); return; } - else if (::strcasecmp (command.c_str (), "PASS") == 0) + else if (compare (command, "PASS") == 0) { { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif - m_config.setPass (arg); + m_config.setPass (std::string (arg)); } sendResponse ("200 OK\r\n"); return; } - else if (::strcasecmp (command.c_str (), "PORT") == 0) + else if (compare (command, "PORT") == 0) { bool error = false; { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif error = !m_config.setPort (arg); @@ -2600,19 +2933,29 @@ void FtpSession::SITE (char const *args_) sendResponse ("200 OK\r\n"); return; } +#ifndef __NDS__ + else if (compare (command, "HOST") == 0) + { + { + auto const lock = m_config.lockGuard (); + m_config.setHostname (std::string (arg)); + mdns::setHostname (std::string (arg)); + } + } +#endif #ifdef __3DS__ - else if (::strcasecmp (command.c_str (), "MTIME") == 0) + else if (compare (command, "MTIME") == 0) { if (arg == "0") { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif m_config.setGetMTime (false); } else if (arg == "1") { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif m_config.setGetMTime (true); @@ -2624,12 +2967,12 @@ void FtpSession::SITE (char const *args_) } } #endif - else if (::strcasecmp (command.c_str (), "SAVE") == 0) + else if (compare (command, "SAVE") == 0) { bool error; { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif error = !m_config.save (FTPDCONFIG); @@ -2667,8 +3010,8 @@ void FtpSession::SIZE (char const *args_) } // stat the path - struct stat st; - if (IOAbstraction::stat (path.c_str (), &st) != 0) + stat_t st; + if (tzStat (path.c_str (), &st) != 0) { sendResponse ("550 %s\r\n", std::strerror (errno)); return; @@ -2746,6 +3089,8 @@ void FtpSession::STOR (char const *args_) void FtpSession::STOU (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); sendResponse ("502 Command not implemented\r\n"); } @@ -2755,7 +3100,7 @@ void FtpSession::STRU (char const *args_) setState (State::COMMAND, false, false); // we only support F (no structure) mode - if (::strcasecmp (args_, "F") == 0) + if (compare (args_, "F") == 0) { sendResponse ("200 OK\r\n"); return; @@ -2766,12 +3111,16 @@ void FtpSession::STRU (char const *args_) void FtpSession::SYST (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); sendResponse ("215 UNIX Type: L8\r\n"); } void FtpSession::TYPE (char const *args_) { + (void)args_; + setState (State::COMMAND, false, false); // we always transfer in binary mode @@ -2788,7 +3137,7 @@ void FtpSession::USER (char const *args_) std::string pass; { -#ifndef NDS +#ifndef __NDS__ auto const lock = m_config.lockGuard (); #endif user = m_config.user (); diff --git a/source/log.cpp b/source/log.cpp index c6effd8..22fad3d 100644 --- a/source/log.cpp +++ b/source/log.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ #include "platform.h" -#ifndef __WIIU__ +#if !defined(__WIIU__) && !defined(CLASSIC) #include "imgui.h" #endif @@ -77,7 +77,7 @@ struct Message /// \brief Log messages std::vector s_messages; -#ifndef NDS +#ifndef __NDS__ /// \brief Log lock platform::Mutex s_lock; #endif @@ -85,7 +85,7 @@ platform::Mutex s_lock; void drawLog () { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif @@ -166,7 +166,7 @@ void drawLog () #ifndef CLASSIC std::string getLog () { -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif @@ -204,6 +204,8 @@ void debug (char const *const fmt_, ...) va_start (ap, fmt_); addLog (DEBUGLOG, fmt_, ap); va_end (ap); +#else + (void)fmt_; #endif } @@ -253,7 +255,8 @@ void addLog (LogLevel const level_, char const *const fmt_, va_list ap_) if (level_ == DEBUGLOG) return; #endif -#ifndef NDS + +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif @@ -296,7 +299,7 @@ void addLog (LogLevel const level_, std::string_view const message_) c = '?'; } -#ifndef NDS +#ifndef __NDS__ auto const lock = std::scoped_lock (s_lock); #endif #ifndef NDEBUG diff --git a/source/main.cpp b/source/main.cpp index d15545d..de489ae 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2022 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -27,13 +27,15 @@ #endif #ifndef CLASSIC +#include + #include #endif #include #include -int main (int argc_, char *argv_[]) +int main () { #ifndef CLASSIC curl_global_init (CURL_GLOBAL_ALL); @@ -58,7 +60,7 @@ int main (int argc_, char *argv_[]) auto server = FtpServer::create (); - while (platform::loop ()) + while (!server->quit () && platform::loop ()) { #ifndef NO_CONSOLE server->draw (); diff --git a/source/mdns.cpp b/source/mdns.cpp new file mode 100644 index 0000000..81311f9 --- /dev/null +++ b/source/mdns.cpp @@ -0,0 +1,597 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://datatracker.ietf.org/doc/html/rfc959) +// - RFC 3659 (https://datatracker.ietf.org/doc/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// ftpd implements mdns based on the following: +// - RFC 1035 (https://datatracker.ietf.org/doc/html/rfc1035) +// - RFC 6762 (https://datatracker.ietf.org/doc/html/rfc6762) +// +// Copyright (C) 2024 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "mdns.h" + +#include "log.h" +#include "platform.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +static_assert ( + std::endian::native == std::endian::big || std::endian::native == std::endian::little); + +static_assert (sizeof (in_addr_t) == 4); + +namespace +{ +constexpr auto MDNS_TTL = 120; + +SockAddr const s_multicastAddress{inet_addr ("224.0.0.251"), 5353}; + +platform::steady_clock::time_point s_lastAnnounce{}; +platform::steady_clock::time_point s_lastProbe{}; + +std::string s_hostname = platform::hostname (); +std::string s_hostnameLocal = s_hostname + ".local"; + +enum class State +{ + Probe1, + Probe2, + Probe3, + Announce1, + Announce2, + Complete, + Conflict, +}; + +auto s_state = State::Probe1; + +#if __has_cpp_attribute(__cpp_lib_byteswap) +template +using byteswap = std::byteswap; +#else +template +constexpr T byteswap (T const value_) noexcept +{ + static_assert (std::has_unique_object_representations_v, "T may not have padding bits"); + auto buffer = std::bit_cast> (value_); + std::ranges::reverse (buffer); + return std::bit_cast (buffer); +} +#endif + +template +constexpr T hton (T const value_) noexcept +{ + if constexpr (std::endian::native == std::endian::big) + return value_; + else + return byteswap (value_); +} + +template +constexpr T ntoh (T const value_) noexcept +{ + if constexpr (std::endian::native == std::endian::big) + return value_; + else + return byteswap (value_); +} + +template +void const *decode (void const *const buffer_, U &size_, T &out_, bool networkToHost_ = true) +{ + if (!buffer_) + return nullptr; + + if (size_ < 0 || static_cast> (size_) < sizeof (T)) + return nullptr; + + std::memcpy (&out_, buffer_, sizeof (T)); + + if (networkToHost_) + out_ = ntoh (out_); + + size_ -= sizeof (T); + return static_cast (buffer_) + sizeof (T); +} + +template +void const *decode (void const *buffer_, T &size_, std::string &out_) +{ + auto p = static_cast (buffer_); + auto const end = p + size_; + + std::string result; + result.reserve (size_); + + while (p < end && *p) + { + auto const len = *p++; + + // punt on compressed labels + if (len & 0xC0) + return nullptr; + + if (p + len >= end) + return nullptr; + + if (!result.empty ()) + result.push_back ('.'); + + result.insert (std::end (result), p, p + len); + p += len; + } + + ++p; + + out_ = std::move (result); + + size_ = end - p; + return p; +} + +template +void *encode (void *const buffer_, U &size_, T in_, bool hostToNetwork_ = true) +{ + if (!buffer_) + return nullptr; + + if (size_ < sizeof (T)) + return nullptr; + + if (hostToNetwork_) + in_ = hton (in_); + + std::memcpy (buffer_, &in_, sizeof (T)); + + size_ -= sizeof (T); + return static_cast (buffer_) + sizeof (T); +} + +template +void *encode (void *const buffer_, T &size_, std::string const &in_) +{ + // names are limited to 255 bytes + if (in_.size () > 0xFF) + return nullptr; + + auto p = static_cast (buffer_); + auto const end = p + size_; + + std::string::size_type prev = 0; + std::string::size_type pos = 0; + while (p < end && pos != std::string::npos) + { + pos = in_.find ('.', prev); + + auto const label = std::string_view (in_).substr (prev, pos); + + // labels are limited to 63 bytes + if (label.size () >= size_ || label.size () > 0x3F) + return nullptr; + + p = static_cast (encode (p, size_, label.size ())); + if (!p) + return nullptr; + + std::memcpy (p, label.data (), label.size ()); + + p += label.size (); + + if (pos != std::string::npos) + prev = pos + 1; + } + + if (p == end) + return nullptr; + + *p++ = 0; + + size_ = end - p; + return p; +} + +struct DNSHeader +{ + std::uint16_t id{}; + std::uint16_t flags{}; + std::uint16_t qdCount{}; + std::uint16_t anCount{}; + std::uint16_t nsCount{}; + std::uint16_t arCount{}; + + template + void const *decode (void const *const buffer_, T &size_) + { + auto in = ::decode (buffer_, size_, id); + in = ::decode (buffer_, size_, flags); + in = ::decode (buffer_, size_, qdCount); + in = ::decode (buffer_, size_, anCount); + in = ::decode (buffer_, size_, nsCount); + in = ::decode (buffer_, size_, arCount); + + return buffer_; + } + + template + void *encode (void *buffer_, T &size_) + { + buffer_ = ::encode (buffer_, size_, id); + buffer_ = ::encode (buffer_, size_, flags); + buffer_ = ::encode (buffer_, size_, qdCount); + buffer_ = ::encode (buffer_, size_, anCount); + buffer_ = ::encode (buffer_, size_, nsCount); + buffer_ = ::encode (buffer_, size_, arCount); + + return buffer_; + } +}; + +struct QueryRecord +{ + std::string qname{}; + std::uint16_t qtype{}; + std::uint16_t qclass{}; + + template + void const *decode (void const *buffer_, T &size_) + { + buffer_ = ::decode (buffer_, size_, qname); + buffer_ = ::decode (buffer_, size_, qtype); + buffer_ = ::decode (buffer_, size_, qclass); + + return buffer_; + } + + template + void *encode (void *buffer_, T &size_) + { + buffer_ = ::encode (buffer_, size_, qname); + buffer_ = ::encode (buffer_, size_, qtype); + buffer_ = ::encode (buffer_, size_, qclass); + + return buffer_; + } +}; + +struct ResourceRecord +{ + std::string rname{}; + std::uint16_t rtype{}; + std::uint16_t rclass{}; + std::uint32_t rttl{}; + std::uint16_t rlen{}; + std::vector rdata{}; + + template + void const *decode (void const *buffer_, T &size_) + { + buffer_ = ::decode (buffer_, size_, rname); + buffer_ = ::decode (buffer_, size_, rtype); + buffer_ = ::decode (buffer_, size_, rclass); + buffer_ = ::decode (buffer_, size_, rttl); + buffer_ = ::decode (buffer_, size_, rlen); + + return buffer_; + } + + template + void *encode (void *buffer_, T &size_) + { + if (rttl > std::numeric_limits::max ()) + return nullptr; + + buffer_ = ::encode (buffer_, size_, rname); + buffer_ = ::encode (buffer_, size_, rtype); + buffer_ = ::encode (buffer_, size_, rclass); + buffer_ = ::encode (buffer_, size_, rttl); + buffer_ = ::encode (buffer_, size_, rlen); + + if (rlen > size_) + return nullptr; + + rdata.resize (rlen); + std::memcpy (rdata.data (), buffer_, rlen); + + size_ -= rlen; + return static_cast (buffer_) + rlen; + } +}; + +void probe (Socket *const socket_, std::string const &qname_) +{ + std::vector response (65536); + auto available = response.size (); + + auto out = DNSHeader{.qdCount = 1}.encode (response.data (), available); + out = QueryRecord{.qname = qname_, .qtype = 255, .qclass = 1}.encode (out, available); + + if (!out) + return; + + info ("Probe mDNS %s\n", qname_.c_str ()); + + socket_->writeTo (response.data (), response.size () - available, s_multicastAddress); + s_lastProbe = platform::steady_clock::now (); +} + +void announce (Socket *const socket_, + SockAddr const *srcAddr_, + std::uint16_t const id_, + std::uint16_t const flags_, + QueryRecord const &record_, + SockAddr const &addr_) +{ + std::vector response (65536); + auto available = response.size (); + + // header + auto out = encode (response.data (), available, id_); + out = + encode (out, available, flags_ | (1 << 15) | (1 << 10)); // mark response/AA + out = encode (out, available, 0); + out = encode (out, available, 1); + out = encode (out, available, 0); + out = encode (out, available, 0); + + // answer section + out = encode (out, available, record_.qname); + out = encode (out, available, record_.qtype); + out = encode (out, available, record_.qclass | (1 << 15)); // mark unique/flush + out = encode (out, available, MDNS_TTL); + out = encode (out, available, sizeof (in_addr_t)); + out = encode ( + out, available, static_cast (addr_).sin_addr.s_addr, false); + + if (!out) + return; + + auto const preferUnicast = srcAddr_ && ((record_.qclass >> 15) & 0x1); + + if (preferUnicast) + { + auto const name = std::string (addr_.name ()); + info ( + "Respond mDNS %s %s to %s\n", record_.qname.c_str (), name.c_str (), srcAddr_->name ()); + socket_->writeTo (response.data (), response.size () - available, *srcAddr_); + } + + auto const now = platform::steady_clock::now (); + if (!preferUnicast || now - s_lastAnnounce > std::chrono::seconds (MDNS_TTL / 4)) + { + info ("Announce mDNS %s %s\n", record_.qname.c_str (), addr_.name ()); + socket_->writeTo (response.data (), response.size () - available, s_multicastAddress); + s_lastAnnounce = now; + } +} +} + +void mdns::setHostname (std::string hostname_) +{ + if (hostname_.empty ()) + hostname_ = platform::hostname (); + + if (s_hostname == hostname_) + return; + + s_hostname = std::move (hostname_); + s_hostnameLocal = s_hostname + ".local"; + + s_state = State::Probe1; + s_lastProbe = platform::steady_clock::now (); +} + +UniqueSocket mdns::createSocket () +{ + auto socket = Socket::create (Socket::eDatagram); + if (!socket) + return nullptr; + + if (!socket->setReuseAddress ()) + return nullptr; + + auto iface = SockAddr::AnyIPv4; + iface.setPort (s_multicastAddress.port ()); + if (!socket->bind (iface)) + return nullptr; + + if (!socket->joinMulticastGroup (s_multicastAddress, iface)) + return nullptr; + + s_state = State::Probe1; + s_lastProbe = platform::steady_clock::now (); + + return socket; +} + +void mdns::handleSocket (Socket *socket_, SockAddr const &addr_) +{ + if (!socket_) + return; + + // only support IPv4 for now + if (addr_.domain () != SockAddr::Domain::IPv4) + return; + + auto const now = platform::steady_clock::now (); + + switch (s_state) + { + case State::Probe1: + case State::Probe2: + case State::Probe3: + if (now - s_lastProbe > 250ms) + { + probe (socket_, s_hostname); + s_state = static_cast (static_cast (s_state) + 1); + } + break; + + case State::Announce1: + case State::Announce2: + if (now - s_lastAnnounce > 1s) + { + announce (socket_, + nullptr, + 0, + 0, + QueryRecord{.qname = s_hostname, .qtype = 1, .qclass = 1}, + addr_); + s_state = static_cast (static_cast (s_state) + 1); + } + + default: + break; + } + + Socket::PollInfo pollInfo{*socket_, POLLIN, 0}; + auto const rc = Socket::poll (&pollInfo, 1, 0ms); + if (rc <= 0 || !(pollInfo.revents & POLLIN)) + return; + + SockAddr srcAddr; + std::vector buffer (65536); + auto bytes = socket_->readFrom (buffer.data (), buffer.size (), srcAddr); + if (bytes <= 0) + return; + + // only support IPv4 for now + if (srcAddr.domain () != SockAddr::Domain::IPv4) + return; + + // ignore loopback + if (std::memcmp (&reinterpret_cast (srcAddr).sin_addr.s_addr, + &reinterpret_cast (addr_).sin_addr.s_addr, + sizeof (in_addr_t)) == 0) + return; + + std::uint16_t id; + std::uint16_t flags; + std::uint16_t qdCount; + std::uint16_t anCount; + std::uint16_t nsCount; + std::uint16_t arCount; + + // parse header + auto in = decode (buffer.data (), bytes, id); + in = decode (in, bytes, flags); + in = decode (in, bytes, qdCount); + in = decode (in, bytes, anCount); + in = decode (in, bytes, nsCount); + in = decode (in, bytes, arCount); + + if (!in) + return; + + auto const qr = (flags >> 15) & 0x1; + + // ill-formed on queries and responses + auto const opcode = (flags >> 11) & 0xF; + if (opcode != 0) + return; + + // ill-formed on queries + if (!qr && ((flags >> 10) & 0x1)) + return; + + // punt on truncated messages + if ((flags >> 9) & 0x1) + return; + + // ill-formed on queries + if (!qr && ((flags >> 7) & 0x1)) + return; + + // must be zero + if ((flags >> 4) & 0x7) + return; + + // ill-formed on queries and responses + if ((flags >> 0) & 0xF) + return; + + // std::vector response (65536); + // void *out = response.data (); + // auto available = response.size (); + + std::vector answers; + + bool announced = false; + for (unsigned i = 0; i < qdCount; ++i) + { + QueryRecord record; + in = record.decode (in, bytes); + + if (!in) + return; + + // only respond to queries + if (qr) + continue; + + // only accept A or ANY type + if (record.qtype != 1 && record.qtype != 255) + continue; + + // only accept IN or ANY class + if ((record.qclass & 0x7FFF) != 1 && (record.qclass & 0x7FFF) != 255) + continue; + + if (record.qname != s_hostname && record.qname != s_hostnameLocal) + continue; + + if (!announced) + { + std::vector data (sizeof (in_addr_t)); + auto n = data.size (); + encode ( + data.data (), n, static_cast (addr_).sin_addr.s_addr, false); + + answers.emplace_back (ResourceRecord{// answer + .rname = record.qname, + .rtype = 1, + .rclass = static_cast (1 | (1 << 15)), + .rttl = MDNS_TTL, + .rlen = sizeof (in_addr_t), + .rdata = std::move (data)}); + + announce (socket_, &srcAddr, id, flags, record, addr_); + announced = true; + } + } + + for (unsigned i = 0; i < anCount; ++i) + { + ResourceRecord record; + in = record.decode (in, bytes); + + if (!in) + return; + } +} diff --git a/source/posix/collate.c b/source/posix/collate.c new file mode 100644 index 0000000..cd3afae --- /dev/null +++ b/source/posix/collate.c @@ -0,0 +1,209 @@ +/*- + * Copyright (c) 1995 Alex Tatmanjants + * at Electronni Visti IA, Kiev, Ukraine. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include + +#include +#include +#include +#include +#include + +#include "collate.h" + +extern char *_PathLocale; +int __collate_load_error = 1; +int __collate_substitute_nontrivial; +char __collate_version[STR_LEN]; +u_char __collate_substitute_table[UCHAR_MAX + 1][STR_LEN]; +struct __collate_st_char_pri __collate_char_pri_table[UCHAR_MAX + 1]; +struct __collate_st_chain_pri __collate_chain_pri_table[TABLE_SIZE]; + +#define FREAD(a, b, c, d) \ + do \ + { \ + if (fread (a, b, c, d) != c) \ + { \ + fclose (d); \ + return -1; \ + } \ + } while (0) + +void __collate_err (int ex, const char *f); + +int __collate_load_tables (encoding) +char *encoding; +{ + char buf[PATH_MAX]; + FILE *fp; + int i, save_load_error; + + save_load_error = __collate_load_error; + __collate_load_error = 1; + if (!encoding) + { + __collate_load_error = save_load_error; + return -1; + } + if (!strcmp (encoding, "C") || !strcmp (encoding, "POSIX")) + return 0; + if (!_PathLocale) + { + __collate_load_error = save_load_error; + return -1; + } + /* Range checking not needed, encoding has fixed size */ + (void)strcpy (buf, _PathLocale); + (void)strcat (buf, "/"); + (void)strcat (buf, encoding); + (void)strcat (buf, "/LC_COLLATE"); + if ((fp = fopen (buf, "r")) == NULL) + { + __collate_load_error = save_load_error; + return -1; + } + FREAD (__collate_version, sizeof (__collate_version), 1, fp); + if (strcmp (__collate_version, COLLATE_VERSION) != 0) + { + fclose (fp); + return -1; + } + FREAD (__collate_substitute_table, sizeof (__collate_substitute_table), 1, fp); + FREAD (__collate_char_pri_table, sizeof (__collate_char_pri_table), 1, fp); + FREAD (__collate_chain_pri_table, sizeof (__collate_chain_pri_table), 1, fp); + fclose (fp); + __collate_load_error = 0; + + __collate_substitute_nontrivial = 0; + for (i = 0; i < UCHAR_MAX + 1; i++) + { + if (__collate_substitute_table[i][0] != i || __collate_substitute_table[i][1] != 0) + { + __collate_substitute_nontrivial = 1; + break; + } + } + + return 0; +} + +u_char *__collate_substitute (s) const u_char *s; +{ + int dest_len, len, nlen; + int delta = strlen ((const char *)s); + u_char *dest_str = NULL; + + if (s == NULL || *s == '\0') + return __collate_strdup ((u_char *)""); + delta += delta / 8; + dest_str = (u_char *)malloc (dest_len = delta); + if (dest_str == NULL) + __collate_err (EXIT_FAILURE, __FUNCTION__); + len = 0; + while (*s) + { + nlen = len + strlen ((const char *)__collate_substitute_table[*s]); + if (dest_len <= nlen) + { + dest_str = reallocf (dest_str, dest_len = nlen + delta); + if (dest_str == NULL) + __collate_err (EXIT_FAILURE, __FUNCTION__); + } + strcpy ((char *)dest_str + len, (const char *)__collate_substitute_table[*s++]); + len = nlen; + } + return dest_str; +} + +void __collate_lookup (t, len, prim, sec) const u_char *t; +int *len, *prim, *sec; +{ + struct __collate_st_chain_pri *p2; + + *len = 1; + *prim = *sec = 0; + for (p2 = __collate_chain_pri_table; p2->str[0]; p2++) + { + if (strncmp ((const char *)t, (const char *)p2->str, strlen ((const char *)p2->str)) == 0) + { + *len = strlen ((const char *)p2->str); + *prim = p2->prim; + *sec = p2->sec; + return; + } + } + *prim = __collate_char_pri_table[*t].prim; + *sec = __collate_char_pri_table[*t].sec; +} + +u_char *__collate_strdup (s) +u_char *s; +{ + u_char *t = (u_char *)strdup ((const char *)s); + + if (t == NULL) + __collate_err (EXIT_FAILURE, __FUNCTION__); + return t; +} + +void __collate_err (int ex, const char *f) +{ + const char *s; + int serrno = errno; + int dummy; + + /* Be careful to change write counts if you change the strings */ + write (STDERR_FILENO, "collate_error: ", 15); + write (STDERR_FILENO, f, strlen (f)); + write (STDERR_FILENO, ": ", 2); + s = _strerror_r (_REENT, serrno, 1, &dummy); + write (STDERR_FILENO, s, strlen (s)); + write (STDERR_FILENO, "\n", 1); + exit (ex); +} + +#ifdef COLLATE_DEBUG +void __collate_print_tables () +{ + int i; + struct __collate_st_chain_pri *p2; + + printf ("Substitute table:\n"); + for (i = 0; i < UCHAR_MAX + 1; i++) + if (i != *__collate_substitute_table[i]) + printf ("\t'%c' --> \"%s\"\n", i, __collate_substitute_table[i]); + printf ("Chain priority table:\n"); + for (p2 = __collate_chain_pri_table; p2->str[0]; p2++) + printf ("\t\"%s\" : %d %d\n\n", p2->str, p2->prim, p2->sec); + printf ("Char priority table:\n"); + for (i = 0; i < UCHAR_MAX + 1; i++) + printf ("\t'%c' : %d %d\n", + i, + __collate_char_pri_table[i].prim, + __collate_char_pri_table[i].sec); +} +#endif diff --git a/source/posix/collate.h b/source/posix/collate.h new file mode 100644 index 0000000..bbd9952 --- /dev/null +++ b/source/posix/collate.h @@ -0,0 +1,69 @@ +/*- + * Copyright (c) 1995 Alex Tatmanjants + * at Electronni Visti IA, Kiev, Ukraine. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * $FreeBSD: src/lib/libc/locale/collate.h,v 1.11 2002/03/21 22:46:54 obrien Exp $ + */ + +#ifndef _COLLATE_H_ +#define _COLLATE_H_ + +#include +#include +#include + +#define STR_LEN 10 +#define TABLE_SIZE 100 +#define COLLATE_VERSION "1.0\n" + +struct __collate_st_char_pri +{ + int prim, sec; +}; +struct __collate_st_chain_pri +{ + u_char str[STR_LEN]; + int prim, sec; +}; + +extern int __collate_load_error; +extern int __collate_substitute_nontrivial; +extern char __collate_version[STR_LEN]; +extern u_char __collate_substitute_table[UCHAR_MAX + 1][STR_LEN]; +extern struct __collate_st_char_pri __collate_char_pri_table[UCHAR_MAX + 1]; +extern struct __collate_st_chain_pri __collate_chain_pri_table[TABLE_SIZE]; + +__BEGIN_DECLS +u_char *__collate_strdup (u_char *); +u_char *__collate_substitute (const u_char *); +int __collate_load_tables (char *); +void __collate_lookup (const u_char *, int *, int *, int *); +int __collate_range_cmp (int, int); +#ifdef COLLATE_DEBUG +void __collate_print_tables (void); +#endif +__END_DECLS + +#endif /* !_COLLATE_H_ */ diff --git a/source/posix/collcmp.c b/source/posix/collcmp.c new file mode 100644 index 0000000..d28fe8b --- /dev/null +++ b/source/posix/collcmp.c @@ -0,0 +1,88 @@ +/* + * Copyright (C) 1996 by Andrey A. Chernov, Moscow, Russia. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include + +#define ASCII_COMPATIBLE_COLLATE /* see share/colldef */ + +#include "collate.h" +#include +#ifndef ASCII_COMPATIBLE_COLLATE +#include +#endif + +/* + * Compare two characters converting collate information + * into ASCII-compatible range, it allows to handle + * "[a-z]"-type ranges with national characters. + */ + +int __collate_range_cmp (c1, c2) +int c1, c2; +{ + static char s1[2], s2[2]; + int ret; +#ifndef ASCII_COMPATIBLE_COLLATE + int as1, as2, al1, al2; +#endif + + c1 &= UCHAR_MAX; + c2 &= UCHAR_MAX; + if (c1 == c2) + return (0); + +#ifndef ASCII_COMPATIBLE_COLLATE + as1 = isascii (c1); + as2 = isascii (c2); + al1 = isalpha (c1); + al2 = isalpha (c2); + + if (as1 || as2 || al1 || al2) + { + if ((as1 && as2) || (!al1 && !al2)) + return (c1 - c2); + if (al1 && !al2) + { + if (isupper (c1)) + return ('A' - c2); + else + return ('a' - c2); + } + else if (al2 && !al1) + { + if (isupper (c2)) + return (c1 - 'A'); + else + return (c1 - 'a'); + } + } +#endif + s1[0] = c1; + s2[0] = c2; + if ((ret = strcoll (s1, s2)) != 0) + return (ret); + return (c1 - c2); +} diff --git a/source/posix/glob.c b/source/posix/glob.c new file mode 100644 index 0000000..a27679b --- /dev/null +++ b/source/posix/glob.c @@ -0,0 +1,839 @@ +/* + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Guido van Rossum. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifdef __CYGWIN__ +#define _NO_GLOB /* Cygwin provides its own glob. */ +#endif + +#ifndef _NO_GLOB + +#if defined(LIBC_SCCS) && !defined(lint) +static char sccsid[] = "@(#)glob.c 8.3 (Berkeley) 10/13/93"; +#endif /* LIBC_SCCS and not lint */ +#include + +/* + * glob(3) -- a superset of the one defined in POSIX 1003.2. + * + * The [!...] convention to negate a range is supported (SysV, Posix, ksh). + * + * Optional extra services, controlled by flags not defined by POSIX: + * + * GLOB_QUOTE: + * Escaping convention: \ inhibits any special meaning the following + * character might have (except \ at end of string is retained). + * GLOB_MAGCHAR: + * Set in gl_flags if pattern contained a globbing character. + * GLOB_NOMAGIC: + * Same as GLOB_NOCHECK, but it will only append pattern if it did + * not contain any magic characters. [Used in csh style globbing] + * GLOB_ALTDIRFUNC: + * Use alternately specified directory access functions. + * GLOB_TILDE: + * expand ~user/foo to the /home/dir/of/user/foo + * GLOB_BRACE: + * expand {1,2}{a,b} to 1a 1b 2a 2b + * gl_matchc: + * Number of matches in the current invocation of glob. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collate.h" + +#define DOLLAR '$' +#define DOT '.' +#define EOS '\0' +#define LBRACKET '[' +#define NOT '!' +#define QUESTION '?' +#define QUOTE '\\' +#define RANGE '-' +#define RBRACKET ']' +#define SEP '/' +#define STAR '*' +#define TILDE '~' +#define UNDERSCORE '_' +#define LBRACE '{' +#define RBRACE '}' +#define SLASH '/' +#define COMMA ',' + +#ifndef DEBUG + +#define M_QUOTE 0x8000 +#define M_PROTECT 0x4000 +#define M_MASK 0xffff +#define M_ASCII 0x00ff + +typedef u_short Char; + +#else + +#define M_QUOTE 0x80 +#define M_PROTECT 0x40 +#define M_MASK 0xff +#define M_ASCII 0x7f + +typedef char Char; + +#endif + +#define CHAR(c) ((Char)((c)&M_ASCII)) +#define META(c) ((Char)((c) | M_QUOTE)) +#define M_ALL META ('*') +#define M_END META (']') +#define M_NOT META ('!') +#define M_ONE META ('?') +#define M_RNG META ('-') +#define M_SET META ('[') +#define ismeta(c) (((c)&M_QUOTE) != 0) + +static int compare (const void *, const void *); +static int g_Ctoc (const Char *, char *, u_int); +static int g_lstat (Char *, struct stat *, glob_t *); +static DIR *g_opendir (Char *, glob_t *); +static Char *g_strchr (Char *, int); +#ifdef notdef +static Char *g_strcat (Char *, const Char *); +#endif +static int g_stat (Char *, struct stat *, glob_t *); +static int glob0 (const Char *, glob_t *, int *); +static int glob1 (Char *, glob_t *, int *); +static int glob2 (Char *, Char *, Char *, Char *, glob_t *, int *); +static int glob3 (Char *, Char *, Char *, Char *, Char *, glob_t *, int *); +static int globextend (const Char *, glob_t *, int *); +static int globexp1 (const Char *, glob_t *, int *); +static int globexp2 (const Char *, const Char *, glob_t *, int *, int *); +static int match (Char *, Char *, Char *); +#ifdef DEBUG +static void qprintf (const char *, Char *); +#endif + +int glob (pattern, flags, errfunc, pglob) const char *__restrict pattern; +int flags, (*errfunc) (const char *, int); +glob_t *__restrict pglob; +{ + const u_char *patnext; + int c, limit; + Char *bufnext, *bufend, patbuf[MAXPATHLEN]; + + patnext = (u_char *)pattern; + if (!(flags & GLOB_APPEND)) + { + pglob->gl_pathc = 0; + pglob->gl_pathv = NULL; + if (!(flags & GLOB_DOOFFS)) + pglob->gl_offs = 0; + } + if (flags & GLOB_LIMIT) + { + limit = pglob->gl_matchc; + if (limit == 0) + limit = ARG_MAX; + } + else + limit = 0; + pglob->gl_flags = flags & ~GLOB_MAGCHAR; + pglob->gl_errfunc = errfunc; + pglob->gl_matchc = 0; + + bufnext = patbuf; + bufend = bufnext + MAXPATHLEN - 1; + if (flags & GLOB_QUOTE) + { + /* Protect the quoted characters. */ + while (bufnext < bufend && (c = *patnext++) != EOS) + if (c == QUOTE) + { + if ((c = *patnext++) == EOS) + { + c = QUOTE; + --patnext; + } + *bufnext++ = c | M_PROTECT; + } + else + *bufnext++ = c; + } + else + while (bufnext < bufend && (c = *patnext++) != EOS) + *bufnext++ = c; + *bufnext = EOS; + + if (flags & GLOB_BRACE) + return globexp1 (patbuf, pglob, &limit); + else + return glob0 (patbuf, pglob, &limit); +} + +/* + * Expand recursively a glob {} pattern. When there is no more expansion + * invoke the standard globbing routine to glob the rest of the magic + * characters + */ +static int globexp1 (pattern, pglob, limit) const Char *pattern; +glob_t *pglob; +int *limit; +{ + const Char *ptr = pattern; + int rv; + + /* Protect a single {}, for find(1), like csh */ + if (pattern[0] == LBRACE && pattern[1] == RBRACE && pattern[2] == EOS) + return glob0 (pattern, pglob, limit); + + while ((ptr = (const Char *)g_strchr ((Char *)ptr, LBRACE)) != NULL) + if (!globexp2 (ptr, pattern, pglob, &rv, limit)) + return rv; + + return glob0 (pattern, pglob, limit); +} + +/* + * Recursive brace globbing helper. Tries to expand a single brace. + * If it succeeds then it invokes globexp1 with the new pattern. + * If it fails then it tries to glob the rest of the pattern and returns. + */ +static int globexp2 (ptr, pattern, pglob, rv, limit) const Char *ptr, *pattern; +glob_t *pglob; +int *rv, *limit; +{ + int i; + Char *lm, *ls; + const Char *pe, *pm, *pl; + Char patbuf[MAXPATHLEN]; + + /* copy part up to the brace */ + for (lm = patbuf, pm = pattern; pm != ptr; *lm++ = *pm++) + continue; + *lm = EOS; + ls = lm; + + /* Find the balanced brace */ + for (i = 0, pe = ++ptr; *pe; pe++) + if (*pe == LBRACKET) + { + /* Ignore everything between [] */ + for (pm = pe++; *pe != RBRACKET && *pe != EOS; pe++) + continue; + if (*pe == EOS) + { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pe = pm; + } + } + else if (*pe == LBRACE) + i++; + else if (*pe == RBRACE) + { + if (i == 0) + break; + i--; + } + + /* Non matching braces; just glob the pattern */ + if (i != 0 || *pe == EOS) + { + *rv = glob0 (patbuf, pglob, limit); + return 0; + } + + for (i = 0, pl = pm = ptr; pm <= pe; pm++) + switch (*pm) + { + case LBRACKET: + /* Ignore everything between [] */ + for (pl = pm++; *pm != RBRACKET && *pm != EOS; pm++) + continue; + if (*pm == EOS) + { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pm = pl; + } + break; + + case LBRACE: + i++; + break; + + case RBRACE: + if (i) + { + i--; + break; + } + /* FALLTHROUGH */ + case COMMA: + if (i && *pm == COMMA) + break; + else + { + /* Append the current string */ + for (lm = ls; (pl < pm); *lm++ = *pl++) + continue; + /* + * Append the rest of the pattern after the + * closing brace + */ + for (pl = pe + 1; (*lm++ = *pl++) != EOS;) + continue; + + /* Expand the current pattern */ +#ifdef DEBUG + qprintf ("globexp2:", patbuf); +#endif + *rv = globexp1 (patbuf, pglob, limit); + + /* move after the comma, to the next string */ + pl = pm + 1; + } + break; + + default: + break; + } + *rv = 0; + return 0; +} + +/* + * The main glob() routine: compiles the pattern (optionally processing + * quotes), calls glob1() to do the real pattern matching, and finally + * sorts the list (unless unsorted operation is requested). Returns 0 + * if things went well, nonzero if errors occurred. It is not an error + * to find no matches. + */ +static int glob0 (pattern, pglob, limit) const Char *pattern; +glob_t *pglob; +int *limit; +{ + const Char *qpatnext; + int c, err, oldpathc; + Char *bufnext, patbuf[MAXPATHLEN]; + + qpatnext = pattern; + oldpathc = pglob->gl_pathc; + bufnext = patbuf; + + /* We don't need to check for buffer overflow any more. */ + while ((c = *qpatnext++) != EOS) + { + switch (c) + { + case LBRACKET: + c = *qpatnext; + if (c == NOT) + ++qpatnext; + if (*qpatnext == EOS || g_strchr ((Char *)qpatnext + 1, RBRACKET) == NULL) + { + *bufnext++ = LBRACKET; + if (c == NOT) + --qpatnext; + break; + } + *bufnext++ = M_SET; + if (c == NOT) + *bufnext++ = M_NOT; + c = *qpatnext++; + do + { + *bufnext++ = CHAR (c); + if (*qpatnext == RANGE && (c = qpatnext[1]) != RBRACKET) + { + *bufnext++ = M_RNG; + *bufnext++ = CHAR (c); + qpatnext += 2; + } + } while ((c = *qpatnext++) != RBRACKET); + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_END; + break; + case QUESTION: + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_ONE; + break; + case STAR: + pglob->gl_flags |= GLOB_MAGCHAR; + /* collapse adjacent stars to one, + * to avoid exponential behavior + */ + if (bufnext == patbuf || bufnext[-1] != M_ALL) + *bufnext++ = M_ALL; + break; + default: + *bufnext++ = CHAR (c); + break; + } + } + *bufnext = EOS; +#ifdef DEBUG + qprintf ("glob0:", patbuf); +#endif + + if ((err = glob1 (patbuf, pglob, limit)) != 0) + return (err); + + /* + * If there was no match we are going to append the pattern + * if GLOB_NOCHECK was specified or if GLOB_NOMAGIC was specified + * and the pattern did not contain any magic characters + * GLOB_NOMAGIC is there just for compatibility with csh. + */ + if (pglob->gl_pathc == oldpathc && + ((pglob->gl_flags & GLOB_NOCHECK) || + ((pglob->gl_flags & GLOB_NOMAGIC) && !(pglob->gl_flags & GLOB_MAGCHAR)))) + return (globextend (pattern, pglob, limit)); + else if (!(pglob->gl_flags & GLOB_NOSORT)) + qsort (pglob->gl_pathv + pglob->gl_offs + oldpathc, + pglob->gl_pathc - oldpathc, + sizeof (char *), + compare); + return (0); +} + +static int compare (p, q) const void *p, *q; +{ + return (strcmp (*(char **)p, *(char **)q)); +} + +static int glob1 (pattern, pglob, limit) +Char *pattern; +glob_t *pglob; +int *limit; +{ + Char pathbuf[MAXPATHLEN]; + + /* A null pathname is invalid -- POSIX 1003.1 sect. 2.4. */ + if (*pattern == EOS) + return (0); + return (glob2 (pathbuf, pathbuf, pathbuf + MAXPATHLEN - 1, pattern, pglob, limit)); +} + +/* + * The functions glob2 and glob3 are mutually recursive; there is one level + * of recursion for each segment in the pattern that contains one or more + * meta characters. + */ +static int glob2 (pathbuf, pathend, pathend_last, pattern, pglob, limit) +Char *pathbuf, *pathend, *pathend_last, *pattern; +glob_t *pglob; +int *limit; +{ + struct stat sb; + Char *p, *q; + int anymeta; + + /* + * Loop over pattern segments until end of pattern or until + * segment with meta character found. + */ + for (anymeta = 0;;) + { + if (*pattern == EOS) + { /* End of pattern? */ + *pathend = EOS; + if (g_lstat (pathbuf, &sb, pglob)) + return (0); + + if (((pglob->gl_flags & GLOB_MARK) && pathend[-1] != SEP) && + (S_ISDIR (sb.st_mode) || + (S_ISLNK (sb.st_mode) && (g_stat (pathbuf, &sb, pglob) == 0) && + S_ISDIR (sb.st_mode)))) + { + if (pathend + 1 > pathend_last) + return (1); + *pathend++ = SEP; + *pathend = EOS; + } + ++pglob->gl_matchc; + return (globextend (pathbuf, pglob, limit)); + } + + /* Find end of next segment, copy tentatively to pathend. */ + q = pathend; + p = pattern; + while (*p != EOS && *p != SEP) + { + if (ismeta (*p)) + anymeta = 1; + if (q + 1 > pathend_last) + return (1); + *q++ = *p++; + } + + if (!anymeta) + { /* No expansion, do next segment. */ + pathend = q; + pattern = p; + while (*pattern == SEP) + { + if (pathend + 1 > pathend_last) + return (1); + *pathend++ = *pattern++; + } + } + else /* Need expansion, recurse. */ + return (glob3 (pathbuf, pathend, pathend_last, pattern, p, pglob, limit)); + } + /* NOTREACHED */ +} + +static int glob3 (pathbuf, pathend, pathend_last, pattern, restpattern, pglob, limit) +Char *pathbuf, *pathend, *pathend_last, *pattern, *restpattern; +glob_t *pglob; +int *limit; +{ + struct dirent *dp; + DIR *dirp; + int err; + char buf[MAXPATHLEN]; + + /* + * The readdirfunc declaration can't be prototyped, because it is + * assigned, below, to two functions which are prototyped in glob.h + * and dirent.h as taking pointers to differently typed opaque + * structures. + */ + struct dirent *(*readdirfunc) (); + + if (pathend > pathend_last) + return (1); + *pathend = EOS; + errno = 0; + + if ((dirp = g_opendir (pathbuf, pglob)) == NULL) + { + /* TODO: don't call for ENOENT or ENOTDIR? */ + if (pglob->gl_errfunc) + { + if (g_Ctoc (pathbuf, buf, sizeof (buf))) + return (GLOB_ABEND); + if (pglob->gl_errfunc (buf, errno) || pglob->gl_flags & GLOB_ERR) + return (GLOB_ABEND); + } + return (0); + } + + err = 0; + + /* Search directory for matching names. */ + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + readdirfunc = pglob->gl_readdir; + else + readdirfunc = readdir; + while ((dp = (*readdirfunc) (dirp))) + { + u_char *sc; + Char *dc; + + /* Initial DOT must be matched literally. */ + if (dp->d_name[0] == DOT && *pattern != DOT) + continue; + dc = pathend; + sc = (u_char *)dp->d_name; + while (dc < pathend_last && (*dc++ = *sc++) != EOS) + ; + if (!match (pathend, pattern, restpattern)) + { + *pathend = EOS; + continue; + } + err = glob2 (pathbuf, --dc, pathend_last, restpattern, pglob, limit); + if (err) + break; + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + (*pglob->gl_closedir) (dirp); + else + closedir (dirp); + return (err); +} + +/* + * Extend the gl_pathv member of a glob_t structure to accomodate a new item, + * add the new item, and update gl_pathc. + * + * This assumes the BSD realloc, which only copies the block when its size + * crosses a power-of-two boundary; for v7 realloc, this would cause quadratic + * behavior. + * + * Return 0 if new item added, error code if memory couldn't be allocated. + * + * Invariant of the glob_t structure: + * Either gl_pathc is zero and gl_pathv is NULL; or gl_pathc > 0 and + * gl_pathv points to (gl_offs + gl_pathc + 1) items. + */ +static int globextend (path, pglob, limit) const Char *path; +glob_t *pglob; +int *limit; +{ + char **pathv; + int i; + u_int newsize, len; + char *copy; + const Char *p; + + if (*limit && pglob->gl_pathc > *limit) + { + errno = 0; + return (GLOB_NOSPACE); + } + + newsize = sizeof (*pathv) * (2 + pglob->gl_pathc + pglob->gl_offs); + pathv = pglob->gl_pathv ? realloc ((char *)pglob->gl_pathv, newsize) : malloc (newsize); + if (pathv == NULL) + { + if (pglob->gl_pathv) + { + free (pglob->gl_pathv); + pglob->gl_pathv = NULL; + } + return (GLOB_NOSPACE); + } + + if (pglob->gl_pathv == NULL && pglob->gl_offs > 0) + { + /* first time around -- clear initial gl_offs items */ + pathv += pglob->gl_offs; + for (i = pglob->gl_offs; --i >= 0;) + *--pathv = NULL; + } + pglob->gl_pathv = pathv; + + for (p = path; *p++;) + continue; + len = (size_t)(p - path); + if ((copy = malloc (len)) != NULL) + { + if (g_Ctoc (path, copy, len)) + { + free (copy); + return (GLOB_NOSPACE); + } + pathv[pglob->gl_offs + pglob->gl_pathc++] = copy; + } + pathv[pglob->gl_offs + pglob->gl_pathc] = NULL; + return (copy == NULL ? GLOB_NOSPACE : 0); +} + +/* + * pattern matching function for filenames. Each occurrence of the * + * pattern causes a recursion level. + */ +static int match (name, pat, patend) +Char *name, *pat, *patend; +{ + int ok, negate_range; + Char c, k; + + while (pat < patend) + { + c = *pat++; + switch (c & M_MASK) + { + case M_ALL: + if (pat == patend) + return (1); + do + if (match (name, pat, patend)) + return (1); + while (*name++ != EOS); + return (0); + case M_ONE: + if (*name++ == EOS) + return (0); + break; + case M_SET: + ok = 0; + if ((k = *name++) == EOS) + return (0); + if ((negate_range = ((*pat & M_MASK) == M_NOT)) != EOS) + ++pat; + while (((c = *pat++) & M_MASK) != M_END) + if ((*pat & M_MASK) == M_RNG) + { + if (__collate_load_error + ? CHAR (c) <= CHAR (k) && CHAR (k) <= CHAR (pat[1]) + : __collate_range_cmp (CHAR (c), CHAR (k)) <= 0 && + __collate_range_cmp (CHAR (k), CHAR (pat[1])) <= 0) + ok = 1; + pat += 2; + } + else if (c == k) + ok = 1; + if (ok == negate_range) + return (0); + break; + default: + if (*name++ != c) + return (0); + break; + } + } + return (*name == EOS); +} + +/* Free allocated data belonging to a glob_t structure. */ +void globfree (pglob) glob_t *pglob; +{ + int i; + char **pp; + + if (pglob->gl_pathv != NULL) + { + pp = pglob->gl_pathv + pglob->gl_offs; + for (i = pglob->gl_pathc; i--; ++pp) + if (*pp) + free (*pp); + free (pglob->gl_pathv); + pglob->gl_pathv = NULL; + } +} + +static DIR *g_opendir (str, pglob) +Char *str; +glob_t *pglob; +{ + char buf[MAXPATHLEN]; + + if (!*str) + strcpy (buf, "."); + else + { + if (g_Ctoc (str, buf, sizeof (buf))) + return (NULL); + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return ((*pglob->gl_opendir) (buf)); + + return (opendir (buf)); +} + +static int g_lstat (fn, sb, pglob) +Char *fn; +struct stat *sb; +glob_t *pglob; +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc (fn, buf, sizeof (buf))) + { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return ((*pglob->gl_lstat) (buf, sb)); + return (lstat (buf, sb)); +} + +static int g_stat (fn, sb, pglob) +Char *fn; +struct stat *sb; +glob_t *pglob; +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc (fn, buf, sizeof (buf))) + { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return ((*pglob->gl_stat) (buf, sb)); + return (stat (buf, sb)); +} + +static Char *g_strchr (str, ch) +Char *str; +int ch; +{ + do + { + if (*str == ch) + return (str); + } while (*str++); + return (NULL); +} + +static int g_Ctoc (str, buf, len) const Char *str; +char *buf; +u_int len; +{ + while (len--) + { + if ((*buf++ = *str++) == '\0') + return (0); + } + return (1); +} + +#ifdef DEBUG +static void qprintf (str, s) const char *str; +Char *s; +{ + Char *p; + + (void)printf ("%s:\n", str); + for (p = s; *p; p++) + (void)printf ("%c", CHAR (*p)); + (void)printf ("\n"); + for (p = s; *p; p++) + (void)printf ("%c", *p & M_PROTECT ? '"' : ' '); + (void)printf ("\n"); + for (p = s; *p; p++) + (void)printf ("%c", ismeta (*p) ? '_' : ' '); + (void)printf ("\n"); +} +#endif +#endif /* !_NO_GLOB */ diff --git a/source/sockAddr.cpp b/source/sockAddr.cpp index a21c751..c570ac3 100644 --- a/source/sockAddr.cpp +++ b/source/sockAddr.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -26,11 +26,79 @@ #include #include +#ifdef __3DS__ +static_assert (sizeof (sockaddr_storage) == 0x1c); +#endif + +namespace +{ +in_addr inaddr_any = {.s_addr = htonl (INADDR_ANY)}; + +std::strong_ordering + strongMemCompare (void const *const a_, void const *const b_, std::size_t const size_) +{ + auto const cmp = std::memcmp (a_, b_, size_); + if (cmp < 0) + return std::strong_ordering::less; + if (cmp > 0) + return std::strong_ordering::greater; + return std::strong_ordering::equal; +} +} + /////////////////////////////////////////////////////////////////////////// +SockAddr const SockAddr::AnyIPv4{inaddr_any}; + +#ifndef NO_IPV6 +SockAddr const SockAddr::AnyIPv6{in6addr_any}; +#endif + SockAddr::~SockAddr () = default; SockAddr::SockAddr () = default; +SockAddr::SockAddr (Domain const domain_) +{ + switch (domain_) + { + case Domain::IPv4: + *this = AnyIPv4; + break; + +#ifndef NO_IPV6 + case Domain::IPv6: + *this = AnyIPv6; + break; +#endif + + default: + std::abort (); + } +} + +SockAddr::SockAddr (in_addr_t const addr_, std::uint16_t const port_) + : SockAddr (in_addr{.s_addr = addr_}, port_) +{ +} + +SockAddr::SockAddr (in_addr const &addr_, std::uint16_t const port_) +{ + std::memset (&m_addr, 0, sizeof (m_addr)); + m_addr.ss_family = AF_INET; + setAddr (addr_); + setPort (port_); +} + +#ifndef NO_IPV6 +SockAddr::SockAddr (in6_addr const &addr_, std::uint16_t const port_) +{ + std::memset (&m_addr, 0, sizeof (m_addr)); + m_addr.ss_family = AF_INET6; + setAddr (addr_); + setPort (port_); +} +#endif + SockAddr::SockAddr (SockAddr const &that_) = default; SockAddr::SockAddr (SockAddr &&that_) = default; @@ -39,109 +107,237 @@ SockAddr &SockAddr::operator= (SockAddr const &that_) = default; SockAddr &SockAddr::operator= (SockAddr &&that_) = default; -SockAddr::SockAddr (struct sockaddr const &addr_) +SockAddr::SockAddr (sockaddr_in const &addr_) { - switch (addr_.sa_family) + assert (addr_.sin_family == AF_INET); + std::memcpy (&m_addr, &addr_, sizeof (sockaddr_in)); +} + +#ifndef NO_IPV6 +SockAddr::SockAddr (sockaddr_in6 const &addr_) +{ + assert (addr_.sin6_family == AF_INET6); + std::memcpy (&m_addr, &addr_, sizeof (sockaddr_in6)); +} +#endif + +SockAddr::SockAddr (sockaddr_storage const &addr_) +{ + switch (addr_.ss_family) { case AF_INET: - std::memcpy (&m_addr, &addr_, sizeof (struct sockaddr_in)); + std::memcpy (&m_addr, &addr_, sizeof (sockaddr_in)); break; #ifndef NO_IPV6 case AF_INET6: - std::memcpy (&m_addr, &addr_, sizeof (struct sockaddr_in6)); + std::memcpy (&m_addr, &addr_, sizeof (sockaddr_in6)); break; #endif default: std::abort (); - break; } } -SockAddr::SockAddr (struct sockaddr_in const &addr_) - : SockAddr (reinterpret_cast (addr_)) +SockAddr::operator sockaddr_in const & () const { assert (m_addr.ss_family == AF_INET); + return reinterpret_cast (m_addr); } -#if !defined(__3DS__) && !defined(__WIIU__) -SockAddr::SockAddr (struct sockaddr_in6 const &addr_) - : SockAddr (reinterpret_cast (addr_)) +#ifndef NO_IPV6 +SockAddr::operator sockaddr_in6 const & () const { assert (m_addr.ss_family == AF_INET6); + return reinterpret_cast (m_addr); } #endif -SockAddr::SockAddr (struct sockaddr_storage const &addr_) - : SockAddr (reinterpret_cast (addr_)) -{ -} - -SockAddr::operator struct sockaddr_in const & () const -{ - assert (m_addr.ss_family == AF_INET); - return reinterpret_cast (m_addr); -} - -#if !defined(__3DS__) && !defined(__WIIU__) -SockAddr::operator struct sockaddr_in6 const & () const -{ - assert (m_addr.ss_family == AF_INET6); - return reinterpret_cast (m_addr); -} -#endif - -SockAddr::operator struct sockaddr_storage const & () const +SockAddr::operator sockaddr_storage const & () const { return m_addr; } -SockAddr::operator struct sockaddr * () +SockAddr::operator sockaddr * () { - return reinterpret_cast (&m_addr); + return reinterpret_cast (&m_addr); } -SockAddr::operator struct sockaddr const * () const +SockAddr::operator sockaddr const * () const { - return reinterpret_cast (&m_addr); + return reinterpret_cast (&m_addr); } -bool SockAddr::setPort (std::uint16_t const port_) +bool SockAddr::operator== (SockAddr const &that_) const { + if (m_addr.ss_family != that_.m_addr.ss_family) + return false; + switch (m_addr.ss_family) { case AF_INET: - reinterpret_cast (&m_addr)->sin_port = htons (port_); - return true; + if (port () != that_.port ()) + return false; + + // ignore sin_zero + return static_cast (*this).sin_addr.s_addr == + static_cast (that_).sin_addr.s_addr; #ifndef NO_IPV6 case AF_INET6: - reinterpret_cast (&m_addr)->sin6_port = htons (port_); - return true; + return std::memcmp (&m_addr, &that_.m_addr, sizeof (sockaddr_in6)) == 0; #endif default: std::abort (); - break; } } +std::strong_ordering SockAddr::operator<=> (SockAddr const &that_) const +{ + if (m_addr.ss_family != that_.m_addr.ss_family) + return m_addr.ss_family <=> that_.m_addr.ss_family; + + switch (m_addr.ss_family) + { + case AF_INET: + { + auto const cmp = + strongMemCompare (&static_cast (*this).sin_addr.s_addr, + &static_cast (that_).sin_addr.s_addr, + sizeof (in_addr_t)); + + if (cmp != std::strong_ordering::equal) + return cmp; + + return port () <=> that_.port (); + } + +#ifndef NO_IPV6 + case AF_INET6: + { + auto const &addr1 = static_cast (*this); + auto const &addr2 = static_cast (that_); + + if (auto const cmp = + strongMemCompare (&addr1.sin6_addr, &addr2.sin6_addr, sizeof (in6_addr)); + cmp != std::strong_ordering::equal) + return cmp; + + auto const p1 = port (); + auto const p2 = that_.port (); + + if (p1 < p2) + return std::strong_ordering::less; + else if (p1 > p2) + return std::strong_ordering::greater; + + if (auto const cmp = strongMemCompare ( + &addr1.sin6_flowinfo, &addr2.sin6_flowinfo, sizeof (std::uint32_t)); + cmp != std::strong_ordering::equal) + return cmp; + + return strongMemCompare ( + &addr1.sin6_flowinfo, &addr2.sin6_flowinfo, sizeof (std::uint32_t)); + } +#endif + + default: + std::abort (); + } +} + +void SockAddr::setAddr (in_addr_t const addr_) +{ + setAddr (in_addr{.s_addr = addr_}); +} + +void SockAddr::setAddr (in_addr const &addr_) +{ + if (m_addr.ss_family != AF_INET) + std::abort (); + + std::memcpy (&reinterpret_cast (m_addr).sin_addr, &addr_, sizeof (addr_)); + ; +} + +#ifndef NO_IPV6 +void SockAddr::setAddr (in6_addr const &addr_) +{ + if (m_addr.ss_family != AF_INET6) + std::abort (); + + std::memcpy (&reinterpret_cast (m_addr).sin6_addr, &addr_, sizeof (addr_)); + ; +} +#endif + std::uint16_t SockAddr::port () const { switch (m_addr.ss_family) { case AF_INET: - return ntohs (reinterpret_cast (&m_addr)->sin_port); + return ntohs (reinterpret_cast (&m_addr)->sin_port); #ifndef NO_IPV6 case AF_INET6: - return ntohs (reinterpret_cast (&m_addr)->sin6_port); + return ntohs (reinterpret_cast (&m_addr)->sin6_port); #endif default: std::abort (); + } +} + +void SockAddr::setPort (std::uint16_t const port_) +{ + switch (m_addr.ss_family) + { + case AF_INET: + reinterpret_cast (&m_addr)->sin_port = htons (port_); break; + +#ifndef NO_IPV6 + case AF_INET6: + reinterpret_cast (&m_addr)->sin6_port = htons (port_); + break; +#endif + + default: + std::abort (); + } +} + +SockAddr::Domain SockAddr::domain () const +{ + switch (m_addr.ss_family) + { + case AF_INET: +#ifndef NO_IPV6 + case AF_INET6: +#endif + return static_cast (m_addr.ss_family); + + default: + std::abort (); + } +} + +socklen_t SockAddr::size () const +{ + switch (m_addr.ss_family) + { + case AF_INET: + return sizeof (sockaddr_in); + +#ifndef NO_IPV6 + case AF_INET6: + return sizeof (sockaddr_in6); +#endif + + default: + std::abort (); } } @@ -150,33 +346,30 @@ char const *SockAddr::name (char *buffer_, std::size_t size_) const switch (m_addr.ss_family) { case AF_INET: -#ifdef NDS - return inet_ntoa (reinterpret_cast (&m_addr)->sin_addr); +#ifdef __NDS__ + (void)buffer_; + (void)size_; + return inet_ntoa (reinterpret_cast (&m_addr)->sin_addr); #else - return inet_ntop (AF_INET, - &reinterpret_cast (&m_addr)->sin_addr, - buffer_, - size_); + return inet_ntop ( + AF_INET, &reinterpret_cast (&m_addr)->sin_addr, buffer_, size_); #endif #ifndef NO_IPV6 case AF_INET6: - return inet_ntop (AF_INET6, - &reinterpret_cast (&m_addr)->sin6_addr, - buffer_, - size_); + return inet_ntop ( + AF_INET6, &reinterpret_cast (&m_addr)->sin6_addr, buffer_, size_); #endif default: std::abort (); - break; } } char const *SockAddr::name () const { -#if defined(NDS) || defined(__WIIU__) - return inet_ntoa (reinterpret_cast (&m_addr)->sin_addr); +#if defined(__NDS__) || defined(__WIIU__) + return inet_ntoa (reinterpret_cast (&m_addr)->sin_addr); #else #ifdef NO_IPV6 thread_local static char buffer[INET_ADDRSTRLEN]; diff --git a/source/socket.cpp b/source/socket.cpp index ed237d4..da78c6e 100644 --- a/source/socket.cpp +++ b/source/socket.cpp @@ -3,7 +3,7 @@ // - RFC 3659 (https://tools.ietf.org/html/rfc3659) // - suggested implementation details from https://cr.yp.to/ftp/filesystem.html // -// Copyright (C) 2020 Michael Theall +// Copyright (C) 2024 Michael Theall // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -43,7 +44,7 @@ Socket::~Socket () if (m_connected) info ("Closing connection to [%s]:%u\n", m_peerName.name (), m_peerName.port ()); -#ifdef NDS +#ifdef __NDS__ if (::closesocket (m_fd) != 0) error ("closesocket: %s\n", std::strerror (errno)); #else @@ -68,7 +69,7 @@ Socket::Socket (int const fd_, SockAddr const &sockName_, SockAddr const &peerNa UniqueSocket Socket::accept () { SockAddr addr; - socklen_t addrLen = sizeof (struct sockaddr_storage); + socklen_t addrLen = sizeof (sockaddr_storage); auto const fd = ::accept (m_fd, addr, &addrLen); if (fd < 0) @@ -83,7 +84,7 @@ UniqueSocket Socket::accept () int Socket::atMark () { -#ifdef NDS +#ifdef __NDS__ errno = ENOSYS; return -1; #else @@ -98,38 +99,16 @@ int Socket::atMark () bool Socket::bind (SockAddr const &addr_) { - switch (static_cast (addr_).ss_family) + if (::bind (m_fd, addr_, addr_.size ()) != 0) { - case AF_INET: - if (::bind (m_fd, addr_, sizeof (struct sockaddr_in)) != 0) - { - platform::Thread::sleep (5000ms); - error ("bind: %s\n", std::strerror (errno)); - return false; - } - break; - -#ifndef NO_IPV6 - case AF_INET6: - if (::bind (m_fd, addr_, sizeof (struct sockaddr_in6)) != 0) - { - error ("bind: %s\n", std::strerror (errno)); - platform::Thread::sleep (5000ms); - return false; - } - break; -#endif - - default: - errno = EINVAL; error ("bind: %s\n", std::strerror (errno)); - break; + return false; } if (addr_.port () == 0) { // get socket name due to request for ephemeral port - socklen_t addrLen = sizeof (struct sockaddr_storage); + socklen_t addrLen = sizeof (sockaddr_storage); if (::getsockname (m_fd, m_sockName, &addrLen) != 0) error ("getsockname: %s\n", std::strerror (errno)); } @@ -141,7 +120,7 @@ bool Socket::bind (SockAddr const &addr_) bool Socket::connect (SockAddr const &addr_) { - if (::connect (m_fd, addr_, sizeof (struct sockaddr_storage)) != 0) + if (::connect (m_fd, addr_, addr_.size ()) != 0) { if (errno != EINPROGRESS) error ("connect: %s\n", std::strerror (errno)); @@ -185,11 +164,13 @@ bool Socket::shutdown (int const how_) bool Socket::setLinger (bool const enable_, std::chrono::seconds const time_) { -#ifdef NDS +#ifdef __NDS__ + (void)enable_; + (void)time_; errno = ENOSYS; return -1; #else - struct linger linger; + linger linger; linger.l_onoff = enable_; linger.l_linger = time_.count (); @@ -209,7 +190,7 @@ bool Socket::setLinger (bool const enable_, std::chrono::seconds const time_) bool Socket::setNonBlocking (bool const nonBlocking_) { -#ifdef NDS +#ifdef __NDS__ unsigned long enable = nonBlocking_; auto const rc = ::ioctl (m_fd, FIONBIO, &enable); @@ -290,6 +271,38 @@ bool Socket::setSendBufferSize (std::size_t const size_) return true; } +#ifndef __NDS__ +bool Socket::joinMulticastGroup (SockAddr const &addr_, SockAddr const &iface_) +{ + ip_mreq group; + group.imr_multiaddr = static_cast (addr_).sin_addr; + group.imr_interface = static_cast (iface_).sin_addr; + + if (::setsockopt (m_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group)) != 0) + { + error ("setsockopt(IP_ADD_MEMBERSHIP, %s): %s\n", addr_.name (), std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::dropMulticastGroup (SockAddr const &addr_, SockAddr const &iface_) +{ + ip_mreq group; + group.imr_multiaddr = static_cast (addr_).sin_addr; + group.imr_interface = static_cast (iface_).sin_addr; + + if (::setsockopt (m_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &group, sizeof (group)) != 0) + { + error ("setsockopt(IP_DROP_MEMBERSHIP, %s): %s\n", addr_.name (), std::strerror (errno)); + return false; + } + + return true; +} +#endif + std::make_signed_t Socket::read (void *const buffer_, std::size_t const size_, bool const oob_) { @@ -314,6 +327,21 @@ std::make_signed_t Socket::read (IOBuffer &buffer_, bool const oob_ return rc; } +std::make_signed_t + Socket::readFrom (void *const buffer_, std::size_t const size_, SockAddr &addr_) +{ + assert (buffer_); + assert (size_); + + socklen_t addrLen = sizeof (sockaddr_storage); + + auto const rc = ::recvfrom (m_fd, buffer_, size_, 0, addr_, &addrLen); + if (rc < 0 && errno != EWOULDBLOCK) + error ("recvfrom: %s\n", std::strerror (errno)); + + return rc; +} + std::make_signed_t Socket::write (void const *const buffer_, std::size_t const size_) { assert (buffer_); @@ -337,6 +365,19 @@ std::make_signed_t Socket::write (IOBuffer &buffer_) return rc; } +std::make_signed_t + Socket::writeTo (void const *buffer_, std::size_t size_, SockAddr const &addr_) +{ + assert (buffer_); + assert (size_ > 0); + + auto const rc = ::sendto (m_fd, buffer_, size_, 0, addr_, addr_.size ()); + if (rc < 0 && errno != EWOULDBLOCK) + error ("sendto: %s\n", std::strerror (errno)); + + return rc; +} + SockAddr const &Socket::sockName () const { return m_sockName; @@ -347,9 +388,9 @@ SockAddr const &Socket::peerName () const return m_peerName; } -UniqueSocket Socket::create () +UniqueSocket Socket::create (Type const type_) { - auto const fd = ::socket (AF_INET, SOCK_STREAM, 0); + auto const fd = ::socket (AF_INET, static_cast (type_), 0); if (fd < 0) { error ("socket: %s\n", std::strerror (errno)); @@ -366,7 +407,7 @@ int Socket::poll (PollInfo *const info_, if (count_ == 0) return 0; - auto const pfd = std::make_unique (count_); + auto const pfd = std::make_unique (count_); for (std::size_t i = 0; i < count_; ++i) { pfd[i].fd = info_[i].socket.get ().m_fd; @@ -387,8 +428,8 @@ int Socket::poll (PollInfo *const info_, return rc; } -#ifdef NDS -extern "C" int poll (struct pollfd *const fds_, nfds_t const nfds_, int const timeout_) +#ifdef __NDS__ +extern "C" int poll (pollfd *const fds_, nfds_t const nfds_, int const timeout_) { fd_set readFds; fd_set writeFds; @@ -406,7 +447,7 @@ extern "C" int poll (struct pollfd *const fds_, nfds_t const nfds_, int const ti FD_SET (fds_[i].fd, &writeFds); } - struct timeval tv; + timeval tv; tv.tv_sec = timeout_ / 1000; tv.tv_usec = (timeout_ % 1000) * 1000; auto const rc = ::select (nfds_, &readFds, &writeFds, &exceptFds, &tv); diff --git a/source/wiiu/platform.cpp b/source/wiiu/platform.cpp index 856cad8..b9323a1 100644 --- a/source/wiiu/platform.cpp +++ b/source/wiiu/platform.cpp @@ -459,6 +459,12 @@ void platform::Thread::sleep (std::chrono::milliseconds const timeout_) std::this_thread::sleep_for (timeout_); } +std::string const &platform::hostname () +{ + static std::string const hostname = "wiiu-ftpd"; + return hostname; +} + /////////////////////////////////////////////////////////////////////////// #define USE_STD_MUTEX 1