NeoGraph 0.16.0
A C++20 Graph Agent Engine Library — LangGraph for C++
Loading...
Searching...
No Matches
endpoint.h
Go to the documentation of this file.
1
14#pragma once
15
16#include <algorithm>
17#include <charconv>
18#include <cctype>
19#include <stdexcept>
20#include <string>
21#include <string_view>
22
23namespace neograph::async {
24
30 std::string host;
31 std::string port;
32 std::string prefix;
33 bool tls = false;
34};
35
40inline AsyncEndpoint split_async_endpoint(const std::string& base_url) {
41 AsyncEndpoint out;
42 std::string rest = base_url;
43
44 auto scheme_end = rest.find("://");
45 if (scheme_end != std::string::npos) {
46 std::string scheme = rest.substr(0, scheme_end);
47 std::transform(scheme.begin(), scheme.end(), scheme.begin(),
48 [](unsigned char c) {
49 return static_cast<char>(std::tolower(c));
50 });
51 if (scheme != "http" && scheme != "https") {
52 throw std::invalid_argument("unsupported HTTP endpoint scheme");
53 }
54 out.tls = scheme == "https";
55 rest = rest.substr(scheme_end + 3);
56 }
57
58 auto path_start = rest.find_first_of("/?#");
59 std::string authority;
60 if (path_start != std::string::npos) {
61 authority = rest.substr(0, path_start);
62 if (rest[path_start] != '/') {
63 throw std::invalid_argument(
64 "HTTP endpoint base URL must not contain a query or fragment");
65 }
66 out.prefix = rest.substr(path_start);
67 if (out.prefix.find('#') != std::string::npos) {
68 throw std::invalid_argument(
69 "HTTP endpoint base URL must not contain a fragment");
70 }
71 } else {
72 authority = rest;
73 }
74
75 if (authority.empty() || authority.find('@') != std::string::npos) {
76 throw std::invalid_argument("invalid HTTP endpoint authority");
77 }
78 if (authority.front() == '[') {
79 const auto close = authority.find(']');
80 if (close == std::string::npos) {
81 throw std::invalid_argument("invalid bracketed HTTP endpoint host");
82 }
83 out.host = authority.substr(1, close - 1);
84 if (close + 1 < authority.size()) {
85 if (authority[close + 1] != ':' || close + 2 == authority.size()) {
86 throw std::invalid_argument("invalid HTTP endpoint port");
87 }
88 out.port = authority.substr(close + 2);
89 }
90 } else {
91 const auto colon = authority.find(':');
92 if (colon != std::string::npos) {
93 if (authority.find(':', colon + 1) != std::string::npos) {
94 throw std::invalid_argument(
95 "IPv6 HTTP endpoint hosts must use brackets");
96 }
97 out.host = authority.substr(0, colon);
98 out.port = authority.substr(colon + 1);
99 } else {
100 out.host = authority;
101 }
102 }
103 if (out.host.empty()) {
104 throw std::invalid_argument("HTTP endpoint host is empty");
105 }
106 if (out.port.empty()) out.port = out.tls ? "443" : "80";
107 unsigned int numeric_port = 0;
108 const auto [port_end, port_error] = std::from_chars(
109 out.port.data(), out.port.data() + out.port.size(), numeric_port);
110 if (port_error != std::errc{} ||
111 port_end != out.port.data() + out.port.size() ||
112 numeric_port == 0 || numeric_port > 65535) {
113 throw std::invalid_argument("invalid HTTP endpoint port");
114 }
115 out.port = std::to_string(numeric_port);
116 return out;
117}
118
119inline bool has_explicit_http_scheme(std::string_view url) {
120 const auto separator = url.find("://");
121 if (separator == std::string_view::npos) return false;
122 std::string scheme(url.substr(0, separator));
123 std::transform(scheme.begin(), scheme.end(), scheme.begin(),
124 [](unsigned char c) {
125 return static_cast<char>(std::tolower(c));
126 });
127 return scheme == "http" || scheme == "https";
128}
129
130inline bool is_loopback_host(std::string_view host) {
131 std::string lower(host);
132 std::transform(lower.begin(), lower.end(), lower.begin(),
133 [](unsigned char c) {
134 return static_cast<char>(std::tolower(c));
135 });
136 if (!lower.empty() && lower.back() == '.') lower.pop_back();
137 if (lower == "::1") return true;
138
139 unsigned int octets[4]{};
140 std::size_t start = 0;
141 for (int i = 0; i < 4; ++i) {
142 const auto end = lower.find('.', start);
143 const auto token_end = end == std::string::npos ? lower.size() : end;
144 if (token_end == start) return false;
145 const auto [parsed, error] = std::from_chars(
146 lower.data() + start, lower.data() + token_end, octets[i]);
147 if (error != std::errc{} || parsed != lower.data() + token_end ||
148 octets[i] > 255) {
149 return false;
150 }
151 if (i < 3 && end == std::string::npos) return false;
152 if (i == 3 && end != std::string::npos) return false;
153 start = token_end + 1;
154 }
155 return octets[0] == 127;
156}
157
158inline AsyncEndpoint validate_credential_endpoint(
159 const std::string& base_url,
160 bool credentialed,
161 bool allow_insecure_loopback) {
162 if (credentialed && !has_explicit_http_scheme(base_url)) {
163 throw std::invalid_argument(
164 "credentialed provider endpoint requires an explicit http:// or https:// scheme");
165 }
166 auto endpoint = split_async_endpoint(base_url);
167 if (credentialed && !endpoint.tls &&
168 !(allow_insecure_loopback && is_loopback_host(endpoint.host))) {
169 throw std::invalid_argument(
170 "credentialed provider endpoint requires TLS; plaintext is allowed only for explicit loopback development");
171 }
172 return endpoint;
173}
174
175} // namespace neograph::async
AsyncEndpoint split_async_endpoint(const std::string &base_url)
Parse a base URL like https://api.openai.com or http://localhost:8080/v1.
Definition endpoint.h:40
Result of decomposing a URL into the pieces async_post needs.
Definition endpoint.h:29