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 : 20 : void InputValidator::validate_host(const std::string& host) {
28 : 40 : validate_non_empty_string(host, "host");
29 : 19 : validate_string_length(host, base::constants::MAX_HOSTNAME_LENGTH, "host");
30 : :
31 [ + + ]: 19 : if (is_valid_host(host)) {
32 : 7 : return;
33 : : }
34 : :
35 : 84 : throw diagnostics::ValidationException("invalid host format", "host", "valid IPv4, IPv6, or hostname");
36 : : }
37 : :
38 : 27 : void InputValidator::validate_ipv4_address(const std::string& address) {
39 : 27 : validate_non_empty_string(address, "ipv4_address");
40 : :
41 [ + + ]: 27 : if (!is_valid_ipv4(address)) {
42 : 154 : throw diagnostics::ValidationException("invalid IPv4 address format", "ipv4_address", "valid IPv4 address");
43 : : }
44 : 5 : }
45 : :
46 : 12 : void InputValidator::validate_ipv6_address(const std::string& address) {
47 : 12 : validate_non_empty_string(address, "ipv6_address");
48 : :
49 [ + + ]: 12 : if (!is_valid_ipv6(address)) {
50 : 63 : throw diagnostics::ValidationException("invalid IPv6 address format", "ipv6_address", "valid IPv6 address");
51 : : }
52 : 3 : }
53 : :
54 : 0 : void InputValidator::validate_uds_path(const std::string& path) {
55 : 0 : validate_non_empty_string(path, "uds_path");
56 : 0 : validate_string_length(path, base::constants::MAX_UDS_PATH_LENGTH, "uds_path");
57 : :
58 [ # # ]: 0 : if (!is_valid_uds_path(path)) {
59 : 0 : throw diagnostics::ValidationException("invalid UDS path format", "uds_path", "valid Unix Domain Socket path");
60 : : }
61 : 0 : }
62 : :
63 : 24 : void InputValidator::validate_device_path(const std::string& device) {
64 : 48 : validate_non_empty_string(device, "device_path");
65 : 22 : validate_string_length(device, base::constants::MAX_DEVICE_PATH_LENGTH, "device_path");
66 : :
67 [ + + ]: 22 : if (!is_valid_device_path(device)) {
68 : 84 : throw diagnostics::ValidationException("invalid device path format", "device_path", "valid device path");
69 : : }
70 : 10 : }
71 : :
72 : 13 : void InputValidator::validate_parity(const std::string& parity) {
73 : 14 : validate_non_empty_string(parity, "parity");
74 : :
75 : : // Convert to lowercase for case-insensitive comparison
76 : 12 : std::string lower_parity = parity;
77 : 12 : std::transform(lower_parity.begin(), lower_parity.end(), lower_parity.begin(),
78 : 44 : [](unsigned char c) { return std::tolower(c); });
79 : :
80 [ + - + + : 12 : if (lower_parity != "none" && lower_parity != "odd" && lower_parity != "even") {
+ - + + +
- + + +
+ ]
81 : 21 : throw diagnostics::ValidationException("invalid parity value", "parity", "none, odd, or even");
82 : : }
83 : 12 : }
84 : :
85 : 42 : bool InputValidator::is_valid_host(const std::string& host) {
86 : : // Check if it's an IPv4 address
87 [ + + ]: 42 : if (is_valid_ipv4(host)) {
88 : 16 : return true;
89 : : }
90 : :
91 : : // Check if it's an IPv6 address
92 [ + + ]: 26 : if (is_valid_ipv6(host)) {
93 : 3 : return true;
94 : : }
95 : :
96 : : // Check if it's a valid hostname
97 [ + + ]: 23 : if (is_valid_hostname(host)) {
98 : 7 : return true;
99 : : }
100 : :
101 : 16 : return false;
102 : : }
103 : :
104 : 203 : bool InputValidator::is_valid_ipv4(std::string_view address) {
105 [ + + ]: 203 : if (address.empty()) return false;
106 : :
107 : : // Use Boost.Asio for parsing to ensure standard compliance
108 : : // and robust validation, but enforce strict canonical form
109 : : // to reject ambiguous formats (e.g., octal, hex, whitespace).
110 : 201 : boost::system::error_code ec;
111 : :
112 : : // Create string copy for compatibility with older Boost versions
113 : 201 : std::string addr_str(address);
114 : 201 : auto ip = boost::asio::ip::make_address_v4(addr_str, ec);
115 : :
116 [ + + ]: 201 : if (ec) {
117 : 56 : return false;
118 : : }
119 : :
120 : : // Canonicalization check: The string representation of the parsed IP
121 : : // must match the input exactly. This rejects:
122 : : // - Octal (0127.0.0.1 -> 87.0.0.1 != 0127.0.0.1)
123 : : // - Hex (0x7F000001 -> 127.0.0.1 != 0x7F000001)
124 : : // - Leading/trailing whitespace
125 : : // - Leading zeros in octets (01.1.1.1 -> 1.1.1.1 != 01.1.1.1)
126 [ + - - + ]: 145 : if (ip.to_string() != addr_str) {
127 : 0 : return false;
128 : : }
129 : :
130 : 145 : return true;
131 : 201 : }
132 : :
133 : 48 : bool InputValidator::is_valid_ipv6(const std::string& address) {
134 : : // Reject addresses containing brackets (e.g. [::1]:80) or port numbers
135 : : // boost::asio::ip::make_address_v6 behavior on Windows regarding this might be permissive
136 : : // or platform-dependent, so we explicitly reject them for consistency.
137 [ + + - + : 48 : if (address.find('[') != std::string::npos || address.find(']') != std::string::npos) {
+ + ]
138 : 3 : return false;
139 : : }
140 : :
141 : 45 : boost::system::error_code ec;
142 : 45 : boost::asio::ip::make_address_v6(address, ec);
143 : 45 : return !ec;
144 : : }
145 : :
146 : 28 : bool InputValidator::is_valid_hostname(std::string_view hostname) {
147 : : // Hostname validation according to RFC 1123
148 : : // - Must not be empty
149 : : // - Must not start or end with hyphen
150 : : // - Must contain only alphanumeric characters and hyphens
151 : : // - Each label must be 1-63 characters
152 : : // - Total length must not exceed 253 characters
153 : :
154 [ + + - + : 28 : if (hostname.empty() || hostname.length() > base::constants::MAX_HOSTNAME_LENGTH) {
+ + ]
155 : 1 : return false;
156 : : }
157 : :
158 [ + + + + : 27 : if (hostname.front() == '-' || hostname.back() == '-') {
+ + ]
159 : 4 : return false;
160 : : }
161 : :
162 : : // Check each label (separated by dots)
163 : 23 : size_t start = 0;
164 : 23 : size_t end = 0;
165 : :
166 [ + + ]: 33 : while ((end = hostname.find('.', start)) != std::string_view::npos) {
167 : 19 : std::string_view label = hostname.substr(start, end - start);
168 : :
169 [ + + + + : 19 : if (label.empty() || label.length() > 63) {
+ + ]
170 : 9 : return false;
171 : : }
172 : :
173 [ + - + + : 16 : if (label.front() == '-' || label.back() == '-') {
+ + ]
174 : 1 : return false;
175 : : }
176 : :
177 : : // Check if label contains only valid characters
178 [ + + ]: 90 : for (char c : label) {
179 [ + + + + ]: 80 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
180 : 5 : return false;
181 : : }
182 : : }
183 : 10 : start = end + 1;
184 : : }
185 : :
186 : : // Check last label
187 : 14 : std::string_view label = hostname.substr(start);
188 [ + - - + : 14 : if (label.empty() || label.length() > 63) {
- + ]
189 : 0 : return false;
190 : : }
191 : :
192 [ + + - + : 14 : if (label.front() == '-' || label.back() == '-') {
+ + ]
193 : 1 : return false;
194 : : }
195 : :
196 [ + + ]: 84 : for (char c : label) {
197 [ + + + + ]: 74 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
198 : 3 : return false;
199 : : }
200 : : }
201 : :
202 : 10 : return true;
203 : : }
204 : :
205 : 91 : bool InputValidator::is_valid_uds_path(const std::string& path) {
206 [ + + + + : 91 : if (path.empty() || path.length() > base::constants::MAX_UDS_PATH_LENGTH) {
+ + ]
207 : 6 : return false;
208 : : }
209 : :
210 : : // UDS path should be a valid file system path.
211 : : // For simplicity, we check if it's not empty and doesn't contain null characters.
212 : : // On Linux/Unix, almost any character except null is valid in a filename.
213 [ - + ]: 85 : if (path.find('\0') != std::string::npos) {
214 : 0 : return false;
215 : : }
216 : :
217 : 85 : return true;
218 : : }
219 : :
220 : 40 : bool InputValidator::is_valid_device_path(const std::string& device) {
221 : : // Basic device path validation
222 : : // - Must not be empty
223 : : // - Must start with '/' (Unix-style) or be a COM port (Windows-style)
224 : : // - Must not contain invalid characters
225 : :
226 [ + + ]: 40 : if (device.empty()) {
227 : 1 : return false;
228 : : }
229 : :
230 : : // Unix-style device path (e.g., /dev/ttyUSB0, /dev/ttyACM0)
231 : : // Must start with "/dev/" for security
232 [ + + + - : 39 : if (device.length() >= 5 && device.substr(0, 5) == "/dev/") {
+ - + + +
+ + + -
- ]
233 : : // Check for valid Unix device path characters
234 [ + + ]: 270 : for (char c : device) {
235 [ + + + + : 252 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '/' && c != '_' && c != '-') {
+ - + - ]
236 : 4 : return false;
237 : : }
238 : : }
239 : 18 : return true;
240 : : }
241 : :
242 : : // Windows-style COM port (e.g., COM1, COM2, etc.)
243 [ + + + - : 17 : if (device.length() >= 4 && device.substr(0, 3) == "COM") {
+ - + + +
+ + + -
- ]
244 : 4 : std::string port_num = device.substr(3);
245 : :
246 : : // Check if port_num contains only digits
247 [ + - + + ]: 8 : if (port_num.empty() ||
248 [ + - + + ]: 11 : !std::all_of(port_num.begin(), port_num.end(), [](unsigned char c) { return std::isdigit(c); })) {
249 : 1 : return false;
250 : : }
251 : :
252 : : try {
253 : 3 : int port = std::stoi(port_num);
254 [ + + + + ]: 3 : return port >= 1 && port <= 255;
255 : 0 : } catch (const std::exception&) {
256 : 0 : return false;
257 : 0 : }
258 : 4 : }
259 : :
260 : : // Windows special device names
261 [ + + + + : 25 : if (device == "NUL" || device == "CON" || device == "PRN" || device == "AUX" || device == "LPT1" ||
+ + + + ]
262 [ + + + + : 25 : device == "LPT2" || device == "LPT3") {
+ + + + ]
263 : 7 : return true;
264 : : }
265 : :
266 : 6 : return false;
267 : : }
268 : :
269 : : } // namespace util
270 : : } // namespace wirestead
|