From 3f39df2356504bc68a4e493a723fd4d907c43ff4 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Sat, 11 Apr 2026 00:50:38 +0800 Subject: feat: initialize project structure with CMake build system, C++23 support, and core browser scaffolding --- ricochet/README.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 ricochet/README.md (limited to 'ricochet/README.md') diff --git a/ricochet/README.md b/ricochet/README.md new file mode 100644 index 0000000..5d6549d --- /dev/null +++ b/ricochet/README.md @@ -0,0 +1,111 @@ +# Ricochet +> **A minimalist CLI web browser for fast, distraction-free navigation.** + +**Ricochet** is a terminal-based web browser designed for those who value speed, privacy, and the original spirit of the World Wide Web. By stripping away the "JavaScript Trap" and focus-stealing elements of modern web design, Ricochet delivers content in its purest form directly to your emulator. + +## Philosophy +Modern browsing has become a minefield of non-free scripts, tracking pixels, and visual clutter. Ricochet ricochets off the noise: +* **Freedom First:** No JavaScript support by design. We serve the user, not the scripts. +* **Minimalist:** Focused entirely on text and structure. +* **Distraction-Free:** No ads, no popups, no auto-playing media. Just the data you came for. + +## Key Features +- [x] **High-Performance C++ Core:** Native speed with minimal memory footprint. +- [x] **Robust HTML5 Parser:** A custom, fault-tolerant DOM builder for the "wild" web. +- [x] **TUI Navigation:** Intuitive, keyboard-driven interface (Vim-like bindings). +- [x] **Privacy Centric:** No cookies by default, no telemetry, and zero JS execution. +- [x] **Secure:** Native HTTPS support via modern SSL/TLS stacks. + +## Architecture +Ricochet is built with a strictly modular approach: +* **`Net`**: Handles TCP/TLS streams and HTTP/1.1 protocol state. +* **`Parser`**: A state-machine driven HTML lexer and DOM tree builder. +* **`Layout`**: Calculates box models and word-wrapping for terminal constraints. +* **`TUI`**: An event-driven terminal UI handling viewport rendering and user input. + +## Getting Started + +### Prerequisites +To build Ricochet, ensure your environment meets the following requirements: +* **Compiler**: A C++ compiler with full **C++23** support (e.g., **GCC 13+** or **Clang 15+**). +* **Build System**: **CMake 3.24.2** or higher. +* **Development Tools** (Optional but recommended): + * `clang-format` for automated code styling. + * `clang-tidy` for static analysis and linting. + +### Build Instructions +Ricochet enforces out-of-source builds to keep the project structure clean. By default, the project builds in Debug mode with Address and Undefined Behavior sanitizers enabled. + +```bash +# 1. Clone the repository +git clone https://github.com/yourusername/ricochet.git +cd ricochet + +# 2. Configure the project +# This will generate compile commands and set the build type to Release +cmake -B build -DCMAKE_BUILD_TYPE=Release + +# 3. Compile the project using all available cores +cmake --build build -j$(nproc) + +# 4. Launch Ricochet +# The executable is located in the bin/ directory +./build/bin/ricochet https://wikipedia.org +``` + +### Build Parameters & Configuration + +You can customize the build by passing `-D=` to the initial `cmake -B build` command. + +#### 1. Build Types (`CMAKE_BUILD_TYPE`) +The project defaults to **Debug** mode if no type is specified. +* **`Debug`**: Enables full sanitizers and debug symbols (Default). +* **`Release`**: Optimizes for performance and reduces binary size. +* **`RelWithDebInfo`**: Optimized build that retains debug symbols. + +#### 2. Security & Quality Flags +These parameters are automatically managed by `etc/cflags.cmake`: +* **Sanitizers:** Includes AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan). Recovery is disabled for all errors (`-fno-sanitize-recover=all`). +* **Warning Level:** Set to extremely strict levels, including `-Wall`, `-Wextra`, `-Wpedantic`, and `-Weffc++`. +* **Warnings as Errors:** All compiler warnings are treated as errors (`-Werror`), ensuring no code is merged with unresolved issues. + +#### 3. Output Directories +The build system organizes outputs into the following structure within your build folder: +* **Executables:** `build/bin/` +* **Static/Shared Libraries:** `build/lib/` +* **Compile Commands:** `build/compile_commands.json` is generated for LSP (Language Server) support. + +--- + +### Development & Maintenance Targets + +The project includes several custom targets for code maintenance: + +| Target | Command | Description | +| :--- | :--- | :--- | +| **Format** | `cmake --build build --target format` | Uses `clang-format` to enforce consistent coding style across all `.cc` and `.hh` files. | +| **Tidy** | `cmake --build build --target tidy` | Runs `clang-tidy` on the core Ricochet source files for static analysis. | +| **Tidy-All** | `cmake --build build --target tidy-all` | Runs `clang-tidy` on every file in the project, including tests. | +| **Test** | `ctest --test-dir build` | Executes the test suite via CTest. | + +### Installation +To install the compiled binaries to your system, use: +```bash +cmake --install build --prefix /usr/local +``` +This installs the `ricochet` binary to `bin` and supporting libraries to `lib`. +## Navigation +| Key | Action | +| :--- | :--- | +| `j / k` | Scroll Down / Up | +| `Tab` | Cycle through links | +| `Enter` | Follow selected link | +| `b` | Go Back in history | +| `u` | Enter new URL | +| `q` | Quit Ricochet | + +## License +Ricochet is free software: you can redistribute it and/or modify it under the terms of the **GNU General Public License (GPL) v3**. + +--- +*Built for the terminal. Built for freedom.* \ No newline at end of file -- cgit v1.2.3 From bfb85334541e44cf03823fe8f0f80c89ee1feeba Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Sat, 11 Apr 2026 02:20:31 +0800 Subject: feat: implement basic HTTP client with socket and address networking utilities --- ricochet/README.md | 3 + ricochet/include/ricochet/net/http_client.hh | 59 +++++ ricochet/include/ricochet/net/sys/address.hh | 88 +++++++ ricochet/include/ricochet/net/sys/exception.hh | 80 ++++++ .../include/ricochet/net/sys/file_descriptor.hh | 141 ++++++++++ ricochet/include/ricochet/net/sys/socket.hh | 160 ++++++++++++ .../include/ricochet/net/sys/string_view_range.hh | 18 ++ ricochet/src/core/browser.cc | 13 + ricochet/src/net/http_client.cc | 118 +++++++++ ricochet/src/net/sys/address.cc | 207 +++++++++++++++ ricochet/src/net/sys/file_descriptor.cc | 213 +++++++++++++++ ricochet/src/net/sys/socket.cc | 286 +++++++++++++++++++++ 12 files changed, 1386 insertions(+) create mode 100644 ricochet/include/ricochet/net/http_client.hh create mode 100644 ricochet/include/ricochet/net/sys/address.hh create mode 100644 ricochet/include/ricochet/net/sys/exception.hh create mode 100644 ricochet/include/ricochet/net/sys/file_descriptor.hh create mode 100644 ricochet/include/ricochet/net/sys/socket.hh create mode 100644 ricochet/include/ricochet/net/sys/string_view_range.hh create mode 100644 ricochet/src/net/http_client.cc create mode 100644 ricochet/src/net/sys/address.cc create mode 100644 ricochet/src/net/sys/file_descriptor.cc create mode 100644 ricochet/src/net/sys/socket.cc (limited to 'ricochet/README.md') diff --git a/ricochet/README.md b/ricochet/README.md index 5d6549d..fd300d2 100644 --- a/ricochet/README.md +++ b/ricochet/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/ricochet/include/ricochet/net/http_client.hh b/ricochet/include/ricochet/net/http_client.hh new file mode 100644 index 0000000..2dd7419 --- /dev/null +++ b/ricochet/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 . + */ + +#pragma once + +#include +#include +#include +#include + +namespace ricochet::net { +/** + * @brief Encapsulate HTTP response result + */ + +struct HttpResponse +{ + int status_code { 0 }; + std::unordered_map 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 fetch( std::string_view url ) const; + +private: +}; + +} // namespace ricochet::net \ No newline at end of file diff --git a/ricochet/include/ricochet/net/sys/address.hh b/ricochet/include/ricochet/net/sys/address.hh new file mode 100644 index 0000000..39c9553 --- /dev/null +++ b/ricochet/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 +#include +#include +#include +#include +#include + +//! 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 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( address_ ); } + //! Safely convert to underlying sockaddr type + template + const sockaddr_type* as() const; + + //!@} +}; diff --git a/ricochet/include/ricochet/net/sys/exception.hh b/ricochet/include/ricochet/net/sys/exception.hh new file mode 100644 index 0000000..9587e5c --- /dev/null +++ b/ricochet/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 +#include +#include +#include +#include + +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 +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 +inline std::unique_ptr notnull( const std::string_view context, std::unique_ptr x ) +{ + return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" ); +} + +template +inline std::shared_ptr notnull( const std::string_view context, std::shared_ptr&& 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 res { abi::__cxa_demangle( name, nullptr, nullptr, &status ), + free }; + if ( status ) { + throw std::runtime_error( "cxa_demangle" ); + } + return res.get(); +} diff --git a/ricochet/include/ricochet/net/sys/file_descriptor.hh b/ricochet/include/ricochet/net/sys/file_descriptor.hh new file mode 100644 index 0000000..4af56cb --- /dev/null +++ b/ricochet/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 +#include +#include +#include + +// 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& 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 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 internal_fd_; + + // Private constructor used to duplicate the FileDescriptor (increase the reference count) + explicit FileDescriptor( std::shared_ptr other_shared_ptr ); + + // Private write method that takes a vector of iovecs (an internal OS structure) + size_t write( const std::vector& 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& 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( 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/ricochet/include/ricochet/net/sys/socket.hh b/ricochet/include/ricochet/net/sys/socket.hh new file mode 100644 index 0000000..f84be26 --- /dev/null +++ b/ricochet/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 +#include + +//! \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& 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 + socklen_t getsockopt( int level, int option, option_type& option_value ) const; + + //! Wrappers around [setsockopt(2)](\ref man2::setsockopt) + template + 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& 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
& destination = {} ); + void send( const StringViewRange auto&& payloads, const std::optional
& destination = {} ) + { + static thread_local std::vector iovecs; + const size_t total_size = to_iovecs( payloads, iovecs ); + send( iovecs, total_size, destination ); + } + +private: + void send( std::vector& iovecs, size_t total_size, const std::optional
& 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/ricochet/include/ricochet/net/sys/string_view_range.hh b/ricochet/include/ricochet/net/sys/string_view_range.hh new file mode 100644 index 0000000..84be394 --- /dev/null +++ b/ricochet/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 +#include + +// This concept represents any range (e.g. vector) whose elements can be converted to string_view. +template +concept StringViewRange + = std::ranges::sized_range && std::is_constructible_v>; diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc index 88c47be..ccda9a1 100644 --- a/ricochet/src/core/browser.cc +++ b/ricochet/src/core/browser.cc @@ -17,6 +17,7 @@ */ #include "ricochet/core/browser.hh" +#include "ricochet/net/http_client.hh" #include 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/ricochet/src/net/http_client.cc b/ricochet/src/net/http_client.cc new file mode 100644 index 0000000..256c1f2 --- /dev/null +++ b/ricochet/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 . + */ + +#include "ricochet/net/http_client.hh" + +#include "ricochet/net/sys/address.hh" +#include "ricochet/net/sys/socket.hh" + +#include +#include +#include + +namespace ricochet::net { + +namespace { + +struct ParsedUrl +{ + std::string host {}; + std::string path {}; +}; + +std::expected 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 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/ricochet/src/net/sys/address.cc b/ricochet/src/net/sys/address.cc new file mode 100644 index 0000000..48f22ac --- /dev/null +++ b/ricochet/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 +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +//! Converts Raw to `sockaddr *`. +Address::Raw::operator sockaddr*() +{ + return reinterpret_cast( &storage ); // NOLINT(*-reinterpret-cast) +} + +//! Converts Raw to `const sockaddr *`. +Address::Raw::operator const sockaddr*() const +{ + return reinterpret_cast( &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 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 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 ip {}; + array port {}; + + const int gni_ret = getnameinfo( static_cast( 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( &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 +constexpr int sockaddr_family = -1; + +template<> +constexpr int sockaddr_family = AF_INET; + +template<> +constexpr int sockaddr_family = AF_INET6; + +template<> +constexpr int sockaddr_family = AF_PACKET; + +// safely cast the address to its underlying sockaddr type +template +const sockaddr_type* Address::as() const +{ + const sockaddr* raw { address_ }; + if ( sizeof( sockaddr_type ) < size() or raw->sa_family != sockaddr_family ) { + throw runtime_error( "Address::as() conversion failure" ); + } + + return reinterpret_cast( raw ); // NOLINT(*-reinterpret-cast) +} + +template const sockaddr_in* Address::as() const; +template const sockaddr_in6* Address::as() const; +template const sockaddr_ll* Address::as() const; diff --git a/ricochet/src/net/sys/file_descriptor.cc b/ricochet/src/net/sys/file_descriptor.cc new file mode 100644 index 0000000..b765308 --- /dev/null +++ b/ricochet/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 +#include +#include +#include +#include +#include + +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( fd ) ) {} + +// Private constructor used by duplicate() +FileDescriptor::FileDescriptor( shared_ptr 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& 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 iovecs; + const size_t total_size = to_iovecs( buffers, iovecs ); + + const size_t bytes_read + = CheckRead( "readv", readv( fd_num(), iovecs.data(), static_cast( 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& iovecs, size_t total_size ) +{ + const size_t bytes_written + = CheckFDSystemCall( "writev", ::writev( fd_num(), iovecs.data(), static_cast( 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/ricochet/src/net/sys/socket.cc b/ricochet/src/net/sys/socket.cc new file mode 100644 index 0000000..701704f --- /dev/null +++ b/ricochet/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 +#include +#include + +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& 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& 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 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
& 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& iovecs, size_t total_size, const optional
& destination ) +{ + const msghdr message { .msg_name = destination.has_value() // NOLINTNEXTLINE(*-const-cast) + ? static_cast( const_cast( 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 +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 +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 ); + } +} -- cgit v1.2.3 From e84e117735f7d922f84cd13f56f6eea93b204e5c Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Mon, 13 Apr 2026 04:50:49 +0800 Subject: feat: move URL formatting to Browser class and update documentation --- ricochet/README.md | 83 ++++++++++++++++++++++++------- ricochet/include/ricochet/core/browser.hh | 1 + ricochet/src/core/browser.cc | 50 +++++++++---------- ricochet/src/main.cc | 17 +++---- 4 files changed, 99 insertions(+), 52 deletions(-) (limited to 'ricochet/README.md') diff --git a/ricochet/README.md b/ricochet/README.md index fd300d2..a09bf4a 100644 --- a/ricochet/README.md +++ b/ricochet/README.md @@ -9,6 +9,19 @@ Modern browsing has become a minefield of non-free scripts, tracking pixels, and * **Minimalist:** Focused entirely on text and structure. * **Distraction-Free:** No ads, no popups, no auto-playing media. Just the data you came for. +## Instructions +- Keyboard-Driven Navigation: Fully controllable via Vim-style keybindings (j/k for scrolling, H/L for history). + +- Link Hinting: Press f to highlight all links with two-letter codes. Jump instantly without using a mouse or typing long numbers. + +- Privacy First: Industrial-grade Cookie Domain Isolation ensures your session data is never leaked across sites. + +- Fast Search: Built-in "Find in Page" (n/N) with real-time ANSI highlighting. + +- Persistent Data: Bookmarks and Browsing History are automatically saved to ~/.ricochet/ for a seamless experience. + +- Highly Configurable: Customize your User-Agent and default Homepage via a simple config file. + ## Key Features - [x] **High-Performance C++ Core:** Native speed with minimal memory footprint. - [x] **Robust HTML5 Parser:** A custom, fault-tolerant DOM builder for the "wild" web. @@ -23,6 +36,40 @@ Ricochet is built with a strictly modular approach: * **`Layout`**: Calculates box models and word-wrapping for terminal constraints. * **`TUI`**: An event-driven terminal UI handling viewport rendering and user input. +## Usage +```bash +# Start with default homepage +./build/bin/ricochet + +# Open a specific URL +./build/bin/ricochet https://www.gnu.org + +# Use DuckDuckGo smart search +./build/bin/ricochet "linux kernel news" +``` + +## Navigation +| Key | Action | +| :--- | :--- | +| `j / k` | Scroll Down / Up | +| `f` | Follow Link (Hint Mode) | +| `n / N` | Find in Page / Next Match | +| `H / L` | History Back / Forward | +| `h` | View Browsing History | +| `b / B` | Add Bookmark / View Bookmarks | +| `/` | Open URL Prompt | +| `i` | Form Input Mode | +| `?` | Show Help Manual | +| `q` | Quit | + +## Configuration +Ricochet stores data in ~/.ricochet/. You can create config.txt to customize your experience: + +```plaintext +homepage=https://v2ex.com +user_agent=MyCustomBrowser/1.0 +``` + ## Getting Started ### Prerequisites @@ -33,26 +80,35 @@ To build Ricochet, ensure your environment meets the following requirements: * `clang-format` for automated code styling. * `clang-tidy` for static analysis and linting. -### Build Instructions +### Building from Source Ricochet enforces out-of-source builds to keep the project structure clean. By default, the project builds in Debug mode with Address and Undefined Behavior sanitizers enabled. +#### 1. Clone the repository ```bash -# 1. Clone the repository git clone https://github.com/yourusername/ricochet.git cd ricochet +``` -# 2. Configure the project -# This will generate compile commands and set the build type to Release +#### 2. Configure the project +This will generate compile commands and set the build type to Release +```bash cmake -B build -DCMAKE_BUILD_TYPE=Release +``` -# 3. Compile the project using all available cores +#### 3. Compile the project using all available cores +```bash cmake --build build -j$(nproc) +``` -# 4. Launch Ricochet -# The executable is located in the bin/ directory -./build/bin/ricochet https://wikipedia.org +### Install +To install Ricochet to your system (defaulting to /usr/local/bin), run the following command with root privileges: + +```bash +sudo cmake --install build ``` +This will install the `ricochet` executable to the `bin` destination and required libraries to `lib`. + ### Build Parameters & Configuration You can customize the build by passing `-D=` to the initial `cmake -B build` command. @@ -94,15 +150,8 @@ To install the compiled binaries to your system, use: cmake --install build --prefix /usr/local ``` This installs the `ricochet` binary to `bin` and supporting libraries to `lib`. -## Navigation -| Key | Action | -| :--- | :--- | -| `j / k` | Scroll Down / Up | -| `Tab` | Cycle through links | -| `Enter` | Follow selected link | -| `b` | Go Back in history | -| `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. diff --git a/ricochet/include/ricochet/core/browser.hh b/ricochet/include/ricochet/core/browser.hh index 36e876d..ab7a473 100644 --- a/ricochet/include/ricochet/core/browser.hh +++ b/ricochet/include/ricochet/core/browser.hh @@ -33,6 +33,7 @@ public: Browser( Browser&& ) = delete; Browser& operator=( Browser&& ) = delete; + static std::string format_url( std::string input ); static std::string get_configured_homepage(); static int run( std::string_view initial_url ); }; diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc index 571783a..c4dbb08 100644 --- a/ricochet/src/core/browser.cc +++ b/ricochet/src/core/browser.cc @@ -412,30 +412,6 @@ void draw_view( const tui::Terminal& terminal, std::cout.flush(); } -std::string format_url( std::string input ) -{ - if ( input.empty() ) { - return input; - } - - if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { - return input; - } - - const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); - if ( is_domain ) { - input.insert( 0, "https://" ); - return input; - } - - for ( char& ch : input ) { - if ( ch == ' ' ) { - ch = '+'; - } - } - return "https://lite.duckduckgo.com/lite/?q=" + input; -} - std::string get_url_input( const tui::Terminal& terminal ) { auto [width, height] = terminal.get_size(); @@ -468,7 +444,7 @@ std::string get_url_input( const tui::Terminal& terminal ) } terminal.show_cursor( false ); - return format_url( std::move( input ) ); + return Browser::format_url( std::move( input ) ); } // std::string get_link_input( const tui::Terminal& terminal ) @@ -1003,6 +979,30 @@ std::vector wrap_lines( const std::vector& original_li } // namespace +std::string Browser::format_url( std::string input ) +{ + if ( input.empty() ) { + return input; + } + + if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { + return input; + } + + const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); + if ( is_domain ) { + input.insert( 0, "https://" ); + return input; + } + + for ( char& ch : input ) { + if ( ch == ' ' ) { + ch = '+'; + } + } + return "https://lite.duckduckgo.com/lite/?q=" + input; +} + std::string Browser::get_configured_homepage() { std::ifstream file( get_config_path( "config.txt" ) ); diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc index 562861a..d27ab55 100644 --- a/ricochet/src/main.cc +++ b/ricochet/src/main.cc @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -#include #include #include #include @@ -42,13 +41,6 @@ int main( int argc, char* argv[] ) ricochet::core::print_license_notice(); - std::string start_url; - if ( args.size() < 2 ) { - start_url = ricochet::core::Browser::get_configured_homepage(); - } else { - start_url = std::string( args[1] ); - } - if ( args.size() >= 2 && args[1] == "show" ) { if ( args.size() >= 3 && args[2] == "w" ) { ricochet::core::show_warranty(); @@ -63,10 +55,15 @@ int main( int argc, char* argv[] ) return 1; } - if ( !start_url.starts_with( "http://" ) && !start_url.starts_with( "https://" ) ) { - start_url.insert( 0, "https://" ); + std::string start_url; + if ( args.size() < 2 ) { + start_url = ricochet::core::Browser::get_configured_homepage(); + } else { + start_url = std::string( args[1] ); } + start_url = ricochet::core::Browser::format_url( std::move( start_url ) ); + try { return ricochet::core::Browser::run( start_url ); } catch ( const std::exception& e ) { -- cgit v1.2.3