Thu, 14 Aug 2014 19:15:12 +0200
Improve logging date format and integrate transactions in database ops.
michael@0 | 1 | #! /usr/bin/env nodejs |
michael@0 | 2 | // |
michael@0 | 3 | // mDNSGw - Zero Configuration DNS Gateway for Mesh Networks |
michael@0 | 4 | // Copyright © 2014 Michael Schloh von Bennewitz <michael@schloh.com> |
michael@0 | 5 | // |
michael@0 | 6 | // Permission to use, copy, modify, and/or distribute this software for |
michael@0 | 7 | // any purpose with or without fee is hereby granted, provided that the |
michael@0 | 8 | // above copyright notice and this permission notice appear in all copies. |
michael@0 | 9 | // |
michael@0 | 10 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL |
michael@0 | 11 | // WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED |
michael@0 | 12 | // WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE |
michael@0 | 13 | // AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL |
michael@0 | 14 | // DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR |
michael@0 | 15 | // PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS |
michael@0 | 16 | // ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF |
michael@0 | 17 | // THIS SOFTWARE. |
michael@0 | 18 | // |
michael@0 | 19 | // This file is part of mDNSGw, a Zero configuration DNS gateway |
michael@0 | 20 | // which can be found at http://dev.europalab.com/mdnsgw/ |
michael@0 | 21 | // |
michael@0 | 22 | // app.js: ECMA JavaScript implementation |
michael@0 | 23 | // |
michael@0 | 24 | |
michael@0 | 25 | /*********************************************************** |
michael@0 | 26 | | ____ _ _ ____ ____ | |
michael@0 | 27 | | _ __ ___ | _ \| \ | / ___| / ___|_ __ | |
michael@0 | 28 | | | '_ ` _ \| | | | \| \___ \| | _\ \ /\ / / | |
michael@0 | 29 | | | | | | | | |_| | |\ |___) | |_| |\ V V / | |
michael@0 | 30 | | |_| |_| |_|____/|_| \_|____/ \____| \_/\_/ | |
michael@0 | 31 | | | |
michael@0 | 32 | | Requirements: Redis server with standard configuration | |
michael@0 | 33 | | NodeJS and NPM modules (see package.json) | |
michael@0 | 34 | | | |
michael@0 | 35 | | Execute: To start this application, launch it with the | |
michael@0 | 36 | | script named fork.js: $ ./fork.js | |
michael@0 | 37 | | | |
michael@0 | 38 | | Support: http://list.europalab.com/mailman/mdnsgs/ | |
michael@0 | 39 | | | |
michael@10 | 40 | | Test: dig @nodeapp.host.tld A realhost.local | |
michael@10 | 41 | | | |
michael@0 | 42 | ***********************************************************/ |
michael@0 | 43 | |
michael@0 | 44 | // import module dependencies |
michael@0 | 45 | var mdnsinst = require('mdns'); |
michael@0 | 46 | var redisdat = require('redis'); |
michael@0 | 47 | var nameinst = require('native-dns'); |
michael@0 | 48 | |
michael@0 | 49 | |
michael@3 | 50 | // install POSIX signal handlers |
michael@3 | 51 | process.on('SIGUSR2', function() { |
michael@3 | 52 | console.log('SIGUSR2: Dumping mDNSGw entries at', Date()); |
michael@3 | 53 | rediscli.hgetall('hostnames', function (error, object) {console.dir(object);}); |
michael@3 | 54 | }); |
michael@3 | 55 | process.on('SIGHUP', function() { |
michael@3 | 56 | console.log('SIGHUP: Cleared all database entries at', Date()); |
michael@3 | 57 | cleardb(); |
michael@3 | 58 | }); |
michael@3 | 59 | |
michael@10 | 60 | // format a date and time |
michael@10 | 61 | function getDateTime() { |
michael@10 | 62 | var date = new Date(); |
michael@10 | 63 | var hour = date.getHours(); |
michael@10 | 64 | hour = (hour < 10 ? '0' : '') + hour; |
michael@10 | 65 | var min = date.getMinutes(); |
michael@10 | 66 | min = (min < 10 ? '0' : '') + min; |
michael@10 | 67 | var sec = date.getSeconds(); |
michael@10 | 68 | sec = (sec < 10 ? '0' : '') + sec; |
michael@10 | 69 | var year = date.getFullYear(); |
michael@10 | 70 | var month = date.getMonth() + 1; |
michael@10 | 71 | month = (month < 10 ? '0' : '') + month; |
michael@10 | 72 | var day = date.getDate(); |
michael@10 | 73 | day = (day < 10 ? '0' : '') + day; |
michael@10 | 74 | |
michael@10 | 75 | return year + '.' + month + '.' + day + '-' + hour + ':' + min + ':' + sec; |
michael@10 | 76 | } |
michael@10 | 77 | |
michael@0 | 78 | // instantiate a new redis client |
michael@0 | 79 | // http://www.rediscookbook.org/ |
michael@0 | 80 | var rediscli = redisdat.createClient(); |
michael@0 | 81 | rediscli.on('error', function (error) { |
michael@0 | 82 | console.log('Error ' + error); |
michael@0 | 83 | }); |
michael@0 | 84 | |
michael@0 | 85 | // clear mDNS service keys |
michael@3 | 86 | function cleardb () { |
michael@3 | 87 | rediscli.del('hostnames'); |
michael@3 | 88 | // this is not working unfortunately for the loop |
michael@3 | 89 | rediscli.keys('*', function (error, replies) { |
michael@3 | 90 | replies.forEach(function (reply, ident) { |
michael@3 | 91 | rediscli.del(reply, function (error, value) { |
michael@3 | 92 | if (error) throw(error); |
michael@3 | 93 | }); |
michael@0 | 94 | }); |
michael@0 | 95 | }); |
michael@3 | 96 | } |
michael@0 | 97 | |
michael@0 | 98 | // scan all advertised mDNS service types |
michael@3 | 99 | cleardb(); // clear old data first |
michael@0 | 100 | var browsall = mdnsinst.browseThemAll(); |
michael@0 | 101 | browsall.on('serviceUp', function(service) { |
michael@0 | 102 | // iterate through hosts and watch accordingly |
michael@0 | 103 | if (service.type.name.match(/^[a-zA-Z0-9\-]+$/)) { // mdns module hack |
michael@0 | 104 | if (service.type.protocol == 'tcp') { |
michael@0 | 105 | var browserv = mdnsinst.createBrowser(mdnsinst.tcp(service.type.name)); |
michael@0 | 106 | } |
michael@0 | 107 | else if (service.type.protocol == 'udp') { |
michael@0 | 108 | var browserv = mdnsinst.createBrowser(mdnsinst.udp(service.type.name)); |
michael@0 | 109 | } |
michael@0 | 110 | else if (service.type.protocol == 'sctp') { |
michael@0 | 111 | var browserv = mdnsinst.createBrowser(mdnsinst.sctp(service.type.name)); |
michael@0 | 112 | } |
michael@0 | 113 | else throw(error); |
michael@0 | 114 | |
michael@0 | 115 | // common logic for all transports (TCP, UDP, SCTP, etcetera) |
michael@0 | 116 | browserv.on('serviceUp', function(service) { |
michael@0 | 117 | //console.log('service up: ', service); |
michael@0 | 118 | //{interfaceIndex: 2, type: {name: 'ssh', protocol: 'tcp', subtypes: [], fullyQualified: true}, replyDomain: 'local.', flags: 2, name: 'hostname-mich', networkInterface: 'eth0', fullname: 'hostname-mich._ssh._tcp.local.', host: 'hostname-mich.local.', port: 22, addresses: ['192.168.1.50']} |
michael@10 | 119 | rediscli.hget('hostnames', service.host.replace(/\.$/, ''), function (error, value) { |
michael@10 | 120 | // handle unexpected errors |
michael@10 | 121 | if (error) throw(error); |
michael@0 | 122 | |
michael@10 | 123 | // insert one or more IP addresses for each hostname.local. |
michael@10 | 124 | if (!value) { // only visit new (or with changed IPs) hosts |
michael@10 | 125 | var rcmulti = rediscli.multi(); // start a new transaction |
michael@10 | 126 | rcmulti.hsetnx('hostnames', service.host.replace(/\.$/, ''), service.addresses, function (error, nret) { |
michael@10 | 127 | if (nret === 1) // database wrote a new entry |
michael@10 | 128 | console.log(' ' + getDateTime() + ' Detected host: ' + service.host.replace(/\.$/, '') + ' ' + service.addresses); |
michael@10 | 129 | }); //rcmulti.hsetnx(); |
michael@10 | 130 | rcmulti.exec(); // flush transaction queue |
michael@10 | 131 | } |
michael@10 | 132 | }); |
michael@0 | 133 | }); |
michael@0 | 134 | browserv.on('serviceDown', function(service) { |
michael@10 | 135 | //console.log('service down: ', service); |
michael@0 | 136 | //FIXME: still need to selectively remove hosts |
michael@0 | 137 | }); |
michael@3 | 138 | browserv.on('serviceChanged', function(service) { |
michael@10 | 139 | //console.log('service changed: ', service); |
michael@3 | 140 | //FIXME: still need to selectively update hosts |
michael@3 | 141 | }); |
michael@0 | 142 | browserv.start(); |
michael@0 | 143 | } |
michael@0 | 144 | }); |
michael@0 | 145 | browsall.start(); |
michael@0 | 146 | |
michael@0 | 147 | // instantiate a new DNS server |
michael@0 | 148 | var nameserv = nameinst.createServer(); |
michael@0 | 149 | |
michael@0 | 150 | nameserv.on('request', function (request, response) { |
michael@0 | 151 | //console.log(request) |
michael@0 | 152 | |
michael@0 | 153 | // ensure that requested hostname is present |
michael@0 | 154 | rediscli.hget('hostnames', request.question[0].name, function (error, value) { |
michael@0 | 155 | // handle unexpected errors |
michael@0 | 156 | if (error) throw(error); |
michael@0 | 157 | |
michael@0 | 158 | if (value) { // the db succeeded in finding a match |
michael@10 | 159 | // FIXME: need to test incoming questions stripping trailing '.' |
michael@10 | 160 | // FIXME: and adding '.local' to handle cases of non FQDNs. |
michael@10 | 161 | // FIXME: var found; // = {}; doesnt work unfortunately |
michael@10 | 162 | // FIXME: if (request.question[0].name == host) |
michael@10 | 163 | // FIXME: found = host; |
michael@10 | 164 | // FIXME: else if (request.question[0].name + '.local' == host) |
michael@10 | 165 | // FIXME: found = host.replace(/\.local$/, ''); |
michael@10 | 166 | // |
michael@10 | 167 | // FIXME: replace silly new block with simple 'push(nameinst.A) |
michael@10 | 168 | // FIXME: since we already know that the host in question exists |
michael@10 | 169 | // |
michael@0 | 170 | // populate the DNS response with the chosen hostname |
michael@0 | 171 | rediscli.hkeys('hostnames', function (error, replies) { |
michael@0 | 172 | replies.forEach(function (host, index) { |
michael@0 | 173 | if (request.question[0].name == host) { |
michael@0 | 174 | rediscli.hget('hostnames', host, function (error, value) { |
michael@0 | 175 | // handle unexpected errors |
michael@0 | 176 | if (error) throw(error); |
michael@0 | 177 | |
michael@0 | 178 | // FIXME: still must handle multihomed hosts |
michael@0 | 179 | //// a host might have more than one address |
michael@0 | 180 | //value.forEach(function (addr, iter) |
michael@0 | 181 | // set the nameserver address |
michael@0 | 182 | response.answer.push(nameinst.A({ |
michael@0 | 183 | name: host, |
michael@0 | 184 | address: value, |
michael@0 | 185 | ttl: 600, |
michael@0 | 186 | })); |
michael@0 | 187 | response.send(); |
michael@0 | 188 | }); |
michael@0 | 189 | } |
michael@0 | 190 | }); |
michael@0 | 191 | }); |
michael@0 | 192 | } |
michael@0 | 193 | else { |
michael@0 | 194 | response.answer.push(nameinst.A({ |
michael@0 | 195 | name: request.question[0].name, |
michael@0 | 196 | address: '127.0.0.1', |
michael@0 | 197 | ttl: 600, |
michael@0 | 198 | })); |
michael@0 | 199 | response.send(); |
michael@0 | 200 | } |
michael@0 | 201 | }); |
michael@0 | 202 | }); |
michael@0 | 203 | |
michael@0 | 204 | // DNS error handler logic |
michael@0 | 205 | nameserv.on('error', function (err, buff, req, res) { |
michael@0 | 206 | console.log('DNS problem: ', err.stack); |
michael@0 | 207 | }); |
michael@0 | 208 | |
michael@3 | 209 | //// debug process user |
michael@3 | 210 | //console.log('Start.'); |
michael@3 | 211 | //console.log(process.env.USER); |
michael@3 | 212 | //console.log(process.env.SUDO_USER); |
michael@3 | 213 | //console.log('Done.'); |
michael@3 | 214 | // |
michael@3 | 215 | // <1024 must run privileged |
michael@3 | 216 | var nudpport = 53; // default DNS |
michael@3 | 217 | if (nudpport < 1024 && process.getuid() !== 0) { |
michael@3 | 218 | //console.log('Serving on port <1024 from an unprivileged user.\nChange to root if using a privileged port number.') |
michael@3 | 219 | throw new Error('Serving on port <1024 from an unprivileged user.') |
michael@3 | 220 | } |
michael@3 | 221 | |
michael@3 | 222 | // start the DNS process |
michael@3 | 223 | nameserv.serve(nudpport, function () { |
michael@3 | 224 | try { |
michael@3 | 225 | console.log('Starting mDNSGw on', Date()); |
michael@3 | 226 | process.stdout.write('Old UID: ' + process.getuid() + ', Old GID: ' + process.getgid() + '... '); |
michael@3 | 227 | process.umask('0644'); |
michael@3 | 228 | process.setgid('daemon'); |
michael@3 | 229 | if (process.env.SUDO_USER) |
michael@3 | 230 | process.setuid(process.env.SUDO_USER); |
michael@3 | 231 | else |
michael@3 | 232 | process.setuid('daemon'); |
michael@3 | 233 | console.log('New UID: ' + process.getuid() + ', New GID: ' + process.getgid()); |
michael@3 | 234 | } catch (err) { |
michael@3 | 235 | console.log('Cowardly refusing to keep the process alive as root.'); |
michael@3 | 236 | process.exit(1); |
michael@3 | 237 | } |
michael@3 | 238 | }); |
michael@0 | 239 | |
michael@0 | 240 | //// debug print all key and value database entries |
michael@0 | 241 | //rediscli.hgetall('hostnames', function (error, object) {console.dir(object);}); |
michael@0 | 242 | |
michael@0 | 243 | //// display stored mDNS service data entries |
michael@0 | 244 | //rediscli.hkeys('hostnames', function (error, replies) { |
michael@0 | 245 | // console.log(replies.length + ' replies:'); |
michael@0 | 246 | // replies.forEach(function (reply, ident) { |
michael@0 | 247 | // console.log(' ' + ident + ': ' + reply); |
michael@0 | 248 | // }); |
michael@0 | 249 | //}); |
michael@0 | 250 | |
michael@0 | 251 | //// block executes on program termination |
michael@0 | 252 | //rediscli.quit(); // cleanup db connection |
michael@0 | 253 | //browserv.stop(); // zombie scope too bad |
michael@0 | 254 | //browsall.stop(); |