diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 02:20:31 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 02:20:31 +0800 |
| commit | 98e88c01eb0e6404d1c5f49a22165d0861e6b924 (patch) | |
| tree | d016d445fe476f747eea9030896214adeaacf51f /include | |
| parent | 44ad3c8b36540c6b0b3144a21ac40310e163bb19 (diff) | |
| download | rwebbrowser-98e88c01eb0e6404d1c5f49a22165d0861e6b924.tar.gz rwebbrowser-98e88c01eb0e6404d1c5f49a22165d0861e6b924.tar.xz | |
feat: implement basic HTTP client with socket and address networking utilities
Diffstat (limited to 'include')
| -rw-r--r-- | include/ricochet/net/http_client.hh | 59 | ||||
| -rw-r--r-- | include/ricochet/net/sys/address.hh | 88 | ||||
| -rw-r--r-- | include/ricochet/net/sys/exception.hh | 80 | ||||
| -rw-r--r-- | include/ricochet/net/sys/file_descriptor.hh | 141 | ||||
| -rw-r--r-- | include/ricochet/net/sys/socket.hh | 160 | ||||
| -rw-r--r-- | include/ricochet/net/sys/string_view_range.hh | 18 |
6 files changed, 546 insertions, 0 deletions
diff --git a/include/ricochet/net/http_client.hh b/include/ricochet/net/http_client.hh new file mode 100644 index 0000000..2dd7419 --- /dev/null +++ b/include/ricochet/net/http_client.hh @@ -0,0 +1,59 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <expected> +#include <string> +#include <string_view> +#include <unordered_map> + +namespace ricochet::net { +/** + * @brief Encapsulate HTTP response result + */ + +struct HttpResponse +{ + int status_code { 0 }; + std::unordered_map<std::string, std::string> headers {}; + std::string body {}; +}; + +class HttpClient +{ +public: + HttpClient() = default; + ~HttpClient() = default; + + HttpClient( const HttpClient& ) = delete; + HttpClient& operator=( const HttpClient& ) = delete; + HttpClient( HttpClient&& ) = delete; + HttpClient& operator=( HttpClient&& ) = delete; + + /** + * @brief Initiates an HTTP GET request (only supports http:// in MVP stage) + * @param url Target URL, for example "http://example.com" + * @return Returns HttpResponse on success, returns an error message string on failure + */ + std::expected<HttpResponse, std::string> fetch( std::string_view url ) const; + +private: +}; + +} // namespace ricochet::net
\ No newline at end of file diff --git a/include/ricochet/net/sys/address.hh b/include/ricochet/net/sys/address.hh new file mode 100644 index 0000000..39c9553 --- /dev/null +++ b/include/ricochet/net/sys/address.hh @@ -0,0 +1,88 @@ +/** + * --------------------------------------------------------------------------- + * @notice This file was adapted from the Minnow networking framework, + * originally developed for Stanford University's CS144 course. + * It has been incorporated into the Ricochet project as foundational + * system utility code. + * --------------------------------------------------------------------------- + */ + +#pragma once + +#include <cstddef> +#include <cstdint> +#include <netdb.h> +#include <string> +#include <sys/socket.h> +#include <utility> + +//! Wrapper around [IPv4 addresses](@ref man7::ip) and DNS operations. +class Address +{ +public: + //! \brief Wrapper around [sockaddr_storage](@ref man7::socket). + //! \details A `sockaddr_storage` is enough space to store any socket address (IPv4 or IPv6). + class Raw + { + public: + sockaddr_storage storage {}; //!< The wrapped struct itself. + // NOLINTBEGIN (*-explicit-*) + operator sockaddr*(); + operator const sockaddr*() const; + // NOLINTEND (*-explicit-*) + }; + +private: + socklen_t size_; //!< Size of the wrapped address. + Raw address_ {}; //!< A wrapped [sockaddr_storage](@ref man7::socket) containing the address. + + //! Constructor from ip/host, service/port, and hints to the resolver. + Address( const std::string& node, const std::string& service, const addrinfo& hints ); + +public: + //! Construct by resolving a hostname and servicename. + Address( const std::string& hostname, const std::string& service ); + + //! Construct from dotted-quad string ("18.243.0.1") and numeric port. + explicit Address( const std::string& ip, std::uint16_t port = 0 ); + + //! Construct from a [sockaddr *](@ref man7::socket). + Address( const sockaddr* addr, std::size_t size ); + + //! Default constructor + Address() : Address( nullptr, 0 ) {} + + //! Equality comparison. + bool operator==( const Address& other ) const; + bool operator!=( const Address& other ) const { return not operator==( other ); } + + //! \name Conversions + //!@{ + + //! Dotted-quad IP address string ("18.243.0.1") and numeric port. + std::pair<std::string, uint16_t> ip_port() const; + //! Dotted-quad IP address string ("18.243.0.1"). + std::string ip() const { return ip_port().first; } + //! Numeric port (host byte order). + uint16_t port() const { return ip_port().second; } + //! Numeric IP address as an integer (i.e., in [host byte order](\ref man3::byteorder)). + uint32_t ipv4_numeric() const; + //! Create an Address from a 32-bit raw numeric IP address + static Address from_ipv4_numeric( uint32_t ip_address ); + //! Human-readable string, e.g., "8.8.8.8:53". + std::string to_string() const; + //!@} + + //! \name Low-level operations + //!@{ + + //! Size of the underlying address storage. + socklen_t size() const { return size_; } + //! Const pointer to the underlying socket address storage. + const sockaddr* raw() const { return static_cast<const sockaddr*>( address_ ); } + //! Safely convert to underlying sockaddr type + template<typename sockaddr_type> + const sockaddr_type* as() const; + + //!@} +}; diff --git a/include/ricochet/net/sys/exception.hh b/include/ricochet/net/sys/exception.hh new file mode 100644 index 0000000..9587e5c --- /dev/null +++ b/include/ricochet/net/sys/exception.hh @@ -0,0 +1,80 @@ +/** + * --------------------------------------------------------------------------- + * @notice This file was adapted from the Minnow networking framework, + * originally developed for Stanford University's CS144 course. + * It has been incorporated into the Ricochet project as foundational + * system utility code. + * --------------------------------------------------------------------------- + */ + +#pragma once + +#include <cxxabi.h> +#include <memory> +#include <string> +#include <string_view> +#include <system_error> + +class tagged_error : public std::system_error +{ +private: + std::string attempt_and_error_; + int error_code_; + +public: + tagged_error( const std::error_category& category, const std::string_view s_attempt, const int error_code ) + : system_error( error_code, category ) + , attempt_and_error_( std::string( s_attempt ) + ": " + std::system_error::what() ) + , error_code_( error_code ) + {} + + const char* what() const noexcept override { return attempt_and_error_.c_str(); } + + int error_code() const { return error_code_; } +}; + +class unix_error : public tagged_error +{ +public: + explicit unix_error( const std::string_view s_attempt, const int s_errno = errno ) + : tagged_error( std::system_category(), s_attempt, s_errno ) + {} +}; + +inline int CheckSystemCall( const std::string_view s_attempt, const int return_value ) +{ + if ( return_value >= 0 ) { + return return_value; + } + + throw unix_error { s_attempt }; +} + +template<typename T> +inline T* notnull( const std::string_view context, T* const x ) +{ + return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" ); +} + +template<typename T> +inline std::unique_ptr<T> notnull( const std::string_view context, std::unique_ptr<T> x ) +{ + return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" ); +} + +template<typename T> +inline std::shared_ptr<T> notnull( const std::string_view context, std::shared_ptr<T>&& x ) +{ + return x ? std::move( x ) : throw std::runtime_error( std::string( context ) + ": returned null pointer" ); +} + +inline std::string demangle( const char* name ) +{ + int status {}; + const std::unique_ptr<char, decltype( &free )> res { abi::__cxa_demangle( name, nullptr, nullptr, &status ), + free }; + if ( status ) { + throw std::runtime_error( "cxa_demangle" ); + } + return res.get(); +} diff --git a/include/ricochet/net/sys/file_descriptor.hh b/include/ricochet/net/sys/file_descriptor.hh new file mode 100644 index 0000000..4af56cb --- /dev/null +++ b/include/ricochet/net/sys/file_descriptor.hh @@ -0,0 +1,141 @@ +/** + * --------------------------------------------------------------------------- + * @notice This file was adapted from the Minnow networking framework, + * originally developed for Stanford University's CS144 course. + * It has been incorporated into the Ricochet project as foundational + * system utility code. + * --------------------------------------------------------------------------- + */ + +#pragma once + +#include "string_view_range.hh" + +#include <bits/types/struct_iovec.h> +#include <cstddef> +#include <memory> +#include <vector> + +// A reference-counted handle to a file descriptor +class FileDescriptor +{ +public: + // Construct from a file descriptor number returned by the kernel + explicit FileDescriptor( int fd ); + + // Read from the file descriptor into `buffer` (a single string) or `buffers` (a vector of strings) + void read( std::string& buffer ); + void read( std::vector<std::string>& buffers ); + + // `write_all` writes a buffer completely. + void write_all( std::string_view buffer ); + + // `write` writes *from* a buffer or range of buffers and returns the number of bytes it actually wrote. + size_t write( std::string_view buffer ); + size_t write( const StringViewRange auto&& buffers ) + { + static thread_local std::vector<iovec> iovecs; + const size_t total_size = to_iovecs( buffers, iovecs ); + return write( iovecs, total_size ); + } + + // Close the underlying file descriptor + void close() { internal_fd_->close(); } + + // Set blocking(true) or non-blocking(false) status on the file descriptor + void set_blocking( bool blocking ); + + // Copy a FileDescriptor explicitly, increasing the internal reference count + FileDescriptor duplicate() const; + + // The destructor calls close() when no more references remain. + ~FileDescriptor() = default; + + // Accessors + int fd_num() const { return internal_fd_->fd_; } // underlying descriptor number + bool eof() const { return internal_fd_->eof_; } // EOF flag state + bool closed() const { return internal_fd_->closed_; } // closed flag state + bool blocking() const { return not internal_fd_->non_blocking_; } // blocking state + unsigned int read_count() const { return internal_fd_->read_count_; } // number of reads + unsigned int write_count() const { return internal_fd_->write_count_; } // number of writes + + // Copy/move constructor/assignment operators + // FileDescriptor can be moved, but cannot be copied implicitly (see duplicate()) + FileDescriptor( const FileDescriptor& other ) = delete; // copy construction is forbidden + FileDescriptor& operator=( const FileDescriptor& other ) = delete; // copy assignment is forbidden + FileDescriptor( FileDescriptor&& other ) = default; // move construction is allowed + FileDescriptor& operator=( FileDescriptor&& other ) = default; // move assignment is allowed + +private: + // FDWrapper: A handle on a kernel file descriptor. + // FileDescriptor objects contain a std::shared_ptr to a FDWrapper. + class FDWrapper + { + public: + int fd_; // The file descriptor number returned by the kernel + bool eof_ = false; // Flag indicating whether FDWrapper::fd_ is at EOF + bool closed_ = false; // Flag indicating whether FDWrapper::fd_ has been closed + bool non_blocking_ = false; // Flag indicating whether FDWrapper::fd_ is non-blocking + unsigned read_count_ = 0; // The number of times FDWrapper::fd_ has been read + unsigned write_count_ = 0; // The numberof times FDWrapper::fd_ has been written + + // Construct from a file descriptor number returned by the kernel + explicit FDWrapper( int fd ); + // Closes the file descriptor upon destruction + ~FDWrapper(); + // Calls [close(2)](\ref man2::close) on FDWrapper::fd_ + void close(); + + size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const; + size_t CheckRead( std::string_view what, ssize_t return_value ); + + // An FDWrapper cannot be copied or moved + FDWrapper( const FDWrapper& other ) = delete; + FDWrapper& operator=( const FDWrapper& other ) = delete; + FDWrapper( FDWrapper&& other ) = delete; + FDWrapper& operator=( FDWrapper&& other ) = delete; + }; + + // A reference-counted handle to a shared FDWrapper + std::shared_ptr<FDWrapper> internal_fd_; + + // Private constructor used to duplicate the FileDescriptor (increase the reference count) + explicit FileDescriptor( std::shared_ptr<FDWrapper> other_shared_ptr ); + + // Private write method that takes a vector of iovecs (an internal OS structure) + size_t write( const std::vector<iovec>& iovecs, size_t total_size ); + +protected: + // size of buffer to allocate by read(), recv(), etc., when passed-in buffer is empty + static constexpr size_t kReadBufferSize = 16384; + + void set_eof() { internal_fd_->eof_ = true; } + void register_read() { ++internal_fd_->read_count_; } // increment read count + void register_write() { ++internal_fd_->write_count_; } // increment write count + + size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const; + size_t CheckRead( std::string_view what, ssize_t return_value ); + + // Convert a range of string_view-convertible objects to a vector of iovecs + static size_t to_iovecs( const StringViewRange auto& buffers, std::vector<iovec>& iovecs ) + { + if ( buffers.empty() ) { + throw std::runtime_error( "to_iovecs called with empty buffer list" ); + } + iovecs.clear(); + iovecs.reserve( buffers.size() ); + size_t total_size = 0; + for ( const auto& buf : buffers ) { + const std::string_view x { buf }; + if ( x.empty() ) { + throw std::runtime_error( "to_iovecs called with empty buffer in buffer list" ); + } + iovecs.push_back( { const_cast<char*>( x.data() ), x.size() } ); // NOLINT(*-const-cast) + total_size += x.size(); + } + if ( total_size == 0 ) { + throw std::runtime_error( "to_iovecs called with zero-size buffer list" ); + } + return total_size; + } +}; diff --git a/include/ricochet/net/sys/socket.hh b/include/ricochet/net/sys/socket.hh new file mode 100644 index 0000000..f84be26 --- /dev/null +++ b/include/ricochet/net/sys/socket.hh @@ -0,0 +1,160 @@ +/** + * --------------------------------------------------------------------------- + * @notice This file was adapted from the Minnow networking framework, + * originally developed for Stanford University's CS144 course. + * It has been incorporated into the Ricochet project as foundational + * system utility code. + * --------------------------------------------------------------------------- + */ + +#pragma once + +#include "address.hh" +#include "file_descriptor.hh" + +#include <functional> +#include <sys/socket.h> + +//! \brief Base class for network sockets (TCP, UDP, etc.) +//! \details Socket is generally used via a subclass. See TCPSocket and UDPSocket for usage examples. +class Socket : public FileDescriptor +{ +public: + //! Bind a socket to a specified address with [bind(2)](\ref man2::bind), usually for listen/accept + void bind( const Address& address ); + + //! Bind a socket to a specified device + void bind_to_device( std::string_view device_name ); + + //! Connect a socket to a specified peer address with [connect(2)](\ref man2::connect) + void connect( const Address& address ); + + //! Shut down a socket via [shutdown(2)](\ref man2::shutdown) + void shutdown( int how ); + + //! Get local address of socket with [getsockname(2)](\ref man2::getsockname) + Address local_address() const; + //! Get peer address of socket with [getpeername(2)](\ref man2::getpeername) + Address peer_address() const; + + //! Allow local address to be reused sooner via [SO_REUSEADDR](\ref man7::socket) + void set_reuseaddr(); + + //! Check for errors (will be seen on non-blocking sockets) + void throw_if_error() const; + +private: + //! Get the local or peer address the socket is connected to + Address get_address( const std::string& name_of_function, + const std::function<int( int, sockaddr*, socklen_t* )>& function ) const; + +protected: + //! Construct via [socket(2)](\ref man2::socket) + Socket( int domain, int type, int protocol = 0 ); + + //! Construct from a file descriptor. + Socket( FileDescriptor&& fd, int domain, int type, int protocol = 0 ); + + //! Wrapper around [getsockopt(2)](\ref man2::getsockopt) + template<typename option_type> + socklen_t getsockopt( int level, int option, option_type& option_value ) const; + + //! Wrappers around [setsockopt(2)](\ref man2::setsockopt) + template<typename option_type> + void setsockopt( int level, int option, const option_type& option_value ); + + void setsockopt( int level, int option, std::string_view option_val ); +}; + +class DatagramSocket : public Socket +{ +public: + // Receive a datagram and the Address of its sender, into a buffer or collection of buffers. + void recv( Address& source_address, std::string& payload ); + void recv( Address& source_address, std::vector<std::string>& payloads ); + + //! Send a datagram to specified Address (if destination is missing, sends to already-connected address) + void send( std::string_view payload, const std::optional<Address>& destination = {} ); + void send( const StringViewRange auto&& payloads, const std::optional<Address>& destination = {} ) + { + static thread_local std::vector<iovec> iovecs; + const size_t total_size = to_iovecs( payloads, iovecs ); + send( iovecs, total_size, destination ); + } + +private: + void send( std::vector<iovec>& iovecs, size_t total_size, const std::optional<Address>& destination = {} ); + +protected: + DatagramSocket( int domain, int type, int protocol = 0 ) : Socket( domain, type, protocol ) {} + + //! Construct from a file descriptor. + DatagramSocket( FileDescriptor&& fd, int domain, int type, int protocol = 0 ) + : Socket( std::move( fd ), domain, type, protocol ) + {} +}; + +//! A wrapper around [UDP sockets](\ref man7::udp) +class UDPSocket : public DatagramSocket +{ + //! \param[in] fd is the FileDescriptor from which to construct + explicit UDPSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_INET, SOCK_DGRAM ) {} + +public: + //! Default: construct an unbound, unconnected UDP socket + UDPSocket() : DatagramSocket( AF_INET, SOCK_DGRAM ) {} +}; + +//! A wrapper around [TCP sockets](\ref man7::tcp) +class TCPSocket : public Socket +{ +private: + //! \brief Construct from FileDescriptor (used by accept()) + //! \param[in] fd is the FileDescriptor from which to construct + explicit TCPSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_INET, SOCK_STREAM, IPPROTO_TCP ) {} + +public: + //! Default: construct an unbound, unconnected TCP socket + TCPSocket() : Socket( AF_INET, SOCK_STREAM ) {} + + //! Mark a socket as listening for incoming connections + void listen( int backlog = 16 ); + + //! Accept a new incoming connection + TCPSocket accept(); +}; + +//! A wrapper around [packet sockets](\ref man7:packet) +class PacketSocket : public DatagramSocket +{ +public: + PacketSocket( const int type, const int protocol ) : DatagramSocket( AF_PACKET, type, protocol ) {} + + void set_promiscuous(); +}; + +//! A wrapper around [raw sockets](\ref man7:raw) +class RawSocket : public DatagramSocket +{ +public: + RawSocket() : DatagramSocket( AF_INET, SOCK_RAW, IPPROTO_RAW ) {} +}; + +//! A wrapper around [Unix-domain stream sockets](\ref man7::unix) +class LocalStreamSocket : public Socket +{ +public: + //! Construct from a file descriptor + explicit LocalStreamSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_UNIX, SOCK_STREAM ) {} +}; + +//! A wrapper around [Unix-domain datagram sockets](\ref man7::unix) +class LocalDatagramSocket : public DatagramSocket +{ + //! \param[in] fd is the FileDescriptor from which to construct + explicit LocalDatagramSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_UNIX, SOCK_DGRAM ) {} + +public: + //! Default: construct an unbound, unconnected socket + LocalDatagramSocket() : DatagramSocket( AF_UNIX, SOCK_DGRAM ) {} +}; diff --git a/include/ricochet/net/sys/string_view_range.hh b/include/ricochet/net/sys/string_view_range.hh new file mode 100644 index 0000000..84be394 --- /dev/null +++ b/include/ricochet/net/sys/string_view_range.hh @@ -0,0 +1,18 @@ +/** + * --------------------------------------------------------------------------- + * @notice This file was adapted from the Minnow networking framework, + * originally developed for Stanford University's CS144 course. + * It has been incorporated into the Ricochet project as foundational + * system utility code. + * --------------------------------------------------------------------------- + */ + +#pragma once + +#include <ranges> +#include <type_traits> + +// This concept represents any range (e.g. vector) whose elements can be converted to string_view. +template<typename R> +concept StringViewRange + = std::ranges::sized_range<R> && std::is_constructible_v<std::string_view, std::ranges::range_value_t<R>>; |
