Branch data Line data Source code
1 : : /*
2 : : * Copyright 2025 Jinwoo Sung
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License");
5 : : * you may not use this file except in compliance with the License.
6 : : * You may obtain a copy of the License at
7 : : *
8 : : * http://www.apache.org/licenses/LICENSE-2.0
9 : : *
10 : : * Unless required by applicable law or agreed to in writing, software
11 : : * distributed under the License is distributed on an "AS IS" BASIS,
12 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : : * See the License for the specific language governing permissions and
14 : : * limitations under the License.
15 : : */
16 : :
17 : : #include "wirestead/util/input_validator.hpp"
18 : :
19 : : #include <algorithm>
20 : : #include <boost/asio/ip/address.hpp>
21 : : #include <boost/system/error_code.hpp>
22 : : #include <string_view>
23 : :
24 : : namespace wirestead {
25 : : namespace util {
26 : :
27 : 665 : bool InputValidator::is_valid_host(const std::string& host) {
28 : : // Check if it's an IPv4 address
29 [ + + ]: 665 : if (is_valid_ipv4(host)) {
30 : 638 : return true;
31 : : }
32 : :
33 : : // Check if it's an IPv6 address
34 [ + + ]: 27 : if (is_valid_ipv6(host)) {
35 : 3 : return true;
36 : : }
37 : :
38 : : // Check if it's a valid hostname
39 [ + + ]: 24 : if (is_valid_hostname(host)) {
40 : 14 : return true;
41 : : }
42 : :
43 : 10 : return false;
44 : : }
45 : :
46 : 2730 : bool InputValidator::is_valid_ipv4(std::string_view address) {
47 [ + + ]: 2730 : if (address.empty()) return false;
48 : :
49 : : // Use Boost.Asio for parsing to ensure standard compliance
50 : : // and robust validation, but enforce strict canonical form
51 : : // to reject ambiguous formats (e.g., octal, hex, whitespace).
52 : 2727 : boost::system::error_code ec;
53 : :
54 : : // Create string copy for compatibility with older Boost versions
55 : 2727 : std::string addr_str(address);
56 : 2727 : auto ip = boost::asio::ip::make_address_v4(addr_str, ec);
57 : :
58 [ + + ]: 2727 : if (ec) {
59 : 49 : return false;
60 : : }
61 : :
62 : : // Canonicalization check: The string representation of the parsed IP
63 : : // must match the input exactly. This rejects:
64 : : // - Octal (0127.0.0.1 -> 87.0.0.1 != 0127.0.0.1)
65 : : // - Hex (0x7F000001 -> 127.0.0.1 != 0x7F000001)
66 : : // - Leading/trailing whitespace
67 : : // - Leading zeros in octets (01.1.1.1 -> 1.1.1.1 != 01.1.1.1)
68 [ + - - + ]: 2678 : if (ip.to_string() != addr_str) {
69 : 0 : return false;
70 : : }
71 : :
72 : 2678 : return true;
73 : 2727 : }
74 : :
75 : 49 : bool InputValidator::is_valid_ipv6(const std::string& address) {
76 : : // Reject addresses containing brackets (e.g. [::1]:80) or port numbers
77 : : // boost::asio::ip::make_address_v6 behavior on Windows regarding this might be permissive
78 : : // or platform-dependent, so we explicitly reject them for consistency.
79 [ + + - + : 49 : if (address.find('[') != std::string::npos || address.find(']') != std::string::npos) {
+ + ]
80 : 2 : return false;
81 : : }
82 : :
83 : 47 : boost::system::error_code ec;
84 : 47 : boost::asio::ip::make_address_v6(address, ec);
85 : 47 : return !ec;
86 : : }
87 : :
88 : 29 : bool InputValidator::is_valid_hostname(std::string_view hostname) {
89 : : // Hostname validation according to RFC 1123
90 : : // - Must not be empty
91 : : // - Must not start or end with hyphen
92 : : // - Must contain only alphanumeric characters and hyphens
93 : : // - Each label must be 1-63 characters
94 : : // - Total length must not exceed 253 characters
95 : :
96 [ + + - + : 29 : if (hostname.empty() || hostname.length() > base::constants::MAX_HOSTNAME_LENGTH) {
+ + ]
97 : 2 : return false;
98 : : }
99 : :
100 [ + + - + : 27 : if (hostname.front() == '-' || hostname.back() == '-') {
+ + ]
101 : 1 : return false;
102 : : }
103 : :
104 : : // Check each label (separated by dots)
105 : 26 : size_t start = 0;
106 : 26 : size_t end = 0;
107 : :
108 [ + + ]: 41 : while ((end = hostname.find('.', start)) != std::string_view::npos) {
109 : 21 : std::string_view label = hostname.substr(start, end - start);
110 : :
111 [ + + - + : 21 : if (label.empty() || label.length() > 63) {
+ + ]
112 : 6 : return false;
113 : : }
114 : :
115 [ + - + + : 20 : if (label.front() == '-' || label.back() == '-') {
+ + ]
116 : 1 : return false;
117 : : }
118 : :
119 : : // Check if label contains only valid characters
120 [ + + ]: 97 : for (char c : label) {
121 [ + + + + ]: 82 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
122 : 4 : return false;
123 : : }
124 : : }
125 : 15 : start = end + 1;
126 : : }
127 : :
128 : : // Check last label
129 : 20 : std::string_view label = hostname.substr(start);
130 [ + - - + : 20 : if (label.empty() || label.length() > 63) {
- + ]
131 : 0 : return false;
132 : : }
133 : :
134 [ + - - + : 20 : if (label.front() == '-' || label.back() == '-') {
- + ]
135 : 0 : return false;
136 : : }
137 : :
138 [ + + ]: 155 : for (char c : label) {
139 [ + + + - ]: 138 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
140 : 3 : return false;
141 : : }
142 : : }
143 : :
144 : 17 : return true;
145 : : }
146 : :
147 : 1293 : bool InputValidator::is_valid_uds_path(const std::string& path) {
148 [ + + + + : 1293 : if (path.empty() || path.length() > base::constants::MAX_UDS_PATH_LENGTH) {
+ + ]
149 : 11 : return false;
150 : : }
151 : :
152 : : // UDS path should be a valid file system path.
153 : : // For simplicity, we check if it's not empty and doesn't contain null characters.
154 : : // On Linux/Unix, almost any character except null is valid in a filename.
155 [ - + ]: 1282 : if (path.find('\0') != std::string::npos) {
156 : 0 : return false;
157 : : }
158 : :
159 : 1282 : return true;
160 : : }
161 : :
162 : 396 : bool InputValidator::is_valid_device_path(const std::string& device) {
163 : : // Basic device path validation
164 : : // - Must not be empty
165 : : // - Must start with '/' (Unix-style) or be a COM port (Windows-style)
166 : : // - Must not contain invalid characters
167 : :
168 [ + + ]: 396 : if (device.empty()) {
169 : 3 : return false;
170 : : }
171 : :
172 : : // Unix-style device path (e.g., /dev/ttyUSB0, /dev/ttyACM0)
173 : : // Must start with "/dev/" for security
174 [ + - + - : 393 : if (device.length() >= 5 && device.substr(0, 5) == "/dev/") {
+ - + + +
- + + -
- ]
175 : : // Check for valid Unix device path characters
176 [ + + ]: 5254 : for (char c : device) {
177 [ + + + + : 4867 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '/' && c != '_' && c != '-') {
+ + + + ]
178 : 1 : return false;
179 : : }
180 : : }
181 : 387 : return true;
182 : : }
183 : :
184 : : // Windows-style COM port (e.g., COM1, COM2, etc.)
185 [ + - + - : 5 : if (device.length() >= 4 && device.substr(0, 3) == "COM") {
+ - - + +
- - + -
- ]
186 : 0 : std::string port_num = device.substr(3);
187 : :
188 : : // Check if port_num contains only digits
189 [ # # # # ]: 0 : if (port_num.empty() ||
190 [ # # # # ]: 0 : !std::all_of(port_num.begin(), port_num.end(), [](unsigned char c) { return std::isdigit(c); })) {
191 : 0 : return false;
192 : : }
193 : :
194 : : try {
195 : 0 : int port = std::stoi(port_num);
196 [ # # # # ]: 0 : return port >= 1 && port <= 255;
197 : 0 : } catch (const std::exception&) {
198 : 0 : return false;
199 : 0 : }
200 : 0 : }
201 : :
202 : : // Windows special device names
203 [ + - + - : 10 : if (device == "NUL" || device == "CON" || device == "PRN" || device == "AUX" || device == "LPT1" ||
+ - + - ]
204 [ + - + - : 10 : device == "LPT2" || device == "LPT3") {
- + - + ]
205 : 0 : return true;
206 : : }
207 : :
208 : 5 : return false;
209 : : }
210 : :
211 : : } // namespace util
212 : : } // namespace wirestead
|