├── README.md ├── .gitignore ├── bin └── xipd ├── package.json ├── LICENSE ├── Cakefile ├── test └── test_xip.coffee ├── src └── index.coffee └── lib └── index.js /README.md: -------------------------------------------------------------------------------- 1 | xip daemon 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /node_modules 3 | -------------------------------------------------------------------------------- /bin/xipd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var xip = require('..'); 4 | var argv = process.argv; 5 | 6 | if (argv.length < 3) { 7 | console.error("usage: xipd [address]"); 8 | process.exit(1); 9 | } 10 | 11 | var domain = argv[2]; 12 | var address = argv[3] || "127.0.0.1"; 13 | var server = xip.createServer(domain, address); 14 | server.bind(5300); 15 | 16 | console.log("xipd listening on port 5300: domain =", domain, "address =", address); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "xip" 2 | , "description": "xip dns server" 3 | , "author": "Sam Stephenson" 4 | , "version": "0.1.0" 5 | , "licenses": [ 6 | { "type": "MIT" 7 | , "url": "https://github.com/sstephenson/xip/raw/master/LICENSE" 8 | }] 9 | , "repository": 10 | { "type": "git" 11 | , "url": "https://github.com/sstephenson/xip.git" 12 | } 13 | , "main": "./lib/index.js" 14 | , "dependencies": 15 | { "dnsserver.js": "https://github.com/sstephenson/dnsserver.js/tarball/4f2c713" 16 | } 17 | , "devDependencies": 18 | { "nodeunit": ">=0.5.0" 19 | , "coffee-script": "~1.3" 20 | } 21 | , "engines": 22 | { "node": "~>0.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Sam Stephenson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | {print} = require 'util' 3 | {spawn, exec} = require 'child_process' 4 | 5 | build = (watch, callback) -> 6 | if typeof watch is 'function' 7 | callback = watch 8 | watch = false 9 | options = ['-c', '-o', 'lib', 'src'] 10 | options.unshift '-w' if watch 11 | 12 | coffee = spawn 'coffee', options 13 | coffee.stdout.on 'data', (data) -> print data.toString() 14 | coffee.stderr.on 'data', (data) -> print data.toString() 15 | coffee.on 'exit', (status) -> callback?() if status is 0 16 | 17 | task 'docs', 'Generate annotated source code with Docco', -> 18 | fs.readdir 'src', (err, contents) -> 19 | files = ("src/#{file}" for file in contents when /\.coffee$/.test file) 20 | docco = spawn 'docco', files 21 | docco.stdout.on 'data', (data) -> print data.toString() 22 | docco.stderr.on 'data', (data) -> print data.toString() 23 | docco.on 'exit', (status) -> callback?() if status is 0 24 | 25 | task 'build', 'Compile CoffeeScript source files', -> 26 | build() 27 | 28 | task 'watch', 'Recompile CoffeeScript source files when modified', -> 29 | build true 30 | 31 | task 'test', 'Run the test suite', -> 32 | build -> 33 | {reporters} = require 'nodeunit' 34 | reporters.default.run ['test'] 35 | -------------------------------------------------------------------------------- /test/test_xip.coffee: -------------------------------------------------------------------------------- 1 | xip = require ".." 2 | {exec} = require "child_process" 3 | 4 | createServer = (callback) -> 5 | server = xip.createServer "xip.io", "1.2.3.4" 6 | server.bind 0 7 | {port} = server.address() 8 | callback port, (done) -> 9 | server.close() 10 | done() 11 | 12 | dig = (port, type, hostname, callback) -> 13 | exec "dig @0.0.0.0 -p #{port} #{type} #{hostname}", (err, stdout, stderr) -> 14 | callback stdout 15 | 16 | digShort = (port, type, hostname, callback) -> 17 | exec "dig +short @0.0.0.0 -p #{port} #{type} #{hostname}", (err, stdout, stderr) -> 18 | result = stdout.split("\n")[0] unless err 19 | callback result 20 | 21 | module.exports = 22 | "encoding is unique": (test) -> 23 | test.expect 3 24 | test.equal xip.encode("10.0.0.1"), xip.encode("10.0.0.1") 25 | test.notEqual xip.encode("10.0.0.1"), xip.encode("10.0.0.2") 26 | test.notEqual xip.encode("10.0.0.1"), xip.encode("192.168.0.1") 27 | test.done() 28 | 29 | "xip.io": (test) -> 30 | test.expect 1 31 | createServer (port, done) -> 32 | digShort port, "A", "xip.io", (result) -> 33 | test.equal "1.2.3.4", result 34 | done test.done 35 | 36 | "ns-1.xip.io": (test) -> 37 | test.expect 1 38 | createServer (port, done) -> 39 | digShort port, "A", "ns-1.xip.io", (result) -> 40 | test.equal "1.2.3.4", result 41 | done test.done 42 | 43 | "NS query": (test) -> 44 | test.expect 1 45 | createServer (port, done) -> 46 | dig port, "NS", "xip.io", (result) -> 47 | test.ok result.match /xip\.io\.\s+600\s+IN\s+SOA\s+ns-1\.xip\.io\.\s+hostmaster\.xip\.io\.\s+\d+\s+28800\s+7200\s+604800\s+3600/ 48 | done test.done 49 | 50 | "encoded": (test) -> 51 | test.expect 1 52 | createServer (port, done) -> 53 | address = "10.0.0.1" 54 | hostname = "#{xip.encode address}.xip.io" 55 | digShort port, "A", hostname, (result) -> 56 | test.equal address, result 57 | done test.done 58 | 59 | "lookup": (test) -> 60 | test.expect 1 61 | createServer (port, done) -> 62 | address = "10.0.0.2" 63 | hostname = "#{address}.xip.io." 64 | digShort port, "A", hostname, (result) -> 65 | test.equal address, result 66 | done test.done 67 | 68 | "encoded subdomain": (test) -> 69 | test.expect 1 70 | createServer (port, done) -> 71 | address = "10.0.0.3" 72 | hostname = "foo.#{xip.encode address}.xip.io" 73 | digShort port, "A", hostname, (result) -> 74 | test.equal address, result 75 | done test.done 76 | 77 | "subdomain lookup": (test) -> 78 | test.expect 1 79 | createServer (port, done) -> 80 | address = "10.0.0.4" 81 | hostname = "foo.#{address}.xip.io" 82 | digShort port, "A", hostname, (result) -> 83 | test.equal address, result 84 | done test.done 85 | 86 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | dnsserver = require "dnsserver" 2 | 3 | exports.Server = class Server extends dnsserver.Server 4 | NS_T_A = 1 5 | NS_T_NS = 2 6 | NS_T_CNAME = 5 7 | NS_T_SOA = 6 8 | NS_C_IN = 1 9 | NS_RCODE_NXDOMAIN = 3 10 | 11 | constructor: (domain, @rootAddress) -> 12 | super 13 | @domain = domain.toLowerCase() 14 | @soa = createSOA @domain 15 | @on "request", @handleRequest 16 | 17 | handleRequest: (req, res) => 18 | question = req.question 19 | subdomain = @extractSubdomain question.name 20 | 21 | if subdomain? and isARequest question 22 | res.addRR question.name, NS_T_A, NS_C_IN, 600, subdomain.getAddress() 23 | else if subdomain?.isEmpty() and isNSRequest question 24 | res.addRR question.name, NS_T_SOA, NS_C_IN, 600, @soa, true 25 | else 26 | res.header.rcode = NS_RCODE_NXDOMAIN 27 | 28 | res.send() 29 | 30 | extractSubdomain: (name) -> 31 | Subdomain.extract name, @domain, @rootAddress 32 | 33 | isARequest = (question) -> 34 | question.type is NS_T_A and question.class is NS_C_IN 35 | 36 | isNSRequest = (question) -> 37 | question.type is NS_T_NS and question.class is NS_C_IN 38 | 39 | createSOA = (domain) -> 40 | mname = "ns-1.#{domain}" 41 | rname = "hostmaster.#{domain}" 42 | serial = parseInt new Date().getTime() / 1000 43 | refresh = 28800 44 | retry = 7200 45 | expire = 604800 46 | minimum = 3600 47 | dnsserver.createSOA mname, rname, serial, refresh, retry, expire, minimum 48 | 49 | exports.createServer = (domain, address = "127.0.0.1") -> 50 | new Server domain, address 51 | 52 | exports.Subdomain = class Subdomain 53 | @extract: (name, domain, address) -> 54 | return unless name 55 | name = name.toLowerCase() 56 | offset = name.length - domain.length 57 | 58 | if domain is name.slice offset 59 | subdomain = if 0 >= offset then null else name.slice 0, offset - 1 60 | new constructor subdomain, address if constructor = @for subdomain 61 | 62 | @for: (subdomain = "") -> 63 | if IPAddressSubdomain.pattern.test subdomain 64 | IPAddressSubdomain 65 | else if EncodedSubdomain.pattern.test subdomain 66 | EncodedSubdomain 67 | else 68 | Subdomain 69 | 70 | constructor: (@subdomain, @address) -> 71 | @labels = subdomain?.split(".") ? [] 72 | @length = @labels.length 73 | 74 | isEmpty: -> 75 | @length is 0 76 | 77 | getAddress: -> 78 | @address 79 | 80 | class IPAddressSubdomain extends Subdomain 81 | @pattern = /// (^|\.) 82 | ((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} 83 | (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) 84 | $ /// 85 | 86 | getAddress: -> 87 | @labels.slice(-4).join "." 88 | 89 | class EncodedSubdomain extends Subdomain 90 | @pattern = /(^|\.)[a-z0-9]{1,7}$/ 91 | 92 | getAddress: -> 93 | decode @labels[@length - 1] 94 | 95 | exports.encode = encode = (ip) -> 96 | value = 0 97 | for byte, index in ip.split "." 98 | value += parseInt(byte, 10) << (index * 8) 99 | (value >>> 0).toString 36 100 | 101 | PATTERN = /^[a-z0-9]{1,7}$/ 102 | 103 | exports.decode = decode = (string) -> 104 | return unless PATTERN.test string 105 | value = parseInt string, 36 106 | ip = [] 107 | for i in [1..4] 108 | ip.push value & 0xFF 109 | value >>= 8 110 | ip.join "." 111 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.3.2 2 | (function() { 3 | var EncodedSubdomain, IPAddressSubdomain, PATTERN, Server, Subdomain, decode, dnsserver, encode, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 5 | __hasProp = {}.hasOwnProperty, 6 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 7 | 8 | dnsserver = require("dnsserver"); 9 | 10 | exports.Server = Server = (function(_super) { 11 | var NS_C_IN, NS_RCODE_NXDOMAIN, NS_T_A, NS_T_CNAME, NS_T_NS, NS_T_SOA, createSOA, isARequest, isNSRequest; 12 | 13 | __extends(Server, _super); 14 | 15 | NS_T_A = 1; 16 | 17 | NS_T_NS = 2; 18 | 19 | NS_T_CNAME = 5; 20 | 21 | NS_T_SOA = 6; 22 | 23 | NS_C_IN = 1; 24 | 25 | NS_RCODE_NXDOMAIN = 3; 26 | 27 | function Server(domain, rootAddress) { 28 | this.rootAddress = rootAddress; 29 | this.handleRequest = __bind(this.handleRequest, this); 30 | 31 | Server.__super__.constructor.apply(this, arguments); 32 | this.domain = domain.toLowerCase(); 33 | this.soa = createSOA(this.domain); 34 | this.on("request", this.handleRequest); 35 | } 36 | 37 | Server.prototype.handleRequest = function(req, res) { 38 | var question, subdomain; 39 | question = req.question; 40 | subdomain = this.extractSubdomain(question.name); 41 | if ((subdomain != null) && isARequest(question)) { 42 | res.addRR(question.name, NS_T_A, NS_C_IN, 600, subdomain.getAddress()); 43 | } else if ((subdomain != null ? subdomain.isEmpty() : void 0) && isNSRequest(question)) { 44 | res.addRR(question.name, NS_T_SOA, NS_C_IN, 600, this.soa, true); 45 | } else { 46 | res.header.rcode = NS_RCODE_NXDOMAIN; 47 | } 48 | return res.send(); 49 | }; 50 | 51 | Server.prototype.extractSubdomain = function(name) { 52 | return Subdomain.extract(name, this.domain, this.rootAddress); 53 | }; 54 | 55 | isARequest = function(question) { 56 | return question.type === NS_T_A && question["class"] === NS_C_IN; 57 | }; 58 | 59 | isNSRequest = function(question) { 60 | return question.type === NS_T_NS && question["class"] === NS_C_IN; 61 | }; 62 | 63 | createSOA = function(domain) { 64 | var expire, minimum, mname, refresh, retry, rname, serial; 65 | mname = "ns-1." + domain; 66 | rname = "hostmaster." + domain; 67 | serial = parseInt(new Date().getTime() / 1000); 68 | refresh = 28800; 69 | retry = 7200; 70 | expire = 604800; 71 | minimum = 3600; 72 | return dnsserver.createSOA(mname, rname, serial, refresh, retry, expire, minimum); 73 | }; 74 | 75 | return Server; 76 | 77 | })(dnsserver.Server); 78 | 79 | exports.createServer = function(domain, address) { 80 | if (address == null) { 81 | address = "127.0.0.1"; 82 | } 83 | return new Server(domain, address); 84 | }; 85 | 86 | exports.Subdomain = Subdomain = (function() { 87 | 88 | Subdomain.extract = function(name, domain, address) { 89 | var constructor, offset, subdomain; 90 | if (!name) { 91 | return; 92 | } 93 | name = name.toLowerCase(); 94 | offset = name.length - domain.length; 95 | if (domain === name.slice(offset)) { 96 | subdomain = 0 >= offset ? null : name.slice(0, offset - 1); 97 | if (constructor = this["for"](subdomain)) { 98 | return new constructor(subdomain, address); 99 | } 100 | } 101 | }; 102 | 103 | Subdomain["for"] = function(subdomain) { 104 | if (subdomain == null) { 105 | subdomain = ""; 106 | } 107 | if (IPAddressSubdomain.pattern.test(subdomain)) { 108 | return IPAddressSubdomain; 109 | } else if (EncodedSubdomain.pattern.test(subdomain)) { 110 | return EncodedSubdomain; 111 | } else { 112 | return Subdomain; 113 | } 114 | }; 115 | 116 | function Subdomain(subdomain, address) { 117 | var _ref; 118 | this.subdomain = subdomain; 119 | this.address = address; 120 | this.labels = (_ref = subdomain != null ? subdomain.split(".") : void 0) != null ? _ref : []; 121 | this.length = this.labels.length; 122 | } 123 | 124 | Subdomain.prototype.isEmpty = function() { 125 | return this.length === 0; 126 | }; 127 | 128 | Subdomain.prototype.getAddress = function() { 129 | return this.address; 130 | }; 131 | 132 | return Subdomain; 133 | 134 | })(); 135 | 136 | IPAddressSubdomain = (function(_super) { 137 | 138 | __extends(IPAddressSubdomain, _super); 139 | 140 | function IPAddressSubdomain() { 141 | return IPAddressSubdomain.__super__.constructor.apply(this, arguments); 142 | } 143 | 144 | IPAddressSubdomain.pattern = /(^|\.)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 145 | 146 | IPAddressSubdomain.prototype.getAddress = function() { 147 | return this.labels.slice(-4).join("."); 148 | }; 149 | 150 | return IPAddressSubdomain; 151 | 152 | })(Subdomain); 153 | 154 | EncodedSubdomain = (function(_super) { 155 | 156 | __extends(EncodedSubdomain, _super); 157 | 158 | function EncodedSubdomain() { 159 | return EncodedSubdomain.__super__.constructor.apply(this, arguments); 160 | } 161 | 162 | EncodedSubdomain.pattern = /(^|\.)[a-z0-9]{1,7}$/; 163 | 164 | EncodedSubdomain.prototype.getAddress = function() { 165 | return decode(this.labels[this.length - 1]); 166 | }; 167 | 168 | return EncodedSubdomain; 169 | 170 | })(Subdomain); 171 | 172 | exports.encode = encode = function(ip) { 173 | var byte, index, value, _i, _len, _ref; 174 | value = 0; 175 | _ref = ip.split("."); 176 | for (index = _i = 0, _len = _ref.length; _i < _len; index = ++_i) { 177 | byte = _ref[index]; 178 | value += parseInt(byte, 10) << (index * 8); 179 | } 180 | return (value >>> 0).toString(36); 181 | }; 182 | 183 | PATTERN = /^[a-z0-9]{1,7}$/; 184 | 185 | exports.decode = decode = function(string) { 186 | var i, ip, value, _i; 187 | if (!PATTERN.test(string)) { 188 | return; 189 | } 190 | value = parseInt(string, 36); 191 | ip = []; 192 | for (i = _i = 1; _i <= 4; i = ++_i) { 193 | ip.push(value & 0xFF); 194 | value >>= 8; 195 | } 196 | return ip.join("."); 197 | }; 198 | 199 | }).call(this); 200 | --------------------------------------------------------------------------------