/* * 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/config.hh" #include "ricochet/net/cookie_jar.hh" #include "ricochet/net/http_utils.hh" #include "ricochet/net/ssl_socket.hh" #include "ricochet/net/sys/address.hh" #include "ricochet/net/sys/socket.hh" #include "ricochet/net/url_parser.hh" #include #include #include #include namespace ricochet::net { namespace { struct FetchResult { std::string raw_response {}; std::string error {}; }; FetchResult do_network_request( const std::string& host, const std::string& service, // NOLINT(bugprone-easily-swappable-parameters) const std::string& path, // NOLINT(bugprone-easily-swappable-parameters) bool is_https, const std::string& method, // NOLINT(bugprone-easily-swappable-parameters) const std::string& body, const std::string& cookie_header ) // NOLINT(bugprone-easily-swappable-parameters) { FetchResult res; try { const Address server_addr( host, service ); std::string request = std::format( "{} {} HTTP/1.1\r\n" "Host: {}\r\n" "User-Agent: {}\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.9\r\n" "DNT: 1\r\n" "{}" "Connection: close\r\n", method, path, host, get_browser_config().user_agent, cookie_header ); if ( !body.empty() ) { request += std::format( "Content-Length: {}\r\n" "Content-Type: application/x-www-form-urlencoded\r\n", body.length() ); } request += "\r\n" + body; if ( is_https ) { SSLSocket ssl_socket; ssl_socket.connect( server_addr, host ); for ( size_t bw = 0; bw < request.size(); ) { bw += ssl_socket.write( request.substr( bw ) ); } while ( !ssl_socket.eof() ) { std::string buffer; ssl_socket.read( buffer ); res.raw_response += buffer; } } else { TCPSocket tcp_socket; tcp_socket.connect( server_addr ); for ( size_t bw = 0; bw < request.size(); ) { bw += tcp_socket.write( request.substr( bw ) ); } while ( !tcp_socket.eof() ) { std::string buffer; tcp_socket.read( buffer ); res.raw_response += buffer; } } } catch ( const std::exception& e ) { res.error = std::string( "Network Error: " ) + e.what(); } return res; } void parse_response_headers( const std::string& raw_response, // NOLINT(bugprone-easily-swappable-parameters) const std::string& host, // NOLINT(bugprone-easily-swappable-parameters) HttpResponse& response, std::string& headers ) { const auto header_end = raw_response.find( "\r\n\r\n" ); headers = ( header_end != std::string::npos ) ? raw_response.substr( 0, header_end ) : raw_response; update_cookies( headers, host ); if ( header_end != std::string::npos ) { response.body = raw_response.substr( header_end + 4 ); std::size_t ct_pos = headers.find( "Content-Type: " ); if ( ct_pos == std::string::npos ) { ct_pos = headers.find( "content-type: " ); } if ( ct_pos != std::string::npos ) { ct_pos += 14; const std::size_t end_pos = headers.find( '\r', ct_pos ); if ( end_pos != std::string::npos ) { response.content_type = headers.substr( ct_pos, end_pos - ct_pos ); } } 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; response.status_code = 0; } } void sanitize_response_body( HttpResponse& response, const std::string& raw_response ) { if ( raw_response.contains( "Transfer-Encoding: chunked" ) ) { response.body = dechunk( response.body ); } std::string ct_lower = response.content_type; for ( char& c : ct_lower ) { if ( c >= 'A' && c <= 'Z' ) { c = static_cast( c + 32 ); } } if ( ct_lower.starts_with( "text/" ) || ct_lower.contains( "xml" ) ) { response.body = remove_html_comments( response.body ); response.body = remove_tag_blocks( response.body, "script" ); response.body = remove_tag_blocks( response.body, "style" ); } } } // namespace std::expected HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static) std::string_view url, // NOLINT(bugprone-easily-swappable-parameters) std::string_view method, // NOLINT(bugprone-easily-swappable-parameters) std::string_view body ) const // NOLINT(bugprone-easily-swappable-parameters) { std::string current_url( url ); std::string current_method( method ); std::string current_body( body ); for ( int redirect_count = 0; redirect_count < 5; ++redirect_count ) { const auto parsed_url = parse_url( current_url ); if ( !parsed_url.has_value() ) { return std::unexpected( parsed_url.error() ); } const auto [host, path] = parsed_url.value(); const bool is_https = current_url.starts_with( "https://" ); const std::string service = is_https ? "https" : "http"; const std::string cookie_header = build_cookie_header( host ); const FetchResult net_res = do_network_request( host, service, path, is_https, current_method, current_body, cookie_header ); if ( !net_res.error.empty() ) { return std::unexpected( net_res.error ); } const std::string& raw_response = net_res.raw_response; HttpResponse response; std::string headers; parse_response_headers( raw_response, host, response, headers ); if ( response.status_code >= 300 && response.status_code < 400 ) { const std::string next_url = extract_redirect_url( headers, is_https, host ); if ( !next_url.empty() ) { current_url = next_url; if ( response.status_code == 301 || response.status_code == 302 || response.status_code == 303 ) { current_method = "GET"; current_body.clear(); } continue; } } sanitize_response_body( response, raw_response ); return response; } return std::unexpected( "Too many redirects" ); } } // namespace ricochet::net