/* $NetBSD: url.c,v 1.1.2.2 2024/02/24 13:07:22 martin Exp $ */ /* * Copyright (C) Internet Systems Consortium, Inc. ("ISC") * * SPDX-License-Identifier: MPL-2.0 and MIT * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at https://mozilla.org/MPL/2.0/. * * See the COPYRIGHT file distributed with this work for additional * information regarding copyright ownership. */ /* * Copyright Joyent, Inc. and other Node contributors. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include #include #include #include #include #include #ifndef BIT_AT #define BIT_AT(a, i) \ (!!((unsigned int)(a)[(unsigned int)(i) >> 3] & \ (1 << ((unsigned int)(i)&7)))) #endif #if HTTP_PARSER_STRICT #define T(v) 0 #else #define T(v) v #endif static const uint8_t normal_url_char[32] = { /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, /* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, /* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; #undef T typedef enum { s_dead = 1, /* important that this is > 0 */ s_start_req_or_res, s_res_or_resp_H, s_start_res, s_res_H, s_res_HT, s_res_HTT, s_res_HTTP, s_res_http_major, s_res_http_dot, s_res_http_minor, s_res_http_end, s_res_first_status_code, s_res_status_code, s_res_status_start, s_res_status, s_res_line_almost_done, s_start_req, s_req_method, s_req_spaces_before_url, s_req_schema, s_req_schema_slash, s_req_schema_slash_slash, s_req_server_start, s_req_server, s_req_server_with_at, s_req_path, s_req_query_string_start, s_req_query_string, s_req_fragment_start, s_req_fragment, s_req_http_start, s_req_http_H, s_req_http_HT, s_req_http_HTT, s_req_http_HTTP, s_req_http_I, s_req_http_IC, s_req_http_major, s_req_http_dot, s_req_http_minor, s_req_http_end, s_req_line_almost_done, s_header_field_start, s_header_field, s_header_value_discard_ws, s_header_value_discard_ws_almost_done, s_header_value_discard_lws, s_header_value_start, s_header_value, s_header_value_lws, s_header_almost_done, s_chunk_size_start, s_chunk_size, s_chunk_parameters, s_chunk_size_almost_done, s_headers_almost_done, s_headers_done, /* * Important: 's_headers_done' must be the last 'header' state. All * states beyond this must be 'body' states. It is used for overflow * checking. See the PARSING_HEADER() macro. */ s_chunk_data, s_chunk_data_almost_done, s_chunk_data_done, s_body_identity, s_body_identity_eof, s_message_done } state_t; typedef enum { s_http_host_dead = 1, s_http_userinfo_start, s_http_userinfo, s_http_host_start, s_http_host_v6_start, s_http_host, s_http_host_v6, s_http_host_v6_end, s_http_host_v6_zone_start, s_http_host_v6_zone, s_http_host_port_start, s_http_host_port } host_state_t; /* Macros for character classes; depends on strict-mode */ #define IS_MARK(c) \ ((c) == '-' || (c) == '_' || (c) == '.' || (c) == '!' || (c) == '~' || \ (c) == '*' || (c) == '\'' || (c) == '(' || (c) == ')') #define IS_USERINFO_CHAR(c) \ (isalnum((unsigned char)c) || IS_MARK(c) || (c) == '%' || \ (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ (c) == '$' || (c) == ',') #if HTTP_PARSER_STRICT #define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) #define IS_HOST_CHAR(c) (isalnum((unsigned char)c) || (c) == '.' || (c) == '-') #else #define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c) || ((c)&0x80)) #define IS_HOST_CHAR(c) \ (isalnum((unsigned char)c) || (c) == '.' || (c) == '-' || (c) == '_') #endif /* * Our URL parser. * * This is designed to be shared by http_parser_execute() for URL validation, * hence it has a state transition + byte-for-byte interface. In addition, it * is meant to be embedded in http_parser_parse_url(), which does the dirty * work of turning state transitions URL components for its API. * * This function should only be invoked with non-space characters. It is * assumed that the caller cares about (and can detect) the transition between * URL and non-URL states by looking for these. */ static state_t parse_url_char(state_t s, const char ch) { if (ch == ' ' || ch == '\r' || ch == '\n') { return (s_dead); } #if HTTP_PARSER_STRICT if (ch == '\t' || ch == '\f') { return (s_dead); } #endif switch (s) { case s_req_spaces_before_url: /* Proxied requests are followed by scheme of an absolute URI * (alpha). All methods except CONNECT are followed by '/' or * '*'. */ if (ch == '/' || ch == '*') { return (s_req_path); } if (isalpha((unsigned char)ch)) { return (s_req_schema); } break; case s_req_schema: if (isalpha((unsigned char)ch)) { return (s); } if (ch == ':') { return (s_req_schema_slash); } break; case s_req_schema_slash: if (ch == '/') { return (s_req_schema_slash_slash); } break; case s_req_schema_slash_slash: if (ch == '/') { return (s_req_server_start); } break; case s_req_server_with_at: if (ch == '@') { return (s_dead); } FALLTHROUGH; case s_req_server_start: case s_req_server: if (ch == '/') { return (s_req_path); } if (ch == '?') { return (s_req_query_string_start); } if (ch == '@') { return (s_req_server_with_at); } if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { return (s_req_server); } break; case s_req_path: if (IS_URL_CHAR(ch)) { return (s); } switch (ch) { case '?': return (s_req_query_string_start); case '#': return (s_req_fragment_start); } break; case s_req_query_string_start: case s_req_query_string: if (IS_URL_CHAR(ch)) { return (s_req_query_string); } switch (ch) { case '?': /* allow extra '?' in query string */ return (s_req_query_string); case '#': return (s_req_fragment_start); } break; case s_req_fragment_start: if (IS_URL_CHAR(ch)) { return (s_req_fragment); } switch (ch) { case '?': return (s_req_fragment); case '#': return (s); } break; case s_req_fragment: if (IS_URL_CHAR(ch)) { return (s); } switch (ch) { case '?': case '#': return (s); } break; default: break; } /* * We should never fall out of the switch above unless there's an * error. */ return (s_dead); } static host_state_t http_parse_host_char(host_state_t s, const char ch) { switch (s) { case s_http_userinfo: case s_http_userinfo_start: if (ch == '@') { return (s_http_host_start); } if (IS_USERINFO_CHAR(ch)) { return (s_http_userinfo); } break; case s_http_host_start: if (ch == '[') { return (s_http_host_v6_start); } if (IS_HOST_CHAR(ch)) { return (s_http_host); } break; case s_http_host: if (IS_HOST_CHAR(ch)) { return (s_http_host); } FALLTHROUGH; case s_http_host_v6_end: if (ch == ':') { return (s_http_host_port_start); } break; case s_http_host_v6: if (ch == ']') { return (s_http_host_v6_end); } FALLTHROUGH; case s_http_host_v6_start: if (isxdigit((unsigned char)ch) || ch == ':' || ch == '.') { return (s_http_host_v6); } if (s == s_http_host_v6 && ch == '%') { return (s_http_host_v6_zone_start); } break; case s_http_host_v6_zone: if (ch == ']') { return (s_http_host_v6_end); } FALLTHROUGH; case s_http_host_v6_zone_start: /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ if (isalnum((unsigned char)ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || ch == '~') { return (s_http_host_v6_zone); } break; case s_http_host_port: case s_http_host_port_start: if (isdigit((unsigned char)ch)) { return (s_http_host_port); } break; default: break; } return (s_http_host_dead); } static isc_result_t http_parse_host(const char *buf, isc_url_parser_t *up, int found_at) { host_state_t s; const char *p = NULL; size_t buflen = up->field_data[ISC_UF_HOST].off + up->field_data[ISC_UF_HOST].len; REQUIRE((up->field_set & (1 << ISC_UF_HOST)) != 0); up->field_data[ISC_UF_HOST].len = 0; s = found_at ? s_http_userinfo_start : s_http_host_start; for (p = buf + up->field_data[ISC_UF_HOST].off; p < buf + buflen; p++) { host_state_t new_s = http_parse_host_char(s, *p); if (new_s == s_http_host_dead) { return (ISC_R_FAILURE); } switch (new_s) { case s_http_host: if (s != s_http_host) { up->field_data[ISC_UF_HOST].off = (uint16_t)(p - buf); } up->field_data[ISC_UF_HOST].len++; break; case s_http_host_v6: if (s != s_http_host_v6) { up->field_data[ISC_UF_HOST].off = (uint16_t)(p - buf); } up->field_data[ISC_UF_HOST].len++; break; case s_http_host_v6_zone_start: case s_http_host_v6_zone: up->field_data[ISC_UF_HOST].len++; break; case s_http_host_port: if (s != s_http_host_port) { up->field_data[ISC_UF_PORT].off = (uint16_t)(p - buf); up->field_data[ISC_UF_PORT].len = 0; up->field_set |= (1 << ISC_UF_PORT); } up->field_data[ISC_UF_PORT].len++; break; case s_http_userinfo: if (s != s_http_userinfo) { up->field_data[ISC_UF_USERINFO].off = (uint16_t)(p - buf); up->field_data[ISC_UF_USERINFO].len = 0; up->field_set |= (1 << ISC_UF_USERINFO); } up->field_data[ISC_UF_USERINFO].len++; break; default: break; } s = new_s; } /* Make sure we don't end somewhere unexpected */ switch (s) { case s_http_host_start: case s_http_host_v6_start: case s_http_host_v6: case s_http_host_v6_zone_start: case s_http_host_v6_zone: case s_http_host_port_start: case s_http_userinfo: case s_http_userinfo_start: return (ISC_R_FAILURE); default: break; } return (ISC_R_SUCCESS); } isc_result_t isc_url_parse(const char *buf, size_t buflen, bool is_connect, isc_url_parser_t *up) { state_t s; isc_url_field_t uf, old_uf; int found_at = 0; const char *p = NULL; if (buflen == 0) { return (ISC_R_FAILURE); } up->port = up->field_set = 0; s = is_connect ? s_req_server_start : s_req_spaces_before_url; old_uf = ISC_UF_MAX; for (p = buf; p < buf + buflen; p++) { s = parse_url_char(s, *p); /* Figure out the next field that we're operating on */ switch (s) { case s_dead: return (ISC_R_FAILURE); /* Skip delimiters */ case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: case s_req_query_string_start: case s_req_fragment_start: continue; case s_req_schema: uf = ISC_UF_SCHEMA; break; case s_req_server_with_at: found_at = 1; FALLTHROUGH; case s_req_server: uf = ISC_UF_HOST; break; case s_req_path: uf = ISC_UF_PATH; break; case s_req_query_string: uf = ISC_UF_QUERY; break; case s_req_fragment: uf = ISC_UF_FRAGMENT; break; default: UNREACHABLE(); } /* Nothing's changed; soldier on */ if (uf == old_uf) { up->field_data[uf].len++; continue; } up->field_data[uf].off = (uint16_t)(p - buf); up->field_data[uf].len = 1; up->field_set |= (1 << uf); old_uf = uf; } /* host must be present if there is a schema */ /* parsing http:///toto will fail */ if ((up->field_set & (1 << ISC_UF_SCHEMA)) && (up->field_set & (1 << ISC_UF_HOST)) == 0) { return (ISC_R_FAILURE); } if (up->field_set & (1 << ISC_UF_HOST)) { isc_result_t result; result = http_parse_host(buf, up, found_at); if (result != ISC_R_SUCCESS) { return (result); } } /* CONNECT requests can only contain "hostname:port" */ if (is_connect && up->field_set != ((1 << ISC_UF_HOST) | (1 << ISC_UF_PORT))) { return (ISC_R_FAILURE); } if (up->field_set & (1 << ISC_UF_PORT)) { uint16_t off; uint16_t len; const char *pp = NULL; const char *end = NULL; unsigned long v; off = up->field_data[ISC_UF_PORT].off; len = up->field_data[ISC_UF_PORT].len; end = buf + off + len; /* * NOTE: The characters are already validated and are in the * [0-9] range */ INSIST(off + len <= buflen); v = 0; for (pp = buf + off; pp < end; pp++) { v *= 10; v += *pp - '0'; /* Ports have a max value of 2^16 */ if (v > 0xffff) { return (ISC_R_RANGE); } } up->port = (uint16_t)v; } return (ISC_R_SUCCESS); }