aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-11 02:20:31 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-11 02:20:31 +0800
commit98e88c01eb0e6404d1c5f49a22165d0861e6b924 (patch)
treed016d445fe476f747eea9030896214adeaacf51f
parent44ad3c8b36540c6b0b3144a21ac40310e163bb19 (diff)
downloadrwebbrowser-98e88c01eb0e6404d1c5f49a22165d0861e6b924.tar.gz
rwebbrowser-98e88c01eb0e6404d1c5f49a22165d0861e6b924.tar.xz
feat: implement basic HTTP client with socket and address networking utilities
-rw-r--r--README.md3
-rw-r--r--include/ricochet/net/http_client.hh59
-rw-r--r--include/ricochet/net/sys/address.hh88
-rw-r--r--include/ricochet/net/sys/exception.hh80
-rw-r--r--include/ricochet/net/sys/file_descriptor.hh141
-rw-r--r--include/ricochet/net/sys/socket.hh160
-rw-r--r--include/ricochet/net/sys/string_view_range.hh18
-rw-r--r--src/core/browser.cc13
-rw-r--r--src/net/http_client.cc118
-rw-r--r--src/net/sys/address.cc207
-rw-r--r--src/net/sys/file_descriptor.cc213
-rw-r--r--src/net/sys/socket.cc286
12 files changed, 1386 insertions, 0 deletions
diff --git a/README.md b/README.md
index 5d6549d..fd300d2 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,9 @@ This installs the `ricochet` binary to `bin` and supporting libraries to `lib`.
| `u` | Enter new URL |
| `q` | Quit Ricochet |
+## Acknowledgments
+The low-level POSIX socket wrappers in `src/net/sys/` are adapted from the Minnow networking framework developed for Stanford University's CS144 course. These utilities are used gracefully to handle file descriptors and network interfaces.
+
## License
Ricochet is free software: you can redistribute it and/or modify it under the terms of the **GNU General Public License (GPL) v3**.
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>>;
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 88c47be..ccda9a1 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -17,6 +17,7 @@
*/
#include "ricochet/core/browser.hh"
+#include "ricochet/net/http_client.hh"
#include <iostream>
namespace ricochet::core {
@@ -24,6 +25,18 @@ namespace ricochet::core {
int Browser::run( std::string_view initial_url )
{
std::cout << "Browser: Navigating to " << initial_url << "...\n";
+
+ const net::HttpClient client;
+ auto response_result = client.fetch( initial_url );
+ if ( !response_result.has_value() ) {
+ std::cerr << "[!] Ricochet failed to load page: " << response_result.error() << "\n";
+ return 1;
+ }
+
+ const auto& response = response_result.value();
+ std::cout << "\n[=== HTTP Status: " << response.status_code << " ===]\n";
+ std::cout << "[=== HTML Content Below ===]\n\n";
+ std::cout << response.body << "\n";
return 0;
}
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
new file mode 100644
index 0000000..256c1f2
--- /dev/null
+++ b/src/net/http_client.cc
@@ -0,0 +1,118 @@
+/*
+ * 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/>.
+ */
+
+#include "ricochet/net/http_client.hh"
+
+#include "ricochet/net/sys/address.hh"
+#include "ricochet/net/sys/socket.hh"
+
+#include <cstddef>
+#include <format>
+#include <string>
+
+namespace ricochet::net {
+
+namespace {
+
+struct ParsedUrl
+{
+ std::string host {};
+ std::string path {};
+};
+
+std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
+{
+ const std::string_view http_prefix = "http://";
+
+ if ( !url.starts_with( http_prefix ) ) {
+ return std::unexpected( "Only 'http://' URLs are supported in MVP. (e.g., http://example.com)" );
+ }
+
+ url.remove_prefix( http_prefix.length() );
+ auto slash_pos = url.find( '/' );
+
+ ParsedUrl result;
+ if ( slash_pos == std::string_view::npos ) {
+ result.host = std::string( url );
+ result.path = "/";
+ } else {
+ result.host = std::string( url.substr( 0, slash_pos ) );
+ result.path = std::string( url.substr( slash_pos ) );
+ }
+
+ return result;
+}
+} // namespace
+
+std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url ) const // NOLINT(readability-convert-member-functions-to-static)
+{
+ auto parsed_url = parse_url( url );
+ if ( !parsed_url.has_value() ) {
+ return std::unexpected( parsed_url.error() );
+ }
+
+ auto [host, path] = parsed_url.value();
+
+ try {
+ const Address server_addr( host, "http" );
+
+ TCPSocket socket;
+ socket.connect( server_addr );
+
+ const std::string request = std::format( "GET {} HTTP/1.1\r\n"
+ "Host: {}\r\n"
+ "User-Agent: Ricochet/0.1.0\r\n"
+ "Connection: close\r\n\r\n",
+ path,
+ host );
+
+ size_t bytes_written = 0;
+ while ( bytes_written < request.size() ) {
+ bytes_written += socket.write( request.substr( bytes_written ) );
+ }
+
+ std::string raw_response;
+ while ( !socket.eof() ) {
+ std::string buffer;
+ socket.read( buffer );
+ raw_response += buffer;
+ }
+
+ HttpResponse response;
+ auto header_end = raw_response.find( "\r\n\r\n" );
+
+ if ( header_end != std::string::npos ) {
+ response.body = raw_response.substr( header_end + 4 );
+ if ( raw_response.size() >= 12 ) {
+ try {
+ response.status_code = std::stoi( raw_response.substr( 9, 3 ) );
+ } catch ( ... ) {
+ response.status_code = 0;
+ }
+ }
+ } else {
+ response.body = raw_response;
+ }
+
+ return response;
+ } catch ( const std::exception& e ) {
+ return std::unexpected( std::string( "Network Error: " ) + e.what() );
+ }
+}
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/src/net/sys/address.cc b/src/net/sys/address.cc
new file mode 100644
index 0000000..48f22ac
--- /dev/null
+++ b/src/net/sys/address.cc
@@ -0,0 +1,207 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @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.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/address.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <array>
+#include <cstring>
+#include <linux/if_packet.h>
+#include <memory>
+#include <netdb.h>
+#include <stdexcept>
+#include <sys/socket.h>
+#include <system_error>
+
+using namespace std;
+
+//! Converts Raw to `sockaddr *`.
+Address::Raw::operator sockaddr*()
+{
+ return reinterpret_cast<sockaddr*>( &storage ); // NOLINT(*-reinterpret-cast)
+}
+
+//! Converts Raw to `const sockaddr *`.
+Address::Raw::operator const sockaddr*() const
+{
+ return reinterpret_cast<const sockaddr*>( &storage ); // NOLINT(*-reinterpret-cast)
+}
+
+//! \param[in] addr points to a raw socket address
+//! \param[in] size is `addr`'s length
+Address::Address( const sockaddr* addr, const size_t size ) : size_( size )
+{
+ // make sure proposed sockaddr can fit
+ if ( size > sizeof( address_.storage ) ) {
+ throw runtime_error( "invalid sockaddr size" );
+ }
+
+ memcpy( &address_.storage, addr, size );
+}
+
+//! Error category for getaddrinfo and getnameinfo failures.
+class gai_error_category : public error_category
+{
+public:
+ //! The name of the wrapped error
+ const char* name() const noexcept override { return "gai_error_category"; }
+ //! \brief An error message
+ //! \param[in] return_value the error return value from [getaddrinfo(3)](\ref man3::getaddrinfo)
+ //! or [getnameinfo(3)](\ref man3::getnameinfo)
+ string message( const int return_value ) const noexcept override { return gai_strerror( return_value ); }
+};
+
+//! \param[in] node is the hostname or dotted-quad address
+//! \param[in] service is the service name or numeric string
+//! \param[in] hints are criteria for resolving the supplied name
+Address::Address( const string& node, const string& service, const addrinfo& hints ) : size_()
+{
+ // prepare for the answer
+ addrinfo* resolved_address = nullptr;
+
+ // look up the name or names
+ const int gai_ret = getaddrinfo( node.c_str(), service.c_str(), &hints, &resolved_address );
+ if ( gai_ret != 0 ) {
+ throw tagged_error( gai_error_category(), "getaddrinfo(" + node + ", " + service + ")", gai_ret );
+ }
+
+ // if success, should always have at least one entry
+ if ( resolved_address == nullptr ) {
+ throw runtime_error( "getaddrinfo returned successfully but with no results" );
+ }
+
+ // put resolved_address in a wrapper so it will get freed if we have to throw an exception
+ auto addrinfo_deleter = []( addrinfo* const x ) { freeaddrinfo( x ); };
+ unique_ptr<addrinfo, decltype( addrinfo_deleter )> wrapped_address( resolved_address, move( addrinfo_deleter ) );
+
+ // assign to our private members (making sure size fits)
+ *this = Address( wrapped_address->ai_addr, wrapped_address->ai_addrlen );
+}
+
+//! \brief Build a `struct addrinfo` containing hints for [getaddrinfo(3)](\ref man3::getaddrinfo)
+//! \param[in] ai_flags is the value of the `ai_flags` field in the [struct addrinfo](\ref man3::getaddrinfo)
+//! \param[in] ai_family is the value of the `ai_family` field in the [struct addrinfo](\ref
+//! man3::getaddrinfo)
+namespace {
+inline addrinfo make_hints( int ai_flags, int ai_family ) // NOLINT(*-swappable-parameters)
+{
+ addrinfo hints {}; // value initialized to all zeros
+ hints.ai_flags = ai_flags;
+ hints.ai_family = ai_family;
+ return hints;
+}
+} // namespace
+
+//! \param[in] hostname to resolve
+//! \param[in] service name (from `/etc/services`, e.g., "http" is port 80)
+Address::Address( const string& hostname, const string& service )
+ : Address( hostname, service, make_hints( AI_ALL, AF_INET ) )
+{}
+
+//! \param[in] ip address as a dotted quad ("1.1.1.1")
+//! \param[in] port number
+Address::Address( const string& ip, const uint16_t port )
+ // tell getaddrinfo that we don't want to resolve anything
+ : Address( ip, ::to_string( port ), make_hints( AI_NUMERICHOST | AI_NUMERICSERV, AF_INET ) )
+{}
+
+// accessors
+pair<string, uint16_t> Address::ip_port() const
+{
+ if ( address_.storage.ss_family != AF_INET and address_.storage.ss_family != AF_INET6 ) {
+ throw runtime_error( "Address::ip_port() called on non-Internet address" );
+ }
+
+ array<char, NI_MAXHOST> ip {};
+ array<char, NI_MAXSERV> port {};
+
+ const int gni_ret = getnameinfo( static_cast<const sockaddr*>( address_ ),
+ size_,
+ ip.data(),
+ ip.size(),
+ port.data(),
+ port.size(),
+ NI_NUMERICHOST | NI_NUMERICSERV );
+ if ( gni_ret != 0 ) {
+ throw tagged_error( gai_error_category(), "getnameinfo", gni_ret );
+ }
+
+ return { ip.data(), stoi( port.data() ) };
+}
+
+string Address::to_string() const
+{
+ if ( address_.storage.ss_family == AF_INET or address_.storage.ss_family == AF_INET6 ) {
+ const auto ip_and_port = ip_port();
+ return ip_and_port.first + ":" + ::to_string( ip_and_port.second );
+ }
+
+ return "(non-Internet address)";
+}
+
+uint32_t Address::ipv4_numeric() const
+{
+ if ( address_.storage.ss_family != AF_INET or size_ != sizeof( sockaddr_in ) ) {
+ throw runtime_error( "ipv4_numeric called on non-IPV4 address" );
+ }
+
+ sockaddr_in ipv4_addr {};
+ memcpy( &ipv4_addr, &address_.storage, size_ );
+
+ return be32toh( ipv4_addr.sin_addr.s_addr );
+}
+
+Address Address::from_ipv4_numeric( const uint32_t ip_address )
+{
+ sockaddr_in ipv4_addr {};
+ ipv4_addr.sin_family = AF_INET;
+ ipv4_addr.sin_addr.s_addr = htobe32( ip_address );
+
+ return { reinterpret_cast<sockaddr*>( &ipv4_addr ), sizeof( ipv4_addr ) }; // NOLINT(*-reinterpret-cast)
+}
+
+// equality
+bool Address::operator==( const Address& other ) const
+{
+ if ( size_ != other.size_ ) {
+ return false;
+ }
+
+ return 0 == memcmp( &address_, &other.address_, size_ );
+}
+
+// address families that correspond to each sockaddr type
+template<typename sockaddr_type>
+constexpr int sockaddr_family = -1;
+
+template<>
+constexpr int sockaddr_family<sockaddr_in> = AF_INET;
+
+template<>
+constexpr int sockaddr_family<sockaddr_in6> = AF_INET6;
+
+template<>
+constexpr int sockaddr_family<sockaddr_ll> = AF_PACKET;
+
+// safely cast the address to its underlying sockaddr type
+template<typename sockaddr_type>
+const sockaddr_type* Address::as() const
+{
+ const sockaddr* raw { address_ };
+ if ( sizeof( sockaddr_type ) < size() or raw->sa_family != sockaddr_family<sockaddr_type> ) {
+ throw runtime_error( "Address::as() conversion failure" );
+ }
+
+ return reinterpret_cast<const sockaddr_type*>( raw ); // NOLINT(*-reinterpret-cast)
+}
+
+template const sockaddr_in* Address::as<sockaddr_in>() const;
+template const sockaddr_in6* Address::as<sockaddr_in6>() const;
+template const sockaddr_ll* Address::as<sockaddr_ll>() const;
diff --git a/src/net/sys/file_descriptor.cc b/src/net/sys/file_descriptor.cc
new file mode 100644
index 0000000..b765308
--- /dev/null
+++ b/src/net/sys/file_descriptor.cc
@@ -0,0 +1,213 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @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.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/file_descriptor.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <fcntl.h>
+#include <iostream>
+#include <stdexcept>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+using namespace std;
+
+size_t FileDescriptor::FDWrapper::CheckFDSystemCall( string_view what, ssize_t return_value ) const
+{
+ if ( return_value >= 0 ) {
+ return return_value;
+ }
+
+ if ( non_blocking_ and ( errno == EAGAIN or errno == EWOULDBLOCK or errno == EINPROGRESS ) ) {
+ return 0;
+ }
+
+ throw unix_error { what };
+}
+
+size_t FileDescriptor::FDWrapper::CheckRead( string_view what, ssize_t return_value )
+{
+ if ( return_value == 0 ) {
+ eof_ = true;
+ }
+
+ return CheckFDSystemCall( what, return_value );
+}
+
+size_t FileDescriptor::CheckFDSystemCall( string_view what, ssize_t return_value ) const
+{
+ if ( not internal_fd_ ) {
+ throw runtime_error( "internal error: missing internal_fd_" );
+ }
+ return internal_fd_->CheckFDSystemCall( what, return_value );
+}
+
+size_t FileDescriptor::CheckRead( string_view what, ssize_t return_value )
+{
+ if ( not internal_fd_ ) {
+ throw runtime_error( "internal error: missing internal_fd_" );
+ }
+ return internal_fd_->CheckRead( what, return_value );
+}
+
+// fd is the file descriptor number returned by [open(2)](\ref man2::open) or similar
+FileDescriptor::FDWrapper::FDWrapper( int fd ) : fd_( fd )
+{
+ if ( fd < 0 ) {
+ throw runtime_error( "invalid fd number:" + to_string( fd ) );
+ }
+
+ const int flags = CheckSystemCall( "fcntl", fcntl( fd, F_GETFL ) ); // NOLINT(*-vararg)
+ non_blocking_ = flags & O_NONBLOCK; // NOLINT(*-bitwise)
+}
+
+void FileDescriptor::FDWrapper::close()
+{
+ CheckSystemCall( "close", ::close( fd_ ) );
+ eof_ = closed_ = true;
+}
+
+FileDescriptor::FDWrapper::~FDWrapper()
+{
+ try {
+ if ( closed_ ) {
+ return;
+ }
+ close();
+ } catch ( const exception& e ) {
+ // don't throw an exception from the destructor
+ cerr << "Exception destructing FDWrapper: " << e.what() << "\n";
+ }
+}
+
+// fd is the file descriptor number returned by [open(2)](\ref man2::open) or similar
+FileDescriptor::FileDescriptor( int fd ) : internal_fd_( make_shared<FDWrapper>( fd ) ) {}
+
+// Private constructor used by duplicate()
+FileDescriptor::FileDescriptor( shared_ptr<FDWrapper> other_shared_ptr ) : internal_fd_( move( other_shared_ptr ) )
+{}
+
+// returns a copy of this FileDescriptor
+FileDescriptor FileDescriptor::duplicate() const
+{
+ return FileDescriptor { internal_fd_ };
+}
+
+// Read into a single buffer of given length (which if empty will be resized to a reasonable value before reading).
+// Afer the read, the buffer will be resized to match whatever was read.
+void FileDescriptor::read( string& buffer )
+{
+ if ( buffer.empty() ) {
+ buffer.resize( kReadBufferSize );
+ }
+
+ const size_t bytes_read = CheckRead( "read", ::read( fd_num(), buffer.data(), buffer.size() ) );
+ register_read();
+
+ if ( bytes_read > buffer.size() ) {
+ throw runtime_error( "read() read more than requested" );
+ }
+
+ buffer.resize( bytes_read );
+}
+
+// Read into a vector of buffers (if all empty, the last one will be resized to a reasonable value).
+// After the read, the buffers will be resized to match whatever was read.
+void FileDescriptor::read( vector<string>& buffers )
+{
+ if ( buffers.empty() ) {
+ throw runtime_error( "FileDescriptor::read called with no buffers" );
+ }
+
+ if ( buffers.back().empty() ) {
+ buffers.back().resize( kReadBufferSize );
+ }
+
+ static thread_local vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( buffers, iovecs );
+
+ const size_t bytes_read
+ = CheckRead( "readv", readv( fd_num(), iovecs.data(), static_cast<int>( iovecs.size() ) ) );
+ register_read();
+
+ if ( bytes_read > total_size ) {
+ throw runtime_error( "read() read more than requested" );
+ }
+
+ size_t remaining_size = bytes_read;
+ for ( auto& buf : buffers ) {
+ if ( remaining_size >= buf.size() ) {
+ remaining_size -= buf.size();
+ } else {
+ buf.resize( remaining_size );
+ remaining_size = 0;
+ }
+ }
+}
+
+void FileDescriptor::write_all( string_view buffer )
+{
+ if ( not blocking() ) {
+ throw runtime_error( "write_all requires a blocking file descriptor" );
+ // otherwise, this could call write on a non-blocking fd that isn't "ready" for writing
+ }
+
+ while ( not buffer.empty() ) {
+ buffer.remove_prefix( write( buffer ) );
+ }
+}
+
+size_t FileDescriptor::write( string_view buffer )
+{
+ const size_t bytes_written = CheckFDSystemCall( "write", ::write( fd_num(), buffer.data(), buffer.size() ) );
+ register_write();
+
+ if ( bytes_written == 0 and not buffer.empty() ) {
+ throw runtime_error( "write returned 0 given non-empty input buffer" );
+ }
+
+ if ( bytes_written > buffer.size() ) {
+ throw runtime_error( "write wrote more than length of input buffer" );
+ }
+
+ return bytes_written;
+}
+
+size_t FileDescriptor::write( const std::vector<iovec>& iovecs, size_t total_size )
+{
+ const size_t bytes_written
+ = CheckFDSystemCall( "writev", ::writev( fd_num(), iovecs.data(), static_cast<int>( iovecs.size() ) ) );
+ register_write();
+
+ if ( bytes_written == 0 and total_size != 0 ) {
+ throw runtime_error( "writev returned 0 given non-empty input buffer" );
+ }
+
+ if ( bytes_written > total_size ) {
+ throw runtime_error( "writev wrote more than length of input buffer" );
+ }
+
+ return bytes_written;
+}
+
+void FileDescriptor::set_blocking( bool blocking )
+{
+ int flags = CheckSystemCall( "fcntl", fcntl( fd_num(), F_GETFL ) ); // NOLINT(*-vararg)
+ if ( blocking ) {
+ flags ^= ( flags & O_NONBLOCK ); // NOLINT(*-bitwise)
+ } else {
+ flags |= O_NONBLOCK; // NOLINT(*-bitwise)
+ }
+
+ CheckSystemCall( "fcntl", fcntl( fd_num(), F_SETFL, flags ) ); // NOLINT(*-vararg)
+
+ internal_fd_->non_blocking_ = not blocking;
+}
diff --git a/src/net/sys/socket.cc b/src/net/sys/socket.cc
new file mode 100644
index 0000000..701704f
--- /dev/null
+++ b/src/net/sys/socket.cc
@@ -0,0 +1,286 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @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.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/socket.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <linux/if_packet.h>
+#include <stdexcept>
+#include <unistd.h>
+
+using namespace std;
+
+// default constructor for socket of (subclassed) domain and type
+//! \param[in] domain is as described in [socket(7)](\ref man7::socket), probably `AF_INET` or `AF_UNIX`
+//! \param[in] type is as described in [socket(7)](\ref man7::socket)
+Socket::Socket( const int domain, const int type, const int protocol )
+ : FileDescriptor( CheckSystemCall( "socket", socket( domain, type, protocol ) ) )
+{}
+
+// construct from file descriptor
+//! \param[in] fd is the FileDescriptor from which to construct
+//! \param[in] domain is `fd`'s domain; throws std::runtime_error if wrong value is supplied
+//! \param[in] type is `fd`'s type; throws std::runtime_error if wrong value is supplied
+//! \param[in] protocol is `fd`'s protocol; throws std::runtime_error if wrong value is supplied
+Socket::Socket( FileDescriptor&& fd, int domain, int type, int protocol ) // NOLINT(*-swappable-parameters)
+ : FileDescriptor( move( fd ) )
+{
+ int actual_value {};
+ socklen_t len {};
+
+ // verify domain
+ len = getsockopt( SOL_SOCKET, SO_DOMAIN, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != domain ) ) {
+ throw runtime_error( "socket domain mismatch" );
+ }
+
+ // verify type
+ len = getsockopt( SOL_SOCKET, SO_TYPE, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != type ) ) {
+ throw runtime_error( "socket type mismatch" );
+ }
+
+ // verify protocol
+ len = getsockopt( SOL_SOCKET, SO_PROTOCOL, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != protocol ) ) {
+ throw runtime_error( "socket protocol mismatch" );
+ }
+}
+
+// get the local or peer address the socket is connected to
+//! \param[in] name_of_function is the function to call (string passed to CheckSystemCall())
+//! \param[in] function is a pointer to the function
+//! \returns the requested Address
+Address Socket::get_address( const string& name_of_function,
+ const function<int( int, sockaddr*, socklen_t* )>& function ) const
+{
+ Address::Raw address;
+ socklen_t size = sizeof( address );
+
+ CheckSystemCall( name_of_function, function( fd_num(), address, &size ) );
+
+ return Address { address, size };
+}
+
+//! \returns the local Address of the socket
+Address Socket::local_address() const
+{
+ return get_address( "getsockname", getsockname );
+}
+
+//! \returns the socket's peer's Address
+Address Socket::peer_address() const
+{
+ return get_address( "getpeername", getpeername );
+}
+
+// bind socket to a specified local address (usually to listen/accept)
+//! \param[in] address is a local Address to bind
+void Socket::bind( const Address& address )
+{
+ CheckSystemCall( "bind", ::bind( fd_num(), address.raw(), address.size() ) );
+}
+
+void Socket::bind_to_device( const string_view device_name )
+{
+ setsockopt( SOL_SOCKET, SO_BINDTODEVICE, device_name );
+}
+
+// connect socket to a specified peer address
+//! \param[in] address is the peer's Address
+void Socket::connect( const Address& address )
+{
+ CheckFDSystemCall( "connect", ::connect( fd_num(), address.raw(), address.size() ) );
+}
+
+// shut down a socket in the specified way
+//! \param[in] how can be `SHUT_RD`, `SHUT_WR`, or `SHUT_RDWR`; see [shutdown(2)](\ref man2::shutdown)
+void Socket::shutdown( const int how )
+{
+ CheckSystemCall( "shutdown", ::shutdown( fd_num(), how ) );
+ switch ( how ) {
+ case SHUT_RD:
+ register_read();
+ break;
+ case SHUT_WR:
+ register_write();
+ break;
+ case SHUT_RDWR:
+ register_read();
+ register_write();
+ break;
+ default:
+ throw runtime_error( "Socket::shutdown() called with invalid `how`" );
+ }
+}
+
+void DatagramSocket::recv( Address& source_address, string& payload )
+{
+ if ( payload.empty() ) {
+ payload.resize( kReadBufferSize );
+ }
+
+ Address::Raw raw_source_address;
+ socklen_t namelen = sizeof( raw_source_address );
+ const size_t recv_len = CheckFDSystemCall(
+ "recvfrom", ::recvfrom( fd_num(), payload.data(), payload.size(), MSG_TRUNC, raw_source_address, &namelen ) );
+ register_read();
+
+ if ( recv_len > payload.size() ) {
+ throw runtime_error( "recvfrom (oversized datagram of length " + to_string( recv_len ) + ")" );
+ }
+ if ( namelen <= 0 || namelen > sizeof( raw_source_address ) ) {
+ throw runtime_error( "recvfrom gave invalid namelen" );
+ }
+
+ source_address = { raw_source_address, namelen };
+}
+
+void DatagramSocket::recv( Address& source_address, vector<string>& payloads )
+{
+ if ( payloads.empty() ) {
+ throw runtime_error( "DatagramSocket::recv called with no payload buffers" );
+ }
+
+ if ( payloads.back().empty() ) {
+ payloads.back().resize( kReadBufferSize );
+ }
+
+ // set up iovecs
+ static thread_local vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( payloads, iovecs );
+
+ // set up msghdr
+ Address::Raw raw_source_address;
+ msghdr message { .msg_name = &raw_source_address,
+ .msg_namelen = sizeof( raw_source_address ),
+ .msg_iov = iovecs.data(),
+ .msg_iovlen = iovecs.size(),
+ .msg_control = nullptr,
+ .msg_controllen {},
+ .msg_flags {} };
+ const size_t recv_len = CheckFDSystemCall( "recvmsg", ::recvmsg( fd_num(), &message, MSG_TRUNC ) );
+ register_read();
+ if ( recv_len > total_size ) {
+ throw runtime_error( "recvmsg (oversized datagram of length " + to_string( recv_len ) + ")" );
+ }
+ if ( message.msg_flags & MSG_TRUNC ) {
+ throw runtime_error( "recvmsg (oversized datagram indicated only by MSG_TRUNC)" );
+ }
+ if ( message.msg_namelen <= 0 || message.msg_namelen > sizeof( raw_source_address ) ) {
+ throw runtime_error( "recvmsg gave invalid namelen" );
+ }
+ source_address = { raw_source_address, message.msg_namelen };
+
+ size_t remaining_size = recv_len;
+ for ( auto& buf : payloads ) {
+ if ( remaining_size >= buf.size() ) {
+ remaining_size -= buf.size();
+ } else {
+ buf.resize( remaining_size );
+ remaining_size = 0;
+ }
+ }
+}
+
+void DatagramSocket::send( string_view payload, const optional<Address>& destination )
+{
+ const size_t bytes_sent = CheckFDSystemCall( "sendto",
+ ::sendto( fd_num(),
+ payload.data(),
+ payload.size(),
+ 0,
+ destination.has_value() ? destination->raw() : nullptr,
+ destination.has_value() ? destination->size() : 0 ) );
+ register_write();
+ if ( bytes_sent != payload.size() ) {
+ throw runtime_error( "sendto sent some length other than that of payload" );
+ }
+}
+
+void DatagramSocket::send( vector<iovec>& iovecs, size_t total_size, const optional<Address>& destination )
+{
+ const msghdr message { .msg_name = destination.has_value() // NOLINTNEXTLINE(*-const-cast)
+ ? static_cast<void*>( const_cast<sockaddr*>( destination->raw() ) )
+ : nullptr,
+ .msg_namelen = destination.has_value() ? destination->size() : 0,
+ .msg_iov = iovecs.data(),
+ .msg_iovlen = iovecs.size(),
+ .msg_control = nullptr,
+ .msg_controllen {},
+ .msg_flags {} };
+ const size_t bytes_sent = CheckFDSystemCall( "sendmsg", ::sendmsg( fd_num(), &message, 0 ) );
+ register_write();
+ if ( bytes_sent != total_size ) {
+ throw runtime_error( "sendmsg sent some length other than that of payload" );
+ }
+}
+
+// mark the socket as listening for incoming connections
+//! \param[in] backlog is the number of waiting connections to queue (see [listen(2)](\ref man2::listen))
+void TCPSocket::listen( const int backlog )
+{
+ CheckSystemCall( "listen", ::listen( fd_num(), backlog ) );
+}
+
+// accept a new incoming connection
+//! \returns a new TCPSocket connected to the peer.
+//! \note This function blocks until a new connection is available
+TCPSocket TCPSocket::accept()
+{
+ register_read();
+ return TCPSocket( FileDescriptor( CheckSystemCall( "accept", ::accept( fd_num(), nullptr, nullptr ) ) ) );
+}
+
+// get socket option
+template<typename option_type>
+socklen_t Socket::getsockopt( const int level, const int option, option_type& option_value ) const
+{
+ socklen_t optlen = sizeof( option_value );
+ CheckSystemCall( "getsockopt", ::getsockopt( fd_num(), level, option, &option_value, &optlen ) );
+ return optlen;
+}
+
+// set socket option
+//! \param[in] level The protocol level at which the argument resides
+//! \param[in] option A single option to set
+//! \param[in] option_value The value to set
+//! \details See [setsockopt(2)](\ref man2::setsockopt) for details.
+template<typename option_type>
+void Socket::setsockopt( const int level, const int option, const option_type& option_value )
+{
+ CheckSystemCall( "setsockopt", ::setsockopt( fd_num(), level, option, &option_value, sizeof( option_value ) ) );
+}
+
+// setsockopt with size only known at runtime
+void Socket::setsockopt( const int level, const int option, const string_view option_val )
+{
+ CheckSystemCall( "setsockopt", ::setsockopt( fd_num(), level, option, option_val.data(), option_val.size() ) );
+}
+
+// allow local address to be reused sooner, at the cost of some robustness
+//! \note Using `SO_REUSEADDR` may reduce the robustness of your application
+void Socket::set_reuseaddr()
+{
+ setsockopt( SOL_SOCKET, SO_REUSEADDR, int { true } );
+}
+
+void Socket::throw_if_error() const
+{
+ int socket_error = 0;
+ const socklen_t len = getsockopt( SOL_SOCKET, SO_ERROR, socket_error );
+ if ( len != sizeof( socket_error ) ) {
+ throw runtime_error( "unexpected length from getsockopt: " + to_string( len ) );
+ }
+
+ if ( socket_error ) {
+ throw unix_error( "socket error", socket_error );
+ }
+}