aboutsummaryrefslogtreecommitdiff
path: root/src/net
diff options
context:
space:
mode:
Diffstat (limited to 'src/net')
-rw-r--r--src/net/http_client.cc135
-rw-r--r--src/net/ssl_socket.cc118
2 files changed, 235 insertions, 18 deletions
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
index b31073c..b31976f 100644
--- a/src/net/http_client.cc
+++ b/src/net/http_client.cc
@@ -17,6 +17,7 @@
*/
#include "ricochet/net/http_client.hh"
+#include "ricochet/net/ssl_socket.hh"
#include "ricochet/net/sys/address.hh"
#include "ricochet/net/sys/socket.hh"
@@ -37,13 +38,17 @@ struct ParsedUrl
std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
{
- const std::string_view http_prefix = "http://";
+ std::string_view prefix = "http://";
- if ( !url.starts_with( http_prefix ) ) {
- return std::unexpected( "Only 'http://' URLs are supported in MVP. (e.g., http://example.com)" );
+ if ( url.starts_with( "https://" ) ) {
+ prefix = "https://";
+ } else if ( url.starts_with( "http://" ) ) {
+ prefix = "http://";
+ } else {
+ return std::unexpected( "Only 'http://' or 'https://' URLs are supported." );
}
- url.remove_prefix( http_prefix.length() );
+ url.remove_prefix( prefix.length() );
auto slash_pos = url.find( '/' );
ParsedUrl result;
@@ -57,6 +62,77 @@ std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
return result;
}
+
+std::string dechunk( std::string_view body ) // NOLINT(readability-convert-member-functions-to-static)
+{
+ std::string result;
+ std::size_t pos = 0;
+ while ( pos < body.size() ) {
+ // find hex length end with \r\n
+ auto line_end = body.find( "\r\n", pos );
+ if ( line_end == std::string_view::npos ) {
+ break;
+ }
+
+ // parse hex length
+ const std::string len_str( body.substr( pos, line_end - pos ) );
+ unsigned int chunk_size = 0;
+ try {
+ chunk_size = std::stoul( len_str, nullptr, 16 );
+ } catch ( ... ) {
+ break;
+ }
+
+ if ( chunk_size == 0 ) {
+ break; // end of stream
+ }
+
+ // extract data block (skip \r\n)
+ pos = line_end + 2;
+ if ( pos + chunk_size > body.size() ) {
+ break;
+ }
+ result.append( body.substr( pos, chunk_size ) );
+
+ // skip data block's \r\n
+ pos += chunk_size + 2;
+ }
+ return result;
+}
+
+std::string remove_html_comments( std::string html ) // NOLINT(readability-convert-member-functions-to-static)
+{
+ std::size_t start_pos = 0;
+ while ( ( start_pos = html.find( "<!--", start_pos ) ) != std::string::npos ) {
+ auto end_pos = html.find( "-->", start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + 3 );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
+std::string remove_tag_blocks( std::string html, const std::string& tag_name ) // NOLINT
+{
+ const std::string open_tag = "<" + tag_name;
+ const std::string close_tag = "</" + tag_name + ">";
+ std::size_t start_pos = 0;
+
+ while ( ( start_pos = html.find( open_tag, start_pos ) ) != std::string::npos ) {
+ const std::size_t end_pos = html.find( close_tag, start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + close_tag.length() );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
} // namespace
std::expected<HttpResponse, std::string>
@@ -69,30 +145,46 @@ HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static)
}
auto [host, path] = parsed_url.value();
+ const bool is_https = url.starts_with( "https://" );
+ const std::string service = is_https ? "https" : "http";
try {
- const Address server_addr( host, "http" );
-
- TCPSocket socket;
- socket.connect( server_addr );
+ const Address server_addr( host, service );
const std::string request = std::format( "GET {} HTTP/1.1\r\n"
"Host: {}\r\n"
- "User-Agent: Ricochet/0.1.0\r\n"
+ "User-Agent: Mozilla/5.0 (Ricochet/0.1.0)\r\n"
+ "Accept: text/html\r\n"
+ "Accept-Encoding: identity\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;
+ if ( is_https ) {
+ SSLSocket ssl_socket;
+ ssl_socket.connect( server_addr, host );
+ size_t bytes_written = 0;
+ while ( bytes_written < request.size() ) {
+ bytes_written += ssl_socket.write( request.substr( bytes_written ) );
+ }
+ while ( !ssl_socket.eof() ) {
+ std::string buffer;
+ ssl_socket.read( buffer );
+ raw_response += buffer;
+ }
+ } else {
+ TCPSocket tcp_socket;
+ tcp_socket.connect( server_addr );
+ size_t bytes_written = 0;
+ while ( bytes_written < request.size() ) {
+ bytes_written += tcp_socket.write( request.substr( bytes_written ) );
+ }
+ while ( !tcp_socket.eof() ) {
+ std::string buffer;
+ tcp_socket.read( buffer );
+ raw_response += buffer;
+ }
}
HttpResponse response;
@@ -111,6 +203,13 @@ HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static)
response.body = raw_response;
}
+ if ( raw_response.contains( "Transfer-Encoding: chunked" ) ) {
+ response.body = dechunk( response.body );
+ }
+
+ response.body = remove_html_comments( response.body );
+ response.body = remove_tag_blocks( response.body, "script" );
+ response.body = remove_tag_blocks( response.body, "style" );
return response;
} catch ( const std::exception& e ) {
return std::unexpected( std::string( "Network Error: " ) + e.what() );
diff --git a/src/net/ssl_socket.cc b/src/net/ssl_socket.cc
new file mode 100644
index 0000000..a31b564
--- /dev/null
+++ b/src/net/ssl_socket.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/ssl_socket.hh"
+#include <iostream>
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#include <stdexcept>
+
+namespace ricochet::net {
+
+// --- RAII Deleters ---
+
+void SslCtxDeleter::operator()( SSL_CTX* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_CTX_free( ptr );
+ }
+}
+
+void SslDeleter::operator()( SSL* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_free( ptr );
+ }
+}
+
+// SSLSocket Implementation
+
+SSLSocket::SSLSocket()
+{
+ const SSL_METHOD* method = TLS_client_method();
+ ctx_.reset( SSL_CTX_new( method ) );
+ if ( !ctx_ ) {
+ throw std::runtime_error( "Failed to create SSL_CTX" );
+ }
+
+ if ( SSL_CTX_set_default_verify_paths( ctx_.get() ) != 1 ) {
+ throw std::runtime_error( "Failed to load default CA certificates" );
+ }
+}
+
+void SSLSocket::connect( const Address& address, const std::string& hostname )
+{
+ // TCP Connect
+ underlying_socket_.connect( address );
+
+ ssl_.reset( SSL_new( ctx_.get() ) );
+ if ( !ssl_ ) {
+ throw std::runtime_error( "Failed to create SSL object" );
+ }
+
+ // Setup SNI (Server Name Indication)
+ if ( SSL_set_tlsext_host_name( ssl_.get(), hostname.c_str() ) != 1 ) {
+ // Some old servers do not support SNI, here we can ignore the error or throw an exception, depending on the
+ // requirements
+ std::cerr << "Warning: Failed to set SNI" << '\n';
+ }
+
+ SSL_set_fd( ssl_.get(), underlying_socket_.fd_num() );
+
+ if ( SSL_connect( ssl_.get() ) != 1 ) {
+ throw std::runtime_error( "TLS Handshake failed!" );
+ }
+}
+
+std::size_t SSLSocket::write( std::string_view data )
+{
+ if ( !ssl_ ) {
+ return 0;
+ }
+
+ const int bytes_written = SSL_write( ssl_.get(), data.data(), static_cast<int>( data.size() ) );
+ if ( bytes_written <= 0 ) {
+ throw std::runtime_error( "SSL write failed" );
+ }
+ return static_cast<std::size_t>( bytes_written );
+}
+
+void SSLSocket::read( std::string& buffer )
+{
+ if ( !ssl_ ) {
+ return;
+ }
+
+ std::array<char, 4096> buf {};
+ // Read decrypted data from the encrypted channel
+ const int bytes_read = SSL_read( ssl_.get(), buf.data(), static_cast<int>( buf.size() ) );
+
+ if ( bytes_read > 0 ) {
+ buffer.append( buf.data(), static_cast<std::size_t>( bytes_read ) );
+ } else {
+ // Read 0 or negative, representing the server closed the connection (EOF)
+ eof_ = true;
+ }
+}
+
+bool SSLSocket::eof() const
+{
+ return eof_;
+}
+
+} // namespace ricochet::net \ No newline at end of file