UPnPsdk 0.1
Universal Plug and Play +, Software Development Kit
 
Loading...
Searching...
No Matches
sockaddr.cpp
Go to the documentation of this file.
1// Copyright (C) 2022+ GPL 3 and higher by Ingo Höft, <Ingo@Hoeft-online.de>
2// Redistribution only with this Copyright remark. Last modified: 2025-05-05
9#include <UPnPsdk/synclog.hpp>
10#include <umock/netdb.hpp>
12#include <algorithm>
13#include <regex>
15
16namespace UPnPsdk {
17
18namespace {
19
20// Free function to logical compare two sockaddr structures
21// --------------------------------------------------------
32bool sockaddrcmp(const ::sockaddr_storage* a_ss1,
33 const ::sockaddr_storage* a_ss2) noexcept {
34 // Throws no exception.
35 if (a_ss1 == nullptr && a_ss2 == nullptr)
36 return true;
37 if (a_ss1 == nullptr || a_ss2 == nullptr)
38 return false;
39
40 switch (a_ss1->ss_family) {
41 case AF_UNSPEC:
42 if (a_ss2->ss_family != AF_UNSPEC)
43 return false;
44 break;
45
46 case AF_INET6: {
47 // I compare ipv6 addresses which are stored in a 16 byte array
48 // (unsigned char s6_addr[16]). So we have to use memcmp() for
49 // comparison.
50 const ::sockaddr_in6* const s6_addr1 =
51 reinterpret_cast<const ::sockaddr_in6*>(a_ss1);
52 const ::sockaddr_in6* const s6_addr2 =
53 reinterpret_cast<const ::sockaddr_in6*>(a_ss2);
54
55 if (a_ss2->ss_family != AF_INET6 ||
56 ::memcmp(&s6_addr1->sin6_addr, &s6_addr2->sin6_addr,
57 sizeof(in6_addr)) != 0 ||
58 s6_addr1->sin6_port != s6_addr2->sin6_port ||
59 s6_addr1->sin6_scope_id != s6_addr2->sin6_scope_id)
60 return false;
61 } break;
62
63 case AF_INET: {
64 const ::sockaddr_in* const s_addr1 =
65 reinterpret_cast<const ::sockaddr_in*>(a_ss1);
66 const ::sockaddr_in* const s_addr2 =
67 reinterpret_cast<const ::sockaddr_in*>(a_ss2);
68
69 if (a_ss2->ss_family != AF_INET ||
70 s_addr1->sin_addr.s_addr != s_addr2->sin_addr.s_addr ||
71 s_addr1->sin_port != s_addr2->sin_port)
72 return false;
73 } break;
74
75 default:
76 return false;
77 }
78
79 return true;
80}
81
82
114int to_port( //
116 const std::string& a_port_str,
119 in_port_t* const a_port_num = nullptr) noexcept {
120 TRACE("Executing to_port() with port=\"" + a_port_str + "\"")
121
122 // // Trim input string.
123 // std::string port_str;
124 // auto start = a_port_str.find_first_not_of(" \t");
125 // // Avoid exception with program terminate if all spaces/tabs.
126 // if (start != a_port_str.npos) {
127 // auto end = a_port_str.find_last_not_of(" \t");
128 // port_str = a_port_str.substr(start, (end - start) + 1);
129 // }
130
131 // Only non empty strings. I have to check this to avoid stoi() exception
132 // below.
133 if (a_port_str.empty()) {
134 if (a_port_num != nullptr)
135 *a_port_num = 0;
136 return 0;
137 }
138
139 // Now I check if the string are all digit characters
140 bool nonzero{false};
141 for (char ch : a_port_str) {
142 if (!std::isdigit(static_cast<unsigned char>(ch))) {
143 return -1;
144 } else if (ch != '0') {
145 nonzero = true;
146 }
147 }
148
149 // Only strings with max. 5 char may be valid (uint16_t has max. 65535).
150 if (a_port_str.length() > 5) {
151 if (nonzero)
152 return 1; // value valid but more than 5 char.
153 else
154 return -1; // string is all zero with more than 5 char.
155 }
156
157 // Valid positive number but is it within the port range (uint16_t)?
158 // stoi may throw std::invalid_argument if no conversion could be
159 // performed or std::out_of_range. But with the prechecked number string
160 // this should never be thrown.
161 int port = std::stoi(a_port_str);
162 if (port > 65535) {
163 return 1;
164 } else if (a_port_num != nullptr) {
165 // Type cast is no problem because the port value is checked to be
166 // 0..65635 so it always fit into in_port_t(uint16_t).
167 *a_port_num = static_cast<in_port_t>(port);
168 }
169
170 return 0;
171}
172
173} // anonymous namespace
174
175
176// Free function to split inet address and port(service)
177// -----------------------------------------------------
178// For port conversion:
179// Don't use '::htons' (with colons) instead of 'htons', MacOS don't like it.
180// 'sin6_port' is also 'sin_port' due to union.
181//
182// Unique pattern recognition, port delimiter is always ':'
183// Starting with '['
184// Pattern e.g.: [2001:db8::1%1]:50001
185// [2001:db8::2]:
186// [2001:db8::3]
187// [::ffff:142.250.185.99]:50001
188// Starting with "::"
189// ::
190// ::1
191// ::1] // invalid
192// ::ffff:142.250.185.99
193// ::101.45.75.219 // deprecated
194// Starting with ':' and is port
195// :50002
196// :https
197// Containing '.'
198// 127.0.0.4:50003
199// 127.0.0.5:
200// 127.0.0.6
201// Containing one ':'
202// example.com:
203// example.com:50004
204// localhost:
205// localhost:50005
206// Is port
207// 50006
208// Remaining
209// 2001:db8::7
210void split_addr_port(const std::string& a_addr_str, std::string& a_addr,
211 std::string& a_serv) {
212 TRACE("Executing split_addr_port()")
213 // Special cases
214 if (a_addr_str.empty()) {
215 // An empty address string clears address/port.
216 a_addr.clear();
217 a_serv.clear();
218 return;
219 }
220
221 std::string addr_str;
222 std::string serv_str;
223
224 size_t pos{};
225 if (a_addr_str.length() < 2) {
226 // The shortest possible ip address is "::". This helps to avoid string
227 // exceptions 'out_of_range'.
228 addr_str = a_addr_str; // Give it back as (possible) address.
229
230 } else if (a_addr_str.front() == '[') {
231 // Starting with '[', split address if required
232 if ((pos = a_addr_str.find("]:")) != std::string::npos) {
233 addr_str = a_addr_str.substr(0, pos + 1); // Get IP address
234 serv_str = a_addr_str.substr(pos + 2); // Get port string
235 if (serv_str.empty())
236 serv_str = '0';
237 } else {
238 addr_str = a_addr_str; // Get IP address
239 }
240
241 } else if (a_addr_str.front() == ':' && a_addr_str[1] == ':') {
242 // Starting with "::", this cannot have a port.
243 addr_str = a_addr_str;
244
245 } else if (a_addr_str.front() == ':') {
246 // Starting with ':' and is port
247 in_port_t port{};
248 switch (to_port(a_addr_str.substr(1), &port)) {
249 case -1:
250 // No numeric port, check for alphanum port.
251 serv_str = a_addr_str.substr(1);
252 break;
253 case 0:
254 // Only port given, set only port.
255 serv_str = std::to_string(port);
256 break;
257 case 1:
258 // Value not in range 0..65535.
259 goto exit_overrun;
260 }
261 } else if (a_addr_str.find_first_of('.') != std::string::npos) {
262 // Containing '.'
263 if ((pos = a_addr_str.find_last_of(':')) != std::string::npos) {
264 addr_str = a_addr_str.substr(0, pos); // Get IP address
265 serv_str = a_addr_str.substr(pos + 1); // Get port string
266 if (serv_str.empty())
267 serv_str = '0';
268 } else {
269 // No port, set only address.
270 addr_str = a_addr_str;
271 }
272 } else if (std::ranges::count(a_addr_str, ':') == 1) {
273 // Containing one ':'
274 pos = a_addr_str.find_last_of(':');
275 addr_str = a_addr_str.substr(0, pos); // Get IP address
276 serv_str = a_addr_str.substr(pos + 1); // Get port string
277 if (serv_str.empty())
278 serv_str = '0';
279 } else {
280 // Is port
281 in_port_t port{};
282 switch (to_port(a_addr_str, &port)) {
283 case -1:
284 // Remaining
285 // is either only numeric address, or any alphanumeric identifier.
286 addr_str = a_addr_str;
287 break;
288 case 0:
289 // Only port given, set only port.
290 serv_str = std::to_string(port);
291 break;
292 case 1:
293 // Value not in range 0..65535
294 goto exit_overrun;
295 }
296 }
297
298 // Check for valid port. ::getaddrinfo accepts invalid ports > 65535.
299 switch (to_port(serv_str)) {
300 case -1:
301 case 0:
302 break;
303 default:
304 goto exit_overrun;
305 }
306
307 // remove surounding brackets if any, shortest possible netaddress is
308 // "[::]"
309 if (addr_str.length() >= 4 && addr_str.front() == '[' &&
310 addr_str.back() == ']' && std::ranges::count(addr_str, ':') >= 2 &&
311 addr_str.find_first_of('.') == std::string::npos) {
312
313 // Remove surounding brackets
314 a_addr = addr_str.substr(1, addr_str.length() - 2);
315 } else {
316 // Here I have exactly to look for an IPv6 mapped IPv4 address. I could
317 // not find any general distinctions, I must use expensive regex. But I
318 // do it here only when realy needed. Hints found at
319 // REF: [Regular expression that matches valid IPv6 addresses]
320 // (https://stackoverflow.com/a/17871737/5014688)
321 const std::regex addr_regex(
322 "\\[::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}"
323 "[0-"
324 "9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\]",
325 std::regex_constants::icase);
326
327 if (std::regex_match(addr_str, addr_regex)) {
328 // Remove surounding brackets.
329 a_addr = addr_str.substr(1, addr_str.length() - 2);
330 } else {
331 // Haven't found a valid numeric ip address, no need to remove
332 // brackets.
333 a_addr = addr_str;
334 }
335 }
336 a_serv = serv_str;
337
338 return;
339
340exit_overrun:
341 throw std::range_error(
342 UPnPsdk_LOGEXCEPT("MSG1127") "Number string from \"" + a_addr_str +
343 "\" for port is out of range 0..65535.");
344}
345
346// Specialized sockaddr_structure
347// ==============================
348// Constructor
349SSockaddr::SSockaddr(){
350 TRACE2(this, " Construct SSockaddr()") //
351}
352
353// Destructor
354SSockaddr::~SSockaddr() {
355 TRACE2(this, " Destruct SSockaddr()")
356 // Destroy structure
357 ::memset(&m_sa_union, 0xAA, sizeof(m_sa_union));
358}
359
360// Get reference to the sockaddr_storage structure.
361// Only as example, we don't use it.
362// SSockaddr::operator const ::sockaddr_storage&() const {
363// return this->ss;
364// }
365
366// Copy constructor
367// ----------------
368SSockaddr::SSockaddr(const SSockaddr& that) {
369 TRACE2(this, " Construct copy SSockaddr()")
370 m_sa_union = that.m_sa_union;
371}
372
373// Copy assignment operator
374// ------------------------
376 TRACE2(this,
377 " Executing SSockaddr::operator=(SSockaddr) (struct assign op).")
378 std::swap(m_sa_union, that.m_sa_union);
379
380 return *this;
381}
382
383// Assignment operator= to set socket address from string.
384// -------------------------------------------------------
385void SSockaddr::operator=(const std::string& a_addr_str) {
386 TRACE2(this, " Executing SSockaddr::operator=(" + a_addr_str + ")")
387
388 if (a_addr_str.empty()) {
389 // This clears the complete socket address.
390 ::memset(&m_sa_union, 0, sizeof(m_sa_union));
391 m_sa_union.ss.ss_family = AF_UNSPEC;
392 return;
393 }
394 std::string ai_addr_str;
395 std::string ai_port_str;
396
397 // Throws exception std::out_of_range().
398 split_addr_port(a_addr_str, ai_addr_str, ai_port_str);
399
400 // With an empty address part (e.g. ":50001") only set the port and leave
401 // the (old) address unmodified.
402 if (ai_addr_str.empty()) {
403 in_port_t port;
404 if (to_port(ai_port_str, &port) != 0)
405 goto exit_fail;
406 m_sa_union.sin6.sin6_port = htons(port);
407 return;
408 }
409
410 { // Block needed to avoid error: "goto label crosses initialization".
411 // Provide resources for ::getaddrinfo()
412 // ai_flags ensure that only numeric values are accepted.
413 ::addrinfo hints{};
414 hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
415 hints.ai_family = AF_UNSPEC;
416 ::addrinfo* res{nullptr}; // Result from getaddrinfo().
417
418 // Call ::getaddrinfo() to check the ip address string.
419 int ret = umock::netdb_h.getaddrinfo(ai_addr_str.c_str(),
420 ai_port_str.c_str(), &hints, &res);
421 if (ret != 0) {
422 umock::netdb_h.freeaddrinfo(res);
423 goto exit_fail;
424 } else {
425 if (ai_port_str.empty()) {
426 // Preserve old port
427 in_port_t port = m_sa_union.sin6.sin6_port;
428 ::memcpy(&m_sa_union, res->ai_addr, sizeof(m_sa_union));
429 m_sa_union.sin6.sin6_port = port;
430 } else {
431 ::memcpy(&m_sa_union, res->ai_addr, sizeof(m_sa_union));
432 }
433 umock::netdb_h.freeaddrinfo(res);
434 }
435
436 return;
437 }
438
439exit_fail:
440 throw std::invalid_argument(
441 UPnPsdk_LOGEXCEPT("MSG1043") "Invalid netaddress \"" + a_addr_str +
442 "\".\n");
443}
444
445// Assignment operator= to set socket port from an integer
446// -------------------------------------------------------
447void SSockaddr::operator=(const in_port_t a_port) {
448 // Don't use ::htons, MacOS don't like it.
449 // sin6_port is also sin_port due to union.
450 m_sa_union.sin6.sin6_port = htons(a_port);
451}
452
453// Assignment operator= to set socket address from a trivial socket address
454// structure
455// ------------------------------------------------------------------------
456void SSockaddr::operator=(const ::sockaddr_storage& a_ss) {
457 ::memcpy(&m_sa_union, &a_ss, sizeof(m_sa_union));
458}
459
460// Compare operator== to test if another trivial socket address is equal to this
461// -----------------------------------------------------------------------------
462bool SSockaddr::operator==(const SSockaddr& a_saddr) const {
463 return sockaddrcmp(&a_saddr.ss, &ss);
464}
465
466// Getter for the assosiated ip address without port
467// -------------------------------------------------
468// e.g. "[2001:db8::2]" or "192.168.254.253".
469const std::string SSockaddr::netaddr() noexcept {
470 // TRACE not usable with chained output.
471 // TRACE2(this, " Executing SSockaddr::netaddr()")
472
473 // Accept nameinfo only for supported address families.
474 switch (m_sa_union.ss.ss_family) {
475 case AF_INET6:
476 case AF_INET:
477 break;
478 case AF_UNSPEC:
479 return "";
480 default:
481 UPnPsdk_LOGERR("MSG1129") "Unsupported address family "
482 << std::to_string(m_sa_union.ss.ss_family)
483 << ". Continue with unspecified netaddress \"\".\n";
484 return "";
485 }
486
487 // The buffer fit to an IPv6 address with mapped IPv4 address (max. 46) and
488 // also fit to an IPv6 address with scope id (max. 51).
489 char addrStr[39 /*sizeof(IPv6_addr)*/ + 1 /*'%'*/ +
490 10 /*sin6_scope_id_max(4294967295)*/ + 1 /*'\0'*/]{};
491 int ret = ::getnameinfo(&m_sa_union.sa, sizeof(m_sa_union.ss), addrStr,
492 sizeof(addrStr), nullptr, 0, NI_NUMERICHOST);
493 if (ret != 0) {
494 // 'std::to_string()' may throw 'std::bad_alloc' from the std::string
495 // constructor. It is a fatal error that violates the promise to
496 // noexcept and immediately terminates the propgram. This is
497 // intentional because the error cannot be handled except improving the
498 // hardware.
499 UPnPsdk_LOGERR(
500 "MSG1036") "Failed to get netaddress with address family "
501 << std::to_string(m_sa_union.ss.ss_family) << ": "
502 << ::gai_strerror(ret)
503 << ". Continue with unspecified netaddress \"\".\n";
504 return "";
505 }
506
507 // Next may throw 'std::length_error' if the length of the constructed
508 // std::string would exceed max_size(). This should never happen with given
509 // lengths of addrStr (promise noexcept).
510 if (m_sa_union.ss.ss_family == AF_INET6)
511 return '[' + std::string(addrStr) + ']';
512 else
513 return std::string(addrStr);
514}
515
516// Getter for the assosiated ip address with port
517// ----------------------------------------------
518// e.g. "[2001:db8::2]:50001" or "192.168.254.253:50001".
519const std::string SSockaddr::netaddrp() noexcept {
520 // TRACE not usable with chained output.
521 // TRACE2(this, " Executing SSockaddr::netaddrp()")
522 //
523 // sin_port and sin6_port are on the same memory location (union of the
524 // structures) so I can use it for AF_INET and AF_INET6. 'std::to_string()'
525 // may throw 'std::bad_alloc' from the std::string constructor. It is a
526 // fatal error that violates the promise to noexcept and immediately
527 // terminates the propgram. This is intentional because the error cannot be
528 // handled except improving the hardware.
529 switch (m_sa_union.ss.ss_family) {
530 case AF_INET6:
531 case AF_INET:
532 case AF_UNSPEC:
533 return this->netaddr() + ":" +
534 std::to_string(ntohs(m_sa_union.sin6.sin6_port));
535 break;
536 case AF_UNIX:
537 return this->netaddr() + ":0";
538 }
539
540 return "";
541}
542
543// Getter for the assosiated port number
544// -------------------------------------
545in_port_t SSockaddr::port() const {
546 TRACE2(this, " Executing SSockaddr::port()")
547 // sin_port and sin6_port are on the same memory location (union of the
548 // structures) so we can use it for AF_INET and AF_INET6.
549 // Don't use ::ntohs, MacOS don't like it.
550 return ntohs(m_sa_union.sin6.sin6_port);
551}
552
553// Getter for sizeof the current (sin6 or sin) Sockaddr Structure.
554// ---------------------------------------------------------------
555socklen_t SSockaddr::sizeof_saddr() const {
556 TRACE2(this, " Executing SSockaddr::sizeof_saddr()")
557 switch (m_sa_union.ss.ss_family) {
558 case AF_INET6:
559 return sizeof(m_sa_union.sin6);
560 case AF_INET:
561 return sizeof(m_sa_union.sin);
562 case AF_UNSPEC:
563 return sizeof(m_sa_union.ss);
564 default:
565 return 0;
566 }
567}
568
569// Get if the socket address is a loopback address
570// ----------------------------------------------
572 return ((m_sa_union.ss.ss_family == AF_INET6 &&
573 IN6_IS_ADDR_LOOPBACK(&m_sa_union.sin6.sin6_addr)) ||
574 (m_sa_union.ss.ss_family == AF_INET &&
575 // address between "127.0.0.0" and "127.255.255.255"
576 ntohl(m_sa_union.sin.sin_addr.s_addr) >= 2130706432 &&
577 ntohl(m_sa_union.sin.sin_addr.s_addr) <= 2147483647));
578}
579
581// Getter of the netaddress to output stream
582// -----------------------------------------
583std::ostream& operator<<(std::ostream& os, SSockaddr& saddr) {
584 os << saddr.netaddrp();
585 return os;
586}
588
589} // namespace UPnPsdk
int to_port(const std::string &a_port_str, in_port_t *const a_port_num=nullptr) noexcept
Free function to check if a string represents a valid port number.
Definition sockaddr.cpp:114
void split_addr_port(const std::string &a_addr_str, std::string &a_addr, std::string &a_serv)
Free function to split inet address and port(service)
Definition sockaddr.cpp:210
bool sockaddrcmp(const ::sockaddr_storage *a_ss1, const ::sockaddr_storage *a_ss2) noexcept
logical compare two sockaddr structures
Definition sockaddr.cpp:32
UPnPsdk_API::std::ostream & operator<<(::std::ostream &os, SSockaddr &saddr)
output the netaddress
Reengineered Object Oriented UPnP+ program code.
Declaration of the Sockaddr class and some free helper functions.
Trivial ::sockaddr structures enhanced with methods.
Definition sockaddr.hpp:94
const std::string netaddrp() noexcept
Get the assosiated netaddress with port.
Definition sockaddr.cpp:519
bool operator==(const SSockaddr &) const
Test if another socket address is logical equal to this.
Definition sockaddr.cpp:462
bool is_loopback() const
Get if the socket address is a loopback address.
Definition sockaddr.cpp:571
SSockaddr & operator=(SSockaddr)
Copy assignment operator, needs user defined copy contructor.
Definition sockaddr.cpp:375
in_port_t port() const
Get the numeric port.
Definition sockaddr.cpp:545
const std::string netaddr() noexcept
Get the assosiated netaddress without port.
Definition sockaddr.cpp:469
sockaddr_storage & ss
Reference to sockaddr_storage struct.
Definition sockaddr.hpp:96
socklen_t sizeof_saddr() const
Get sizeof the current filled (sin6 or sin) Sockaddr Structure.
Definition sockaddr.cpp:555
Define macro for synced logging to the console for detailed info and debug.