1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class containing methods for IP range and network mask parsing. 24 */ 25class CIPRangeParser { 26 27 /** 28 * An error message if IP range is not valid. 29 * 30 * @var string 31 */ 32 private $error; 33 34 /** 35 * Maximum amount of IP addresses. 36 * 37 * @var string 38 */ 39 private $max_ip_count; 40 41 /** 42 * IP address range with maximum amount of IP addresses. 43 * 44 * @var string 45 */ 46 private $max_ip_range; 47 48 /** 49 * @var CIPv4Parser 50 */ 51 private $ipv4_parser; 52 53 /** 54 * @var CIPv6Parser 55 */ 56 private $ipv6_parser; 57 58 /** 59 * @var CDnsParser 60 */ 61 private $dns_parser; 62 63 /** 64 * @var CUserMacroParser 65 */ 66 private $user_macro_parser; 67 68 /** 69 * @var CMacroParser 70 */ 71 private $macro_parser; 72 73 /** 74 * Supported options: 75 * v6 enabled support of IPv6 addresses 76 * dns enabled support of DNS names 77 * ranges enabled support of IP ranges like 192.168.3.1-255 78 * max_ipv4_cidr maximum value for IPv4 CIDR subnet mask notations 79 * usermacros allow usermacros syntax 80 * macros allow macros syntax like {HOST.HOST}, {HOST.NAME}, ... 81 * 82 * @var array 83 */ 84 private $options = [ 85 'v6' => true, 86 'dns' => true, 87 'ranges' => true, 88 'max_ipv4_cidr' => 32, 89 'usermacros' => false, 90 'macros' => [] 91 ]; 92 93 /** 94 * @param array $options 95 */ 96 public function __construct(array $options = []) { 97 foreach (['v6', 'dns', 'ranges', 'max_ipv4_cidr', 'usermacros', 'macros'] as $option) { 98 if (array_key_exists($option, $options)) { 99 $this->options[$option] = $options[$option]; 100 } 101 } 102 103 $this->ipv4_parser = new CIPv4Parser(); 104 if ($this->options['v6']) { 105 $this->ipv6_parser = new CIPv6Parser(); 106 } 107 if ($this->options['dns']) { 108 $this->dns_parser = new CDnsParser(); 109 } 110 if ($this->options['usermacros']) { 111 $this->user_macro_parser = new CUserMacroParser(); 112 } 113 if ($this->options['macros']) { 114 $this->macro_parser = new CMacroParser($this->options['macros']); 115 } 116 } 117 118 /** 119 * Validate comma-separated IP address ranges. 120 * 121 * @param string $ranges 122 * 123 * @return bool 124 */ 125 public function parse($ranges) { 126 $this->error = ''; 127 $this->max_ip_count = '0'; 128 $this->max_ip_range = ''; 129 130 foreach (explode(',', $ranges) as $range) { 131 $range = trim($range, " \t\r\n"); 132 133 if (!$this->isValidMask($range) && !$this->isValidRange($range) && !$this->isValidDns($range) 134 && !$this->isValidUserMacro($range) && !$this->isValidMacro($range)) { 135 $this->error = _s('invalid address range "%1$s"', $range); 136 $this->max_ip_count = '0'; 137 $this->max_ip_range = ''; 138 139 return false; 140 } 141 } 142 143 return true; 144 } 145 146 /** 147 * Get first validation error. 148 * 149 * @return string 150 */ 151 public function getError() { 152 return $this->error; 153 } 154 155 /** 156 * Get maximum number of IP addresses. 157 * 158 * @return string 159 */ 160 public function getMaxIPCount() { 161 return $this->max_ip_count; 162 } 163 164 /** 165 * Get range with maximum number of IP addresses. 166 * 167 * @return string 168 */ 169 public function getMaxIPRange() { 170 return $this->max_ip_range; 171 } 172 173 /** 174 * Validate an IP mask. 175 * 176 * @param string $range 177 * 178 * @return bool 179 */ 180 protected function isValidMask($range) { 181 return ($this->isValidMaskIPv4($range) || $this->isValidMaskIPv6($range)); 182 } 183 184 /** 185 * Validate an IPv4 mask. 186 * 187 * @param string $range 188 * 189 * @return bool 190 */ 191 protected function isValidMaskIPv4($range) { 192 $parts = explode('/', $range); 193 194 if (count($parts) != 2) { 195 return false; 196 } 197 198 if ($this->ipv4_parser->parse($parts[0]) != CParser::PARSE_SUCCESS) { 199 return false; 200 } 201 202 if (!preg_match('/^[0-9]{1,2}$/', $parts[1]) || $parts[1] > $this->options['max_ipv4_cidr']) { 203 return false; 204 } 205 206 $ip_count = bcpow(2, 32 - $parts[1], 0); 207 208 if (bccomp($this->max_ip_count, $ip_count) < 0) { 209 $this->max_ip_count = $ip_count; 210 $this->max_ip_range = $range; 211 } 212 213 return true; 214 } 215 216 /** 217 * Validate an IPv6 mask. 218 * 219 * @param string $range 220 * 221 * @return bool 222 */ 223 protected function isValidMaskIPv6($range) { 224 if (!$this->options['v6']) { 225 return false; 226 } 227 228 $parts = explode('/', $range); 229 230 if (count($parts) != 2) { 231 return false; 232 } 233 234 if ($this->ipv6_parser->parse($parts[0]) != CParser::PARSE_SUCCESS) { 235 return false; 236 } 237 238 if (!preg_match('/^[0-9]{1,3}$/', $parts[1]) || $parts[1] > 128) { 239 return false; 240 } 241 242 $ip_count = bcpow(2, 128 - $parts[1], 0); 243 244 if (bccomp($this->max_ip_count, $ip_count) < 0) { 245 $this->max_ip_count = $ip_count; 246 $this->max_ip_range = $range; 247 } 248 249 return true; 250 } 251 252 /** 253 * Validate an IP address range. 254 * 255 * @param string $range 256 * 257 * @return bool 258 */ 259 protected function isValidRange($range) { 260 return ($this->isValidRangeIPv4($range) || $this->isValidRangeIPv6($range)); 261 } 262 263 /** 264 * Validate an IPv4 address range. 265 * 266 * @param string $range 267 * 268 * @return bool 269 */ 270 protected function isValidRangeIPv4($range) { 271 $parts = explode('.', $range); 272 273 $ip_count = '1'; 274 $ip_parts = []; 275 276 foreach ($parts as $part) { 277 if (preg_match('/^([0-9]{1,3})-([0-9]{1,3})$/', $part, $matches)) { 278 if (!$this->options['ranges'] || $matches[1] > $matches[2]) { 279 return false; 280 } 281 282 $ip_count = bcmul($ip_count, $matches[2] - $matches[1] + 1, 0); 283 $ip_parts[] = $matches[2]; 284 } 285 else { 286 $ip_parts[] = $part; 287 } 288 } 289 290 if ($this->ipv4_parser->parse(implode('.', $ip_parts)) != CParser::PARSE_SUCCESS) { 291 return false; 292 } 293 294 if (bccomp($this->max_ip_count, $ip_count) < 0) { 295 $this->max_ip_count = $ip_count; 296 $this->max_ip_range = $range; 297 } 298 299 return true; 300 } 301 302 /** 303 * Validate an IPv6 address range. 304 * 305 * @param string $range 306 * 307 * @return bool 308 */ 309 protected function isValidRangeIPv6($range) { 310 if (!$this->options['v6']) { 311 return false; 312 } 313 314 $parts = explode(':', $range); 315 316 $ip_count = '1'; 317 $ip_parts = []; 318 319 foreach ($parts as $part) { 320 if (preg_match('/^([a-f0-9]{1,4})-([a-f0-9]{1,4})$/i', $part, $matches)) { 321 sscanf($matches[1], '%x', $from); 322 sscanf($matches[2], '%x', $to); 323 324 if (!$this->options['ranges'] || $from > $to) { 325 return false; 326 } 327 328 $ip_count = bcmul($ip_count, $to - $from + 1, 0); 329 $ip_parts[] = $matches[1]; 330 } 331 else { 332 $ip_parts[] = $part; 333 } 334 } 335 336 if ($this->ipv6_parser->parse(implode(':', $ip_parts)) != CParser::PARSE_SUCCESS) { 337 return false; 338 } 339 340 if (bccomp($this->max_ip_count, $ip_count) < 0) { 341 $this->max_ip_count = $ip_count; 342 $this->max_ip_range = $range; 343 } 344 345 return true; 346 } 347 348 /** 349 * Validate a DNS name. 350 * 351 * @param string $range 352 * 353 * @return bool 354 */ 355 protected function isValidDns($range) { 356 if (!$this->options['dns']) { 357 return false; 358 } 359 360 if ($this->dns_parser->parse($range) != CParser::PARSE_SUCCESS) { 361 return false; 362 } 363 364 if (bccomp($this->max_ip_count, 1) < 0) { 365 $this->max_ip_count = '1'; 366 $this->max_ip_range = $range; 367 } 368 369 return true; 370 } 371 372 /** 373 * Validate a user macros syntax. 374 * 375 * @param string $range 376 * 377 * @return bool 378 */ 379 protected function isValidUserMacro($range) { 380 if (!$this->options['usermacros']) { 381 return false; 382 } 383 384 return ($this->user_macro_parser->parse($range) == CParser::PARSE_SUCCESS); 385 } 386 387 /** 388 * Validate a host macros syntax. 389 * Example: {HOST.IP}, {HOST.CONN} etc. 390 * 391 * @param string $range 392 * 393 * @return bool 394 */ 395 protected function isValidMacro($range) { 396 if (!$this->options['macros']) { 397 return false; 398 } 399 400 return ($this->macro_parser->parse($range) == CParser::PARSE_SUCCESS); 401 } 402} 403