1: <?php
2: declare(strict_types=1);
3: /**
4: * +------------------------------------------------------------+
5: * | apnscp |
6: * +------------------------------------------------------------+
7: * | Copyright (c) Apis Networks |
8: * +------------------------------------------------------------+
9: * | Licensed under Artistic License 2.0 |
10: * +------------------------------------------------------------+
11: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
12: * +------------------------------------------------------------+
13: */
14:
15: /**
16: * Provides interface bindings to apnscp.
17: *
18: * @package core
19: */
20: class Ipinfo_Module extends Module_Skeleton implements \Module\Skeleton\Contracts\Hookable
21: {
22: // netmask of allowable IP addresses
23: const IP_ALLOCATION_BLOCK = DNS_ALLOCATION_CIDR;
24:
25: const NAMEBASED_INTERFACE_FILE = '/etc/virtualhosting/interface';
26:
27: /**
28: * {{{ void __construct(void)
29: */
30: public function __construct()
31: {
32: parent::__construct();
33:
34: $this->exportedFunctions = array(
35: '*' => PRIVILEGE_SITE,
36: 'release_ip' => PRIVILEGE_ADMIN,
37: 'ip_allocated' => PRIVILEGE_ADMIN,
38: );
39: }
40:
41: /**
42: * Release IP into allocation pool
43: *
44: * @param $ip IP address to release
45: * @param $domain additional truthiness check before releasing IP
46: * @return bool
47: */
48: public function release_ip($ip, $domain = null)
49: {
50: return $this->__deleteIP($ip, $domain);
51: }
52:
53: /*
54: * Check zone for errors
55: *
56: * Additional DNS records are formatted as arguments
57: * to add_record()-
58: * check_zone("debug.com",["mail","a",86400,"127.0.0.1"])
59: *
60: * Hostname, RR, TTL, and parameter are required
61: *
62: * @param $zone zone - usually a domain
63: * @param $recs additional DNS records used in check
64: *
65: * @return bool
66: */
67:
68: /**
69: * Release PTR assignment from an IP
70: *
71: * @param $ip
72: * @param string $domain confirm PTR rDNS matches domain
73: * @return bool
74: */
75: protected function __deleteIP($ip, $domain = null)
76: {
77: return true;
78: }
79:
80: public function _delete()
81: {
82: $conf = $this->getAuthContext()->getAccount()->cur;
83: if (!$this->getServiceValue('ipinfo', 'namebased')) {
84: $ips = (array)$this->getServiceValue('ipinfo', 'ipaddrs');
85: // pass the domain to verify the PTR isn't detached incorrectly
86: // from another domain that has recycled it
87: $domain = $this->getServiceValue('siteinfo', 'domain');
88: foreach ($ips as $ip) {
89: $this->__deleteIP($ip, $domain);
90: }
91: }
92: }
93:
94: public function _create()
95: {
96: $ipinfo = $this->getAuthContext()->conf('ipinfo', 'cur');
97: $siteinfo = $this->getAuthContext()->conf('siteinfo', 'cur');
98: $domain = $siteinfo['domain'];
99: $ip = $ipinfo['namebased'] ? $ipinfo['nbaddrs'] : $ipinfo['ipaddrs'];
100: //$this->add_zone($domain, $ip[0]);
101: if (!$ipinfo['namebased']) {
102: $this->_addIP($ip[0], $siteinfo['domain']);
103: }
104: }
105:
106: /**
107: * Add an IP address to hosting
108: *
109: * @param string $ip
110: * @param string $hostname
111: * @return bool
112: */
113: protected function _addIP($ip, $hostname = '')
114: {
115: return true;
116: }
117:
118: public function _edit_user(string $userold, string $usernew, array $oldpwd)
119: {
120: // TODO: Implement _edit_user() method.
121: }
122:
123: public function _create_user(string $user)
124: {
125: // TODO: Implement _create_user() method.
126: }
127:
128: public function _delete_user(string $user)
129: {
130: // TODO: Implement _delete_user() method.
131: }
132:
133: public function _edit()
134: {
135: $conf_old = $this->getAuthContext()->conf('ipinfo', 'old');
136: $conf_new = $this->getAuthContext()->conf('ipinfo', 'new');
137: $domainold = array_get($this->getAuthContext()->getAccount()->old, 'siteinfo.domain');
138: $domainnew = array_get($this->getAuthContext()->getAccount()->new, 'siteinfo.domain');
139:
140: // changing to IP address, no IP address specified in commandline
141: // this can't happen yet, because configuration requires an IP before
142: // it can be committed (and therefore invoke editVirtDomain.sh)
143: if ($conf_old['namebased'] && !$conf_new['namebased'] && !$conf_new['ipaddrs']) {
144: $ip = \Opcenter\Net\Ip4::allocate();
145: $conf_new['ipaddrs'] = array($ip);
146: info("allocated ip `%s'", $ip);
147: }
148: // domain name change via auth_change_domain()
149: if ($domainold !== $domainnew) {
150: $ip = $conf_new['namebased'] ? array_pop($conf_new['nbaddrs']) :
151: array_pop($conf_new['ipaddrs']);
152: //$this->remove_zone($domainold);
153: //$this->add_zone($domainnew, $ip);
154: // domain name changed
155: if (!$conf_new['namebased']) {
156: //$this->__changePTR($ip, $domainnew, $domainold);
157: }
158: }
159:
160: if ($conf_new === $conf_old) {
161: return;
162: }
163: $ipadd = $ipdel = array();
164:
165: if ($conf_old['namebased'] && !$conf_new['namebased']) {
166: // enable ip hosting
167: $ipadd = $conf_new['ipaddrs'];
168: } else {
169: if (!$conf_old['namebased'] && $conf_new['namebased']) {
170: // disable ip hosting
171: $ipdel = $conf_old['ipaddrs'];
172: } else {
173: // add/remove ip hosting
174: $ipdel = array_diff((array)$conf_old['ipaddrs'], (array)$conf_new['ipaddrs']);
175: $ipadd = array_diff((array)$conf_new['ipaddrs'], (array)$conf_old['ipaddrs']);
176: }
177: }
178:
179: foreach ($ipdel as $ip) {
180: // NB __changePTR is called before to update domain on change
181: $this->__deleteIP($ip, $domainnew);
182: }
183:
184: foreach ($ipadd as $ip) {
185: $this->_addIP($ip, $domainnew);
186: }
187:
188: $domains = array_keys($this->web_list_domains());
189: if ($conf_old['namebased'] && !$conf_new['namebased']) {
190: // added ip-based hosting
191: $ipdel = $conf_old['nbaddrs'];
192: } else {
193: if (!$conf_old['namebased'] && $conf_new['namebased']) {
194: // removed ip-based hosting
195: $ipadd = $conf_new['nbaddrs'];
196: }
197: }
198: if (!$this->dns_configured()) {
199: return;
200: }
201: // change DNS
202: // there will always be a 1:1 pairing for IP addresses
203: foreach ($ipadd as $newip) {
204: $oldip = array_pop($ipdel);
205: $class = apnscpFunctionInterceptor::get_autoload_class_from_module('dns');
206: $newparams = array('ttl' => $class::DNS_TTL, 'parameter' => $newip);
207: foreach ($domains as $domain) {
208: $records = $this->dns_get_records_by_rr('A', $domain);
209: foreach ($records as $r) {
210: if ($r['parameter'] !== $oldip) {
211: continue;
212: }
213: if (!$this->dns_modify_record($r['domain'], $r['subdomain'], 'A', $oldip, $newparams)) {
214: $frag = ltrim($r['subdomain'] . ' . ' . $r['domain'], '.');
215: error('failed to modify record for `' . $frag . "'");
216: } else {
217: $pieces = array($r['subdomain'], $r['domain']);
218: $host = trim(join('.', $pieces), '.');
219: info("modified `%s'", $host);
220: }
221: }
222: }
223: }
224:
225: return;
226: }
227:
228: /**
229: * Check whether IP is assigned
230: *
231: * Assigned IP addresses will have PTRs. Unassigned will be empty.
232: *
233: * @param $ip string ip address
234: * @return bool
235: */
236: public function ip_allocated($ip)
237: {
238: return gethostbyaddr($ip) !== $ip;
239: }
240:
241: public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
242: {
243: return true;
244: }
245:
246: /**
247: * Announce an IP address via ARP
248: *
249: * @param string $ip
250: * @return bool
251: */
252: protected function _announceIP($ip)
253: {
254: $iface = $this->_getInterface();
255: $proc = new Util_Process_Schedule('+2 minutes');
256: $newpath = join(PATH_SEPARATOR, array('/sbin', getenv('PATH')));
257: $proc->setEnvironment('PATH', $newpath);
258: $ret = $proc->run(
259: 'arping -U -I %s -c 1 %s',
260: $iface,
261: $ip
262: );
263:
264: return $ret['success'];
265: }
266:
267: /**
268: * Get interface names to use with hosting
269: *
270: * @return bool|string
271: */
272: protected function _getInterface()
273: {
274: // default in case file is missing
275: $iface = 'eth0';
276: if (!file_exists(static::NAMEBASED_INTERFACE_FILE)) {
277: warn("missing interface file `%s', assuming main iface is `%s'",
278: static::NAMEBASED_INTERFACE_FILE, $iface);
279:
280: return $iface;
281: }
282: $iface = file_get_contents(static::NAMEBASED_INTERFACE_FILE);
283:
284: return trim($iface);
285: }
286: }