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