Skip to content

Commit 7fc443a

Browse files
etrclaude
andcommitted
refactor: extract ip_representation impl from http_utils.cpp (730 → 493)
Step 4 of the FILE_LOC_MAX ratchet. Moves the ip_representation method bodies — both ctors, parse_ipv4 / parse_ipv6 + the carved-out helpers (compute_ipv6_omitted_segments, parse_nested_ipv4, apply_ipv6_part), operator< + its anonymous-namespace helpers (accumulate_octet_score, is_v4_mapped_prefix_octet_pair, ipv4_mapped_prefix_invalid) — into src/detail/ip_representation.cpp. Co-extracts get_ip_str / get_port (the sockaddr -> string helpers). They're declared in httpserver/http_utils.hpp as namespace-level free functions, but functionally they're "sockaddr -> IP textual form" — the same concern as ip_representation's sockaddr ctor. Lives alongside its peers in the new TU. http_utils.cpp now carries the URL / filename helpers, http_unescape, load_file, header/arg dump helpers, base_unescaper, the MHD-thin-wrapper functions (reason_phrase, is_feature_supported, get_mhd_version), and the TASK-020 static_assert enum pin block. The file is purely "string + HTTP misc utilities" again; the network / address concern has moved out. The new TU adds itself to libhttpserver_la_SOURCES in src/Makefile.am. The two local bit-twiddling macros (CHECK_BIT / CLEAR_BIT) are re-declared at the top of the new TU; both http_utils.cpp and the new TU need them, and a shared header for two one-line macros would be overkill. FILE_LOC_MAX stays at 2700 -- webserver.cpp (2673) still pins it. Offender list down to three files. Verification: make check 51/51 PASS (includes hygiene, install-layout, doxygen, examples, readme, release-notes) ./scripts/check-file-size.sh PASS at FILE_LOC_MAX=2700 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8464996 commit 7fc443a

4 files changed

Lines changed: 308 additions & 242 deletions

File tree

scripts/check-file-size.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
# src/webserver.cpp 2673
3434
# src/http_request.cpp 1175
3535
# src/httpserver/webserver.hpp 845
36-
# src/http_utils.cpp 730
3736
#
3837
# FILE_LOC_MAX is pinned by the largest unfixed file (webserver.cpp at
3938
# 2673), so it cannot drop until the top offender is decomposed. The

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ lib_LTLIBRARIES = libhttpserver.la
2525
# builds. The WS-off branch in websocket_handler.cpp provides stub
2626
# definitions (every member throws feature_unavailable except is_valid()
2727
# which returns false).
28-
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp detail/http_endpoint.cpp detail/body.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp
2929
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
3030
# Detail headers (httpserver/detail/*.hpp) live here so they cannot leak to
3131
# downstream consumers — the public surface comes in through <httpserver.hpp>.

src/detail/ip_representation.cpp

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
#include "httpserver/ip_representation.hpp"
22+
23+
// sockaddr family pulls in the platform's BSD-socket headers. The
24+
// public ip_representation.hpp forward-declares `struct sockaddr` at
25+
// file scope; the implementations below need the full definition plus
26+
// inet_ntop / sockaddr_in[6] / NI_MAXHOST, which live in the headers
27+
// included here.
28+
#if defined(_WIN32) && !defined(__CYGWIN__)
29+
#include <winsock2.h>
30+
#include <ws2tcpip.h>
31+
#else // WIN32 check
32+
#include <arpa/inet.h>
33+
#include <netdb.h>
34+
#include <netinet/in.h>
35+
#include <sys/socket.h>
36+
#include <sys/types.h>
37+
#endif // WIN32 check
38+
39+
#include <stdint.h>
40+
#include <stdlib.h>
41+
42+
#include <algorithm>
43+
#include <iomanip>
44+
#include <sstream>
45+
#include <stdexcept>
46+
#include <string>
47+
#include <vector>
48+
49+
#include "httpserver/constants.hpp"
50+
#include "httpserver/http_utils.hpp"
51+
#include "httpserver/string_utilities.hpp"
52+
53+
// Local bit ops mirror the ones in http_utils.cpp; both TUs need them
54+
// and a shared header would be overkill for two one-line macros.
55+
#pragma GCC diagnostic ignored "-Warray-bounds"
56+
#define CHECK_BIT(var, pos) ((var) & (1 << (pos)))
57+
#define CLEAR_BIT(var, pos) ((var) &= ~(1 << (pos)))
58+
59+
namespace httpserver {
60+
namespace http {
61+
62+
std::string get_ip_str(const struct sockaddr *sa) {
63+
if (!sa) throw std::invalid_argument("socket pointer is null");
64+
65+
char to_ret[NI_MAXHOST];
66+
if (AF_INET6 == sa->sa_family) {
67+
inet_ntop(AF_INET6, &((reinterpret_cast<const sockaddr_in6*>(sa))->sin6_addr), to_ret, INET6_ADDRSTRLEN);
68+
return to_ret;
69+
} else if (AF_INET == sa->sa_family) {
70+
inet_ntop(AF_INET, &((reinterpret_cast<const sockaddr_in*>(sa))->sin_addr), to_ret, INET_ADDRSTRLEN);
71+
return to_ret;
72+
} else {
73+
throw std::invalid_argument("IP family must be either AF_INET or AF_INET6");
74+
}
75+
}
76+
77+
uint16_t get_port(const struct sockaddr* sa) {
78+
if (!sa) throw std::invalid_argument("socket pointer is null");
79+
80+
if (sa->sa_family == AF_INET) {
81+
return (reinterpret_cast<const struct sockaddr_in*>(sa))->sin_port;
82+
} else if (sa->sa_family == AF_INET6) {
83+
return (reinterpret_cast<const struct sockaddr_in6*>(sa))->sin6_port;
84+
} else {
85+
throw std::invalid_argument("IP family must be either AF_INET or AF_INET6");
86+
}
87+
}
88+
89+
ip_representation::ip_representation(const struct sockaddr* ip) {
90+
std::fill(pieces, pieces + 16, 0);
91+
if (ip->sa_family == AF_INET) {
92+
ip_version = http_utils::IPV4;
93+
const in_addr* sin_addr_pt = &((reinterpret_cast<const struct sockaddr_in*>(ip))->sin_addr);
94+
for (int i = 0; i < 4; i++) {
95+
pieces[12 + i] = (reinterpret_cast<const u_char*>(sin_addr_pt))[i];
96+
}
97+
} else {
98+
ip_version = http_utils::IPV6;
99+
const in6_addr* sin_addr6_pt = &((reinterpret_cast<const struct sockaddr_in6*>(ip))->sin6_addr);
100+
for (int i = 0; i < 16; i++) {
101+
pieces[i] = (reinterpret_cast<const u_char*>(sin_addr6_pt))[i];
102+
}
103+
}
104+
mask = constants::DEFAULT_MASK_VALUE;
105+
}
106+
107+
namespace {
108+
109+
// "::ffff:1.2.3.4" / "::1.2.3.4" prefix invariants: bytes 10-11 must
110+
// be either 0x00 (pure ::-mapped) or 0xFF (v4-mapped). Lifted out of
111+
// parse_nested_ipv4 so the surrounding function stays below the CCN
112+
// bar.
113+
bool ipv4_mapped_prefix_invalid(uint16_t a, uint16_t b) {
114+
return (a != 0 && a != 255) || (b != 0 && b != 255);
115+
}
116+
117+
} // namespace
118+
119+
void ip_representation::parse_ipv4(const std::string& ip) {
120+
ip_version = http_utils::IPV4;
121+
auto parts = string_utilities::string_split(ip, '.');
122+
if (parts.size() != 4) {
123+
throw std::invalid_argument("IP is badly formatted. Max 4 parts in IPV4.");
124+
}
125+
for (unsigned int i = 0; i < parts.size(); i++) {
126+
if (parts[i] == "*") {
127+
CLEAR_BIT(mask, 12+i);
128+
continue;
129+
}
130+
pieces[12+i] = strtol(parts[i].c_str(), nullptr, 10);
131+
if (pieces[12+i] > 255) {
132+
throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
133+
}
134+
}
135+
}
136+
137+
unsigned int ip_representation::compute_ipv6_omitted_segments(std::vector<std::string>& parts) {
138+
unsigned int omitted = 8 - (parts.size() - 1);
139+
if (omitted == 0) return 0;
140+
141+
int empty_count = 0;
142+
for (unsigned int i = 0; i < parts.size(); i++) {
143+
if (parts[i].size() == 0) empty_count++;
144+
}
145+
if (empty_count <= 1) return omitted;
146+
147+
// > 1 empty segments: the only legal shape is a leading "::" (which
148+
// string_split produces as two consecutive empties) on an IPv6 that
149+
// also has a nested IPv4 trailing dotted-quad — the "::" produces
150+
// one extra empty segment beyond the canonical single placeholder.
151+
if (parts.back().find('.') != std::string::npos) omitted -= 1;
152+
153+
const bool leading_double_colon =
154+
empty_count == 2 && parts[0].empty() && parts[1].empty();
155+
if (!leading_double_colon) {
156+
throw std::invalid_argument(
157+
"IP is badly formatted. Cannot have more than one omitted segment in IPV6.");
158+
}
159+
omitted += 1;
160+
parts.erase(parts.begin());
161+
return omitted;
162+
}
163+
164+
void ip_representation::parse_nested_ipv4(const std::vector<std::string>& parts,
165+
unsigned int i, int y) {
166+
if (y != 12) {
167+
throw std::invalid_argument("IP is badly formatted. Missing parts before nested IPV4.");
168+
}
169+
if (i != parts.size() - 1) {
170+
throw std::invalid_argument("IP is badly formatted. Nested IPV4 should be at the end");
171+
}
172+
auto subparts = string_utilities::string_split(parts[i], '.');
173+
if (subparts.size() != 4) {
174+
throw std::invalid_argument("IP is badly formatted. Nested IPV4 can have max 4 parts.");
175+
}
176+
// Bytes 0-9 must be zero; bytes 10-11 must be 0x00 or 0xFF.
177+
for (unsigned int k = 0; k < 10; k++) {
178+
if (pieces[k] != 0) {
179+
throw std::invalid_argument(
180+
"IP is badly formatted. Nested IPV4 can be preceded only by 0 "
181+
"(and, optionally, two 255 octects)");
182+
}
183+
}
184+
if (ipv4_mapped_prefix_invalid(pieces[10], pieces[11])) {
185+
throw std::invalid_argument(
186+
"IP is badly formatted. Nested IPV4 can be preceded only by 0 "
187+
"(and, optionally, two 255 octects)");
188+
}
189+
for (unsigned int ii = 0; ii < subparts.size(); ii++) {
190+
if (subparts[ii] == "*") {
191+
CLEAR_BIT(mask, y+ii);
192+
continue;
193+
}
194+
pieces[y+ii] = strtol(subparts[ii].c_str(), nullptr, 10);
195+
if (pieces[y+ii] > 255) {
196+
throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
197+
}
198+
}
199+
}
200+
201+
void ip_representation::apply_ipv6_part(std::vector<std::string>& parts, unsigned int i,
202+
int& y, unsigned int omitted) {
203+
auto& part = parts[i];
204+
if (part == "*") {
205+
CLEAR_BIT(mask, y);
206+
CLEAR_BIT(mask, y+1);
207+
y += 2;
208+
return;
209+
}
210+
if (part.empty()) {
211+
// Placeholder for one or more omitted segments. Zero-fill the
212+
// implied slots; the bump is `omitted` segments x 2 bytes each.
213+
for (unsigned int o = 0; o < omitted; o++) {
214+
pieces[y] = 0;
215+
pieces[y+1] = 0;
216+
y += 2;
217+
}
218+
return;
219+
}
220+
if (part.size() < 4) {
221+
// Pad short hex group to 4 chars (e.g. "f" -> "000f").
222+
std::stringstream ss;
223+
ss << std::setfill('0') << std::setw(4) << part;
224+
part = ss.str();
225+
}
226+
if (part.size() == 4) {
227+
pieces[y] = strtol(part.substr(0, 2).c_str(), nullptr, 16);
228+
pieces[y+1] = strtol(part.substr(2, 2).c_str(), nullptr, 16);
229+
y += 2;
230+
return;
231+
}
232+
if (part.find('.') == std::string::npos) {
233+
throw std::invalid_argument(
234+
"IP is badly formatted. IPV6 parts can have max 4 characters (or nest an IPV4)");
235+
}
236+
parse_nested_ipv4(parts, i, y);
237+
}
238+
239+
void ip_representation::parse_ipv6(const std::string& ip) {
240+
ip_version = http_utils::IPV6;
241+
auto parts = string_utilities::string_split(ip, ':', false);
242+
if (parts.size() > 8) {
243+
throw std::invalid_argument("IP is badly formatted. Max 8 parts in IPV6.");
244+
}
245+
unsigned int omitted = compute_ipv6_omitted_segments(parts);
246+
int y = 0;
247+
for (unsigned int i = 0; i < parts.size(); i++) {
248+
apply_ipv6_part(parts, i, y, omitted);
249+
}
250+
}
251+
252+
ip_representation::ip_representation(const std::string& ip) {
253+
mask = constants::DEFAULT_MASK_VALUE;
254+
std::fill(pieces, pieces + 16, 0);
255+
if (ip.find(':') != std::string::npos) {
256+
parse_ipv6(ip);
257+
} else {
258+
parse_ipv4(ip);
259+
}
260+
}
261+
262+
namespace {
263+
264+
// Add (16 - i) * piece[i] for both ip representations when both have
265+
// the i-th octet masked in. Pulled out of operator< so the surrounding
266+
// function stays below the CCN bar.
267+
void accumulate_octet_score(const ip_representation& a,
268+
const ip_representation& b, int i,
269+
int64_t& a_score, int64_t& b_score) {
270+
if (!(CHECK_BIT(a.mask, i) && CHECK_BIT(b.mask, i))) return;
271+
a_score += (16 - i) * a.pieces[i];
272+
b_score += (16 - i) * b.pieces[i];
273+
}
274+
275+
// True when both v4-mapped-prefix octets on a and b are 0x00 or 0xFF.
276+
// Mirrors the "::ffff:" / "::"-prefix invariant from parse_nested_ipv4.
277+
bool is_v4_mapped_prefix_octet_pair(uint16_t a, uint16_t b) {
278+
return (a == 0x00 || a == 0xFF) && (b == 0x00 || b == 0xFF);
279+
}
280+
281+
} // namespace
282+
283+
bool ip_representation::operator <(const ip_representation& b) const {
284+
int64_t this_score = 0;
285+
int64_t b_score = 0;
286+
for (int i = 0; i < 16; i++) {
287+
if (i == 10 || i == 11) continue;
288+
accumulate_octet_score(*this, b, i, this_score, b_score);
289+
}
290+
291+
if (this_score == b_score
292+
&& is_v4_mapped_prefix_octet_pair(pieces[10], b.pieces[10])
293+
&& is_v4_mapped_prefix_octet_pair(pieces[11], b.pieces[11])) {
294+
return false;
295+
}
296+
297+
accumulate_octet_score(*this, b, 10, this_score, b_score);
298+
accumulate_octet_score(*this, b, 11, this_score, b_score);
299+
300+
return this_score < b_score;
301+
}
302+
303+
} // namespace http
304+
} // namespace httpserver

0 commit comments

Comments
 (0)