paivana-httpd_helper.h (11696B)
1 /* 2 This file is part of GNUnet. 3 Copyright (C) 2026 Taler Systems SA 4 5 Paivana is free software; you can redistribute it and/or 6 modify it under the terms of the GNU Affero General Public License 7 as published by the Free Software Foundation; either version 8 3, or (at your option) any later version. 9 10 Paivana is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty 12 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 13 the GNU Affero General Public License for more details. 14 15 You should have received a copy of the GNU Affero General Public 16 License along with Paivana; see the file COPYING. If not, 17 write to the Free Software Foundation, Inc., 51 Franklin 18 Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @author Christian Grothoff 23 * @file paivana-httpd_helper.h 24 * @brief helper functions 25 */ 26 #ifndef PAIVANA_HTTPD_HELPER_H 27 #define PAIVANA_HTTPD_HELPER_H 28 29 #include <stdbool.h> 30 #include <microhttpd.h> 31 #include <gnunet/gnunet_util_lib.h> 32 33 34 /** 35 * Longest forwarding chain we are willing to look at. 36 * 37 * A real chain has single digits; MHD's default connection pool would 38 * otherwise let a client hand us some 2500 elements per request, and 39 * anything that touches each of them is work we do on the one thread 40 * that also drives all outbound traffic. A longer chain is treated 41 * as malformed, i.e. as if no forwarding header had been sent. 42 */ 43 #define PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS 32 44 45 46 /** 47 * Everything about a request that the forwarding layer may look at. 48 * 49 * All header members are arrays of the *field line* values MHD 50 * reported for that field name, in the order they arrived, terminated 51 * by a NULL entry (NULL for "field not present at all"). RFC 9110 52 * §5.3 makes repeated field lines of a list-valued field one list, so 53 * every line has to be considered, not just the first. 54 * 55 * `Forwarded` and `X-Forwarded-For` say the same thing in two 56 * grammars, so only one of them is walked: `Forwarded` wherever it is 57 * present at all, `X-Forwarded-For` otherwise. The two are never 58 * merged -- a proxy that emits both and disagrees with itself would 59 * otherwise make the answer depend on the order we happened to read 60 * them in. 61 */ 62 struct PAIVANA_HTTPD_Forwarding 63 { 64 65 /** 66 * Address of the peer we accepted the connection from, in the binary 67 * form used throughout, or NULL for a peer that has none (AF_UNIX). 68 */ 69 const void *peer; 70 71 /** 72 * Number of bytes in @e peer. 73 */ 74 size_t peer_len; 75 76 /** 77 * Are we behind a trusted reverse proxy, i.e. was `-f` given? If 78 * not, none of the header members below is looked at at all. 79 */ 80 bool respect_forwarded; 81 82 /** 83 * Field lines of the `Forwarded` header (RFC 7239). 84 */ 85 const char *const *forwarded; 86 87 /** 88 * Field lines of the `X-Forwarded-For` header. 89 */ 90 const char *const *xff; 91 92 /** 93 * Field lines of the `X-Forwarded-Proto` header. 94 */ 95 const char *const *xfp; 96 97 /** 98 * Field lines of the `X-Forwarded-Host` header. 99 */ 100 const char *const *xfh; 101 102 /** 103 * Field lines of the `X-Forwarded-Port` header. 104 */ 105 const char *const *xfport; 106 }; 107 108 109 /** 110 * What the forwarding layer concluded about the client. 111 */ 112 struct PAIVANA_HTTPD_Client 113 { 114 115 /** 116 * Client address in binary form: 4 bytes (`struct in_addr`) for 117 * IPv4, 16 bytes (`struct in6_addr`) for IPv6. NULL only if the 118 * peer had no address and no header supplied one. 119 */ 120 void *ca; 121 122 /** 123 * Number of bytes in @e ca. 124 */ 125 size_t ca_len; 126 127 /** 128 * Scheme the client itself used, "http" or "https", or NULL if 129 * nothing we trust said. 130 */ 131 char *proto; 132 133 /** 134 * Authority the client itself addressed (host, optionally with a 135 * port), or NULL if nothing we trust said. 136 */ 137 char *host; 138 }; 139 140 141 /** 142 * Determine who the client is, and what it thinks it connected to. 143 * 144 * This is the one walk over the forwarding chain; both the address the 145 * access cookie is keyed on and the base URL we reconstruct come out 146 * of it, so that the two can never disagree about which hop was 147 * believed. 148 * 149 * Given `-f` we are behind a trusted reverse proxy, so the peer we 150 * accepted from is trusted implicitly and `TRUSTED_PROXIES` names the 151 * *additional* hops further out that may also speak for their 152 * predecessor. The chain is therefore walked from the right: we step 153 * leftwards over a node for as long as that node is one we trust, and 154 * the first node that is not is the client. Nothing to the left of an 155 * untrusted node is attributable to anyone, and nothing to the left of 156 * a node that names no address (RFC 7239 §6.3 `unknown`, an obfuscated 157 * identifier, a hostname) can be reached at all. 158 * 159 * The scheme and authority are taken from the same element the address 160 * was: that element was written by a hop we trust and describes the 161 * connection its predecessor -- the client -- made. Where the chain 162 * says nothing, the de-facto `X-Forwarded-Proto` / `-Host` / `-Port` 163 * headers are consulted instead, and they too only when `-f` says 164 * something in front of us is entitled to have set them. 165 * 166 * Anything the client controls that we cannot believe falls back to 167 * the peer: a malformed, empty or over-long chain never removes the 168 * one address we know for certain. 169 * 170 * @param fi what the request carried 171 * @param[out] cl what we concluded; to be released with 172 * #PAIVANA_HTTPD_client_clear() whatever the return value 173 * @return true if a client address was determined, false only if the 174 * peer has no address and no header supplied one 175 */ 176 bool 177 PAIVANA_HTTPD_resolve_forwarding (const struct PAIVANA_HTTPD_Forwarding *fi, 178 struct PAIVANA_HTTPD_Client *cl); 179 180 181 /** 182 * Release the members of @a cl and zero it. 183 * 184 * @param[in,out] cl result of #PAIVANA_HTTPD_resolve_forwarding() 185 */ 186 void 187 PAIVANA_HTTPD_client_clear (struct PAIVANA_HTTPD_Client *cl); 188 189 190 /** 191 * Obtain the client address of @a connection. 192 * 193 * A thin adapter over #PAIVANA_HTTPD_resolve_forwarding(): it reads 194 * the socket peer and the forwarding field lines off @a connection and 195 * hands them to that function, which is where the policy lives. 196 * 197 * The address is returned in binary form: 4 bytes (`struct in_addr`) 198 * for IPv4, 16 bytes (`struct in6_addr`) for IPv6. That 199 * representation is canonical and does not depend on where the 200 * address was taken from -- a forwarding header or the socket -- which 201 * matters because it feeds the access-cookie MAC: two requests from 202 * the same host have to yield the same bytes or the cookie stops 203 * verifying. 204 * 205 * @param connection HTTP client connection 206 * @param[out] ca where to write the client address 207 * @param[out] ca_len number of bytes in @a ca 208 * @return true on success, false if no address could be determined, 209 * which for a request that arrived over IP cannot happen: only a 210 * non-IP socket with nothing to fall back on gets here 211 */ 212 bool 213 PAIVANA_HTTPD_get_client_address (struct MHD_Connection *connection, 214 void **ca, 215 size_t *ca_len); 216 217 218 /** 219 * Render the `for=` chain of a `Forwarded` header value in 220 * `X-Forwarded-For` form, so that an origin which speaks only the 221 * de-facto header still learns the client. 222 * 223 * Returns NULL unless *every* element carries a bare IP address: 224 * `X-Forwarded-For` has no way to say "this hop had no address", so an 225 * element whose `for` is RFC 7239's `unknown` or an obfuscated 226 * identifier cannot be represented, and omitting it would silently 227 * shift every position to its left. 228 * 229 * @param fwd value of the `Forwarded` header 230 * @return the comma-separated chain, or NULL; to be freed by the 231 * caller 232 */ 233 char * 234 PAIVANA_HTTPD_forwarded_for_chain (const char *fwd); 235 236 237 /** 238 * Read a parameter of the leftmost element of a `Forwarded` header 239 * value. Only `proto` and `host` are supported. 240 * 241 * The leftmost element describes the connection the client itself 242 * made, which is what those two are being asked about. 243 * 244 * The result is *validated*, not merely unquoted: `proto` is one of 245 * the two literals "http" and "https", and `host` is an RFC 3986 §3.2 246 * authority. Neither can therefore contain a `;`, `,`, `"` or space, 247 * which is what makes it safe to splice into a header we build. A 248 * value that does not pass is reported as absent. 249 * 250 * Note that a `host` may still contain `:` and `[`/`]`, which are not 251 * `tchar` (RFC 9110 §5.6.2): pass it through 252 * #PAIVANA_HTTPD_forwarded_value() before emitting it inside a 253 * `Forwarded` element, though it may be used as-is as the value of an 254 * `X-Forwarded-Host`. 255 * 256 * @param fwd value of the `Forwarded` header 257 * @param name parameter to look for, "proto" or "host" 258 * @return the value, or NULL if the header does not carry a usable 259 * one; to be freed by the caller 260 */ 261 char * 262 PAIVANA_HTTPD_forwarded_param (const char *fwd, 263 const char *name); 264 265 266 /** 267 * Render @a v as an RFC 7239 §4 `value`, i.e. as a bare token where 268 * that is legal and as an escaped quoted-string otherwise. 269 * 270 * Splicing a value into a `Forwarded` element without this is how a 271 * `;` or `,` in it becomes a parameter or an element of its own, and 272 * an origin that trusts us reads what the client wrote as something we 273 * said. 274 * 275 * @param v value to render 276 * @return the token or quoted-string, or NULL if @a v contains bytes 277 * that no `Forwarded` value may carry (a control character other 278 * than HTAB); to be freed by the caller 279 */ 280 char * 281 PAIVANA_HTTPD_forwarded_value (const char *v); 282 283 284 /** 285 * Render @a ca as an RFC 7239 `for=` node identifier, quoting and 286 * bracketing an IPv6 address as §6 requires. 287 * 288 * @param ca address in binary form, or NULL for a peer that has none 289 * @param ca_len number of bytes in @a ca 290 * @return the identifier, e.g. "203.0.113.7", "\"[2001:db8::1]\"" or 291 * "unknown"; to be freed by the caller 292 */ 293 char * 294 PAIVANA_HTTPD_forwarded_node (const void *ca, 295 size_t ca_len); 296 297 298 /** 299 * Is @a ca the address of a reverse proxy we trust to report the 300 * client address truthfully, i.e. one covered by `TRUSTED_PROXIES` or 301 * `TRUSTED_PROXIES6`? 302 * 303 * Always false if no policy was configured: "trust nothing in 304 * particular" and "trust everything" are deliberately different 305 * answers, and only the former can be stated safely by default. 306 * 307 * @param ca address in the binary form used throughout, i.e. 4 bytes 308 * for IPv4 and 16 for IPv6 309 * @param ca_len number of bytes in @a ca 310 * @return true if @a ca is within a configured trusted network 311 */ 312 bool 313 PAIVANA_HTTPD_is_trusted_proxy (const void *ca, 314 size_t ca_len); 315 316 /** 317 * Determine the Base URL that the client made the HTTP request to. 318 * The URL returned will be without the trailing '/'. 319 * 320 * `BASE_URL` wins if it is configured. Otherwise the scheme and 321 * authority come from #PAIVANA_HTTPD_resolve_forwarding(), i.e. from 322 * the same element of the forwarding chain the client address came 323 * from, so that the string the cookie is keyed on and the address it 324 * is keyed on were reported by the same trusted hop. Where that says 325 * nothing we go by the transport and the `Host` header. 326 * 327 * @param connection client connection used 328 * @param[out] buf where to write the base URL; buffer will be cleared 329 * and must be reaped by caller. 330 * @return false on error (no suitable HTTP headers found and nothing 331 * configured) 332 */ 333 bool 334 PAIVANA_HTTPD_get_base_url (struct MHD_Connection *connection, 335 struct GNUNET_Buffer *buf); 336 337 338 #endif