Coverage

75%
1028
780
248

src/lib/blockchain.coffee

60%
79
48
31
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14###
15
16INSTRUCTIONS:
17
18 1. Copy this file and rename it to your blockchain's name.
19 The name you choose will also be your metaTLD (e.g. namecoin.coffee => namecoin.dns)
20 2. Rename the class (following the same naming convention as shown in
21 `blockchains/namecoin.coffee`) and `extend BlockchainResolver`
22 3. Uncomment and edit the code as appropriate.
23 Look at how the other blockchains do it (especially namecoin.coffee)
24
25 REMEMBER: When in doubt, look at `blockchains/namecoin.coffee` !
26
27###
28
291module.exports = (dnschain) ->
30 # expose these into our namespace
3124 for k of dnschain.globals
32672 eval "var #{k} = dnschain.globals.#{k};"
33
34 # Uncomment this:
35 # BlockchainResolver = require('../blockchain.coffee')(dnschain)
36
37 # For `dnsHandler:`
3824 ResolverStream = require('./resolver-stream')(dnschain)
3924 NAME_RCODE = dns2.consts.NAME_TO_RCODE
4024 RCODE_NAME = dns2.consts.RCODE_TO_NAME
41
42 # This class is partially annotated using flowtate
43 # http://flowtype.org/blog/2015/02/20/Flow-Comments.html
44 # https://github.com/jareware/flotate
45 ### @flow ###
46
47 # Uncomment the 'extends' comment below:
4824 class BlockchainResolver # extends BlockchainResolver
49 # Do you initialization in here.
5024 ### (dnschain: DNSChain): BlockchainResolver ###
51 constructor: (@dnschain) ->
52 # Fill these in as appropriate:
530 @log = gNewLogger 'YourChainName'
540 @tld = 'chn' # Your chain's TLD
550 @name = 'templatechain' # Your chain's name (lowercase, no spaces)
56
57 # Optionally specify how long Redis should cache entries in seconds
58 # @cacheTTL = 600 # 0 == no cache, override here
59
60 # Fills this object with `startCheck` and `shutdownCheck` methods
610 gFillWithRunningChecks @
62
63 # This is the default TTL value for all blockchains.
64 # Override it above in the constructor for your blockchain.
65 cacheTTL: gConf.get 'redis:blockchain:ttl'
66
67 # Return `this` upon successful load, falsy otherwise
68 # If you return `this`, this chain will be included in the parent
69 # DNSChain instance's `@servers` list, and will have its `start:`
70 # method called.
71 ### : ?(BlockchainResolver | boolean) ###
72 config: ->
736 @log.debug "Loading resolver config"
746 @
75 # Fill this in with code to load your config.
76 # We recommend copying and editing the stuff from namecoin.coffee
77 #
78 # if "loaded successfully"
79 # return this
80 # else
81 # return false
82
83 # Connect to your blockchain. Return a Promise
84 ### : Promise ###
85 start: ->
86 # Replace this with something useful.
87 # 'cb' is of the form (err, args...) ->
8812 @startCheck (cb) => cb null
89
90 # Close connection to your blockchain and do any other cleanup. Return a Promise.
91 ### : Promise ###
92 shutdown: ->
93 # Replace this with something useful.
94 # 'cb' is of the form (err, args...) ->
9524 @shutdownCheck (cb) => cb null
96
97
98 # Do not modify the result template itself. Instead, set its .data property
99 # in your resources.key function.See how other blockchains do it.
100 ### : object ###
101 resultTemplate: ->
10213 version: '0.0.1'
103 header:
104 datastore: @name
105 data: {} # <== Your `resources.key` function *must* set this to a JSON
106 # object that contains the output from your blockchain.
107
108 standardizers:
109 # These Functions convert data in the `data` object (see `resultTemplate` above)
110 # into a standard form (primarily for processing by `dnsHandler`s.
111 # See nxt.coffee for example of how to override.
1126 dnsInfo: (data) -> data.value # Value sould conform to Namecoin's d/ spec.
1137 ttlInfo: (data) -> 600 # 600 seconds for Namecoin (override if necessary)
114
115 # Any valid resource for a blockchain should be set here.
116 # All keys of this object must be functions of this form:
117 # (property: string, operation: string, fmt: string, args: object, cb: function(err: object, result: object)) ->
118 #
119 # For more information, see:
120 # https://github.com/okTurtles/openname-specifications/blob/resolvers/resolvers.md
121 resources:
122 key: (property, operation, fmt, args, cb) ->
1230 cb new Error "Not Implemented"
124 # Example of what this function should do.
125 # Uncomment and edit:
126 #result = @resultTemplate()
127
128 # myBlockchain.resolve path, (err, answer) =>
129 # if err
130 # cb err
131 # else
132 # result.data = JSON.parse answer
133 # cb null, result
134
135 dnsHandler: # A dictionary of functions corresponding to traditional DNS.
136 # You DO NOT need to copy these functions over unless you want to customize
137 # them for your blockchain. By default, these are designed for Namecoin's d/ spec.
138 # Value of `this` is bound to the class instance (i.e. you blockchain).
139 # TODO: handle all the types specified in the specification!
140 # https://wiki.namecoin.info/index.php?title=Domain_Name_Specification#Value_field
141 #
142 # TODO: handle other info outside of the specification!
143 # - GNS support
144 # - DNSSEC support?
145 # Functions must be of this function signature
146 ### (object, object, number, object, function): any ###
147 A: (req, res, qIdx, data, cb) ->
1487 q = req.question[qIdx]
1497 @log.debug gLineInfo "A handler for #{@name}", {q:q}
1507 info = @standardizers.dnsInfo data
1517 ttl = @standardizers.ttlInfo data
152
153 # According to NMC specification, specifying 'ns'
154 # overrules 'ip' value, so check it here and resolve using
155 # old-style DNS.
1567 if info.ns?.length > 0
157 # 1. Create a stream of nameserver IP addresses out of info.ns
158 # 2. Send request to each of the servers, separated by `stackedDelay`.
159 # On receiving the first answer from any of them, cancel all other
160 # pending requests and respond to our client.
161 #
162 # TODO: handle ns = IPv6 addr!
1631 [nsIPs, nsCNAMEs] = [[],[]]
164
1651 for ip in info.ns
1662 (if net.isIP(ip) then nsIPs else nsCNAMEs).push(ip)
167
1681 if @method is gConsts.oldDNS.NO_OLD_DNS_EVER
1690 nsCNAMEs = []
170
171 # IMPORTANT! This DNSChain server might have a bi-directional relationship
172 # with another local resolver for oldDNS (like PowerDNS). We
173 # don't want malicious data in the blockchain to result in
174 # queries being sent back and forth between them ad-infinitum!
175 # Namecoin's specification states that these should only be
176 # oldDNS TLDs anyway. Use 'delegate', 'import', or 'map' to
177 # refer to other blockchain locations:
178 # https://wiki.namecoin.info/index.php?title=Domain_Name_Specification#Value_field
179 # WARNING! Because of this issue, it's probably best to not create a
180 # bi-directional relationship like this between two resolvers.
181 # It's far safer to tell DNSChain to use a different resolver
182 # that won't re-ask DNSChain any questions.
1831 nsCNAMEs = _.reject nsCNAMEs, (ns)->/\.(bit|dns)$/.test ns
184 # IPs like 127.0.0.1 are checked below against gConf.localhosts array
185
1861 if nsIPs.length == nsCNAMEs.length == 0
1870 return cb NAME_RCODE.REFUSED
188
189 # TODO: use these statically instead of creating new instances for each request
190 # See: https://github.com/okTurtles/dnschain/issues/11
1911 nsCNAME2IP = new ResolverStream
192 name : 'nsCNAME2IP' # +'-'+(instanceNum++)
193 stackedDelay: 100
1941 stackedQuery = new ResolverStream
195 name : 'stackedQuery' #+'-'+(instanceNum-1)
196 stackedDelay: 1000
197 reqMaker : (nsIP) =>
1981 dns2.Request
199 question: q
200 server: address: nsIP
201
2021 nsIPs = gES.merge(gES.readArray(nsIPs), gES.readArray(nsCNAMEs).pipe(nsCNAME2IP))
203
2041 stopRequests = (code) =>
2051 if code
2060 @log.warn gLineInfo("errors on all NS!"), {q:q, code:RCODE_NAME[code]}
207 else
2081 @log.debug gLineInfo('ending async requests'), {q:q}
2091 rs.cancelRequests(true) for rs in [nsCNAME2IP, stackedQuery]
2101 cb code
211
2121 nsIPs.on 'data', (nsIP) =>
2132 if _.find(gConf.localhosts, (ip)->S(nsIP).startsWith ip)
214 # avoid the possible infinite-loop on some (perhaps poorly) configured systems
2150 @log.warn gLineInfo('dropping query, NMC NS ~= localhost!'), {q:q, nsIP:nsIP, info:info}
216 else
2172 stackedQuery.write(nsIP)
218
2191 nsCNAME2IP.on 'failed', (err) =>
2200 @log.warn gLineInfo('nsCNAME2IP error'), {error:err?.message, q:q}
2210 if nsCNAME2IP.errCount == info.ns.length
2220 stopRequests err.code ? NAME_RCODE.NOTFOUND
223
2241 stackedQuery.on 'failed', (err) =>
2250 @log.warn gLineInfo('stackedQuery error'), {error:err?.message, q:q}
2260 if stackedQuery.errCount == info.ns.length
2270 stopRequests err.code ? NAME_RCODE.SERVFAIL
228
2291 stackedQuery.on 'answers', (answers) =>
2301 @log.debug gLineInfo('stackedQuery answers'), {answers:answers}
2311 res.answer.push answers...
2321 stopRequests()
233
2346 else if info.ip
235 # we have its IP! send reply to client
236 # TODO: handle more info! send the rest of the
237 # stuff in 'info', and all the IPs!
2386 info.ip = [info.ip] if typeof info.ip is 'string'
239 # info.ip.forEach (a)-> res.answer.push gIP2type(q.name, ttl)(a)
2406 res.answer.push (info.ip.map gIP2type(q.name, ttl))...
2416 cb()
242 else
2430 @log.warn gLineInfo('no useful data from nmc_show'), {q:q}
2440 cb NAME_RCODE.NOTFOUND
245 # /end 'A'
246
247 # TODO: implement this!
248 AAAA: (req, res, qIdx, data, cb) ->
2490 @log.debug gLineInfo "AAAA handler for #{@name}"
2500 cb NAME_RCODE.NOTIMP
251
252 TLSA: (req, res, qIdx, data, cb) ->
2530 q = req.question[qIdx]
2540 @log.debug gLineInfo "TLSA handler for #{@name}", {q:q}
2550 ttl = @standardizers.ttlInfo data
2560 len = res.answer.length
2570 if info = @standardizers.dnsInfo data
2580 res.answer.push gTls2tlsa(info.tls, ttl, q.name)...
259 # check if any records were added
2600 if res.answer.length - len is 0
2610 @log.warn gLineInfo('no TLSA found'), {q:q, data:data}
2620 cb NAME_RCODE.NOTFOUND
263 else
2640 cb()
265
266 ANY: ->
2670 @log.debug gLineInfo "ANY handler for #{@name}"
268 # TODO: loop through dnsInfo and call approrpriate handlers
269 # TODO: enable EDNS reply
2700 @dnsHandler.A.apply @, [].slice.call arguments

src/lib/blockchains/icann.coffee

95%
23
22
1
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141Packet = require('native-dns-packet')
15
161module.exports = (dnschain) ->
17 # expose these into our namespace
186 for k of dnschain.globals
19168 eval "var #{k} = dnschain.globals.#{k};"
20
216 BlockchainResolver = require('../blockchain.coffee')(dnschain)
22
236 QTYPE_NAME = dns2.consts.QTYPE_TO_NAME
246 NAME_QTYPE = dns2.consts.NAME_TO_QTYPE
256 NAME_RCODE = dns2.consts.NAME_TO_RCODE
266 RCODE_NAME = dns2.consts.RCODE_TO_NAME
27
286 class IcannResolver extends BlockchainResolver
296 constructor: (@dnschain) ->
306 @log = gNewLogger 'ICANN'
316 @name = 'icann'
326 gFillWithRunningChecks @
33
34 resources:
35 key: (property, operation, fmt, args, cb) ->
363 req = new Packet()
373 result = @resultTemplate()
383 @log.debug gLineInfo("#{@name} resolve"), {property:property, args:args}
393 req.question.push dns2.Question {name: property, type: args.type if args?.type? and _.has NAME_QTYPE,args.type}
403 @dnschain.dns.oldDNSLookup req, (code, packet) =>
413 if code
420 cb {code:code, name:RCODE_NAME[code]}
43 else
443 result.data = packet
453 cb null, result
46

src/lib/blockchains/keyid.coffee

61%
42
26
16
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141module.exports = (dnschain) ->
15 # expose these into our namespace
166 for k of dnschain.globals
17168 eval "var #{k} = dnschain.globals.#{k};"
18
196 BlockchainResolver = require('../blockchain.coffee')(dnschain)
20
216 class KeyidResolver extends BlockchainResolver
226 constructor: (@dnschain) ->
236 @log = gNewLogger 'KeyID'
246 @tld = 'p2p'
256 @name = 'keyid'
266 gFillWithRunningChecks @
27
28 config: ->
296 @log.debug "Loading #{@name} resolver"
30
316 return unless gConf.add @name, _.map(_.filter([
32 [process.env.APPDATA, 'KeyID', 'config.json'],
33 [process.env.HOME, '.KeyID', 'config.json'],
34 [process.env.HOME, 'Library', 'Application Support', 'KeyID', 'config.json']]
3518 , (x) -> !!x[0])
3612 , (x) -> path.join x...)
37
386 @params = _.transform ["httpd_endpoint", "rpc_user", "rpc_password"], (o,v) =>
3918 o[v] = gConf.chains[@name].get 'rpc:'+v
40 , {}
41
426 unless _(@params).values().every()
430 missing = _.transform @params, ((o,v,k)->if !v then o.push 'rpc:'+k), []
440 @log.info "Disabled. Missing params:", missing
450 return
46
476 [@params.host, @params.port] = @params.httpd_endpoint.split ':'
486 @params.port = parseInt @params.port
496 gConf.chains[@name].set 'host', @params.host
506 @
51
52 start: ->
536 @startCheck (success) =>
546 params = _.at @params, ["port", "host", "rpc_user", "rpc_password"]
55 # TODO: $create doesn't actually connect. you need to open a raw socket
56 # or an http socket and see if that works before declaring it works
576 @peer = rpc.Client.$create(params...) or gErr "rpc create"
586 @log.info "rpc to bitshares_client on: %s:%d/rpc", @params.host, @params.port
596 success()
60
61 resources:
62 key: (property, operation, fmt, args, cb) ->
630 result = @resultTemplate()
640 @log.debug gLineInfo("#{@name} resolve"), {property:property}
650 @peer.call 'dotp2p_show', [property], property:'/rpc', (err, ans) ->
660 return cb(err) if err
670 if _.isString ans
680 try
690 result.data = JSON.parse ans
70 catch e
710 @log.error glineInfo(e.message)
720 return cb e
730 else if not _.isObject ans
740 @log.warn gLineInfo('type not string or object!'), {json: ans, type: typeof(ans)}
750 result.data = {}
760 cb null, result
77

src/lib/blockchains/namecoin.coffee

78%
52
41
11
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141module.exports = (dnschain) ->
15 # expose these into our namespace
166 for k of dnschain.globals
17168 eval "var #{k} = dnschain.globals.#{k};"
18
196 BlockchainResolver = require('../blockchain.coffee')(dnschain)
20
21 # https://wiki.namecoin.info/index.php?title=Domain_Name_Specification#Regular_Expression
226 VALID_NMC_DOMAINS = /^[a-z]([a-z0-9-]{0,62}[a-z0-9])?$/
23
246 class NamecoinResolver extends BlockchainResolver
256 constructor: (@dnschain) ->
266 @log = gNewLogger 'NMC'
276 @name = 'namecoin'
286 @tld = 'bit'
296 gFillWithRunningChecks @
30
31 config: ->
326 @log.debug "Loading #{@name} resolver"
33
346 return unless gConf.add @name, _.map(_.filter([
35 [process.env.APPDATA, 'Namecoin', 'namecoin.conf']
36 [process.env.HOME, '.namecoin', 'namecoin.conf']
37 [process.env.HOME, 'Library', 'Application Support', 'Namecoin', 'namecoin.conf']
38 ['/etc/namecoin/namecoin.conf']]
3924 , (x) -> !!x[0])
4018 , (x) -> path.join x...), 'INI'
41
426 @params = _.transform ["port", "connect", "user", "password"], (o,v) =>
4324 o[v] = gConf.chains[@name].get 'rpc'+v
44 , {}
456 @params.connect ?= "127.0.0.1"
46
476 unless _(@params).values().every()
480 missing = _.transform @params, ((o,v,k)->if !v then o.push 'rpc'+k), []
490 @log.info "Disabled. Missing params:", missing
500 return
51
526 gConf.chains[@name].set 'host', @params.connect
536 @
54
55 start: ->
566 @startCheck (cb) =>
576 params = _.at @params, ["port", "connect", "user", "password"]
58 # TODO: $create doesn't actually connect. you need to open a raw socket
59 # or an http socket and see if that works before declaring it works
606 @peer = rpc.Client.$create(params...) or gErr "rpc create"
616 @log.info "rpc to namecoind on: %s:%d", @params.connect, @params.port
626 cb null
63
64 resources:
65 key: (property, operation, fmt, args, cb) ->
668 if not operation?
678 result = @resultTemplate()
688 if S(property).endsWith(".#{@tld}") # namecoinize Domain
695 property = S(property).chompRight(".#{@tld}").s
705 if (dotIdx = property.lastIndexOf('.')) != -1
710 property = property.slice(dotIdx+1) #rm subdomain
725 if not VALID_NMC_DOMAINS.test property
730 err = new Error "Invalid Domain: #{property}"
740 err.httpCode = 400
750 return cb(err)
765 property = 'd/' + property
778 @log.debug gLineInfo("#{@name} resolve"), {property:property}
788 @peer.call 'name_show', [property], (err, ans) =>
798 if err
800 @log.error gLineInfo('name_show failed'), {property:property, errMsg:err.message}
810 cb err
82 else
838 try
848 ans.value = JSON.parse ans.value
85 catch e
861 @log.debug gLineInfo('bad JSON'), {value:ans.value, errMsg:e.message}
878 result.data = ans
888 cb null, result
89 else
900 @log.debug gLineInfo "unsupported op #{operation} for prop: #{property}"
910 cb new Error "Not Implemented"
92

src/lib/blockchains/nxt.coffee

82%
47
39
8
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141request = require 'superagent'
15
161module.exports = (dnschain) ->
17 # expose these into our namespace
186 for k of dnschain.globals
19168 eval "var #{k} = dnschain.globals.#{k};"
20
216 BlockchainResolver = require('../blockchain.coffee')(dnschain)
226 ResolverStream = require('../resolver-stream')(dnschain)
23
246 getAsync = Promise.promisify request.get
25
266 QTYPE_NAME = dns2.consts.QTYPE_TO_NAME
276 NAME_QTYPE = dns2.consts.NAME_TO_QTYPE
286 NAME_RCODE = dns2.consts.NAME_TO_RCODE
296 RCODE_NAME = dns2.consts.RCODE_TO_NAME
306 BLOCKS2SEC = 60
31
326 class NxtResolver extends BlockchainResolver
336 constructor: (@dnschain) ->
346 @log = gNewLogger 'NXT'
356 @name = 'nxt'
366 @tld = 'nxt'
376 @standardizers.dnsInfo = (data) -> data.aliasURI
386 gFillWithRunningChecks @
39
40 config: ->
416 @log.debug "Loading #{@name} resolver"
42
436 @params = _.transform ["port", "connect"], (o,v) =>
4412 o[v] = gConf.get 'nxt:'+v
45 , {}
46
476 unless _(@params).values().every()
480 missing = _.transform @params, ((o,v,k)->if !v then o.push 'nxt:'+k), []
490 @log.info "Disabled. Missing params:", missing
500 return
51
526 @peer = "http://#{@params.connect}:#{@params.port}/nxt?requestType=getAlias&aliasName="
53
546 @log.info "Nxt API on:", @params
556 @
56
57 resources:
58 key: (property, operation, fmt, args, cb) ->
592 result = @resultTemplate()
60
612 if S(property).endsWith(".#{@tld}")
621 property = S(property).chompRight(".#{@tld}").s
631 if (dotIdx = property.lastIndexOf('.')) != -1
640 property = property.slice(dotIdx+1) #rm subdomain
65
662 @log.debug gLineInfo("#{@name} resolve"), {property:property}
67
682 getAsync(@peer + encodeURIComponent(property)).then (res) =>
692 try
702 json = JSON.parse res.text
712 if json.aliasURI?
722 try json.aliasURI = JSON.parse json.aliasURI
732 result.data = json
742 @log.debug gLineInfo('resolved OK!'), {result:result}
752 cb null, result
76 catch e
770 @log.warn gLineInfo('server did not respond with valid json!'), {err:e.message, url:res.request.url, response:res.text}
780 cb e
79 .catch (e) =>
800 @log.error gLineInfo('error contacting NXT server!'), {err:e.message}
810 cb e
82

src/lib/cache.coffee

90%
61
55
6
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141if process.env.TEST_DNSCHAIN and not process.env.TEST_REAL_REDIS
151 redis = require 'fakeredis'
16else
170 redis = require 'redis'
18
191module.exports = (dnschain) ->
20 # expose these into our namespace
211 for k of dnschain.globals
2228 eval "var #{k} = dnschain.globals.#{k};"
23
241 class ResolverCache
251 constructor: (@dnschain) ->
266 @log = gNewLogger 'Redis'
276 gFillWithRunningChecks @
28
29 start: ->
306 @startCheck (cb) =>
316 @blockchainEnabled = gConf.get 'redis:blockchain:enabled'
326 @oldDNSEnabled = gConf.get 'redis:oldDNS:enabled'
336 @oldDNSTTL = gConf.get 'redis:oldDNS:ttl'
34
356 if !@blockchainEnabled and !@oldDNSEnabled
365 @log.info "cache not enabled".bold.yellow
37 else
381 @log.info "cache is enabled".bold
391 [host,port] = gConf.get('redis:socket').split(':')
401 @cache =
41 if port?
421 if isNaN(portNum = parseInt port)
430 gErr "Redis port is NaN: #{port}"
441 redis.createClient portNum, host
45 else
460 redis.createClient host
471 @cache.on 'error', (err) =>
480 @log.error "cache errored: #{err.message}", err
490 @shutdown()
506 cb()
51
52 shutdown: ->
536 @shutdownCheck (cb) =>
546 @cache?.end()
556 cb()
56
57 get: (key, valueRetriever, valueDoer) ->
5850 @cache.get key, (err, result) =>
5950 if err
600 @log.error gLineInfo('caching error'), {err: err}
6150 if result?
6225 @log.debug gLineInfo('resolved from cache'), {key: key}
6325 valueDoer null, key, JSON.parse result
6425 return
6525 valueRetriever key, (err2, ttl, value) =>
6625 @cache.setex(key, ttl, JSON.stringify value) if not err2
6725 valueDoer err, key, value
68
69 resolveResource: (datastore, requestFn, serialization, cb) ->
7014 if @blockchainEnabled and datastore.cacheTTL?
712 retriever = (key, callback) =>
721 requestFn (err, result) =>
731 callback err, datastore.cacheTTL, result
742 doer = (err, key, result) =>
752 cb err, result
762 @get serialization, retriever, doer
77 else
7812 requestFn cb
79
80 resolveOldDNS: (req, cb) ->
81104 if @oldDNSEnabled
8248 q = req.question[0]
8348 retriever = (key, callback) =>
8424 f = (err, result) =>
8524 ttl =
86 if result.answer[0]?.ttl?
8723 Math.min result.answer[0].ttl, @oldDNSTTL
88 else
891 @oldDNSTTL
9024 callback err, ttl, result
9124 @dnschain.dns.oldDNSLookup req, f
9248 doer = (err, key, result) =>
9348 cb err, result
9448 @get "oldDNS:#{q.name}:#{q.type}", retriever, doer
95 else
9656 @dnschain.dns.oldDNSLookup req, cb
97

src/lib/config.coffee

80%
55
44
11
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14###
15All configuration options can be overwritten using command line args
16and/or environment variables.
17
18Below you will see the available options and their defaults.
19
20- The top-level options map to sections in the config file.
21 I.e. `dns` and `log` designate the sections `[dns]` and `[log]`
22- All non top-level options are respresented via dot notation.
23 I.e. to set the `oldDNS` `address`, you'd do:
24
25 [dns]
26 oldDNS.address = 8.8.4.4
27
28- For each blockchain, you can specify its configuration file
29 by specifying the blockchain name as a section, and then
30 setting the config variable.
31 Example:
32
33 [namecoin]
34 config = /home/namecoin/.namecoin/namecoin.conf
35
36See also:
37<https://github.com/okTurtles/dnschain/blob/master/docs/How-do-I-run-my-own.md#Configuration>
38###
39
401nconf = require 'nconf'
411props = require 'properties'
421fs = require 'fs'
431tty = require 'tty'
44
451module.exports = (dnschain) ->
46 # expose these into our namespace
471 for k of dnschain.globals
4826 eval "var #{k} = dnschain.globals.#{k};"
49
50 # TODO: add path to our private key for signing answers
511 amRoot = process.getuid() is 0
52
53 # =================================================
54 # BEGIN DNSCHAIN CONFIGURATION OPTIONS AND DEFAULTS
55 # =================================================
561 defaults = {
57 log:
580 level: if process.env.DNS_EXAMPLE then 'debug' else 'info'
59 colors: true
60 pretty: tty.isatty process.stdout
61 timestamp: tty.isatty process.stdout
62 dns:
630 port: if amRoot then 53 else 5333
64 host: '0.0.0.0' # what we bind to
65 externalIP: gExternalIP() # Advertised IP for .dns metaTLD (ex: namecoin.dns)
66 oldDNSMethod: 'NATIVE_DNS' # see 'globals.coffee' for possible values
67 oldDNS:
68 address: '8.8.8.8' # Google (we recommend running PowerDNS yourself and sending it there)
69 port: 53
70 type: 'udp'
71 http:
720 port: if amRoot then 80 else 8088 # Standard HTTP port
730 tlsPort: if amRoot then 443 else 4443 # Standard HTTPS port
741 tlsKey: "#{process.env.HOME}/.dnschain/key.pem"
751 tlsCert: "#{process.env.HOME}/.dnschain/cert.pem"
76 internalTLSPort: 2500 # Not accessible from the internet, used internally only
77 internalAdminPort: 3000 # Not accessible from the internet, used internally only
78 host: '0.0.0.0' # what we bind to. 0.0.0.0 for the whole internet
79 redis:
80 socket: '127.0.0.1:6379' # or UNIX domain socket path
81 oldDNS:
82 enabled: false
83 ttl: 600 # Maximum time to keep DNS records in cache, regardless of TTL
84 blockchain:
85 enabled: false
86 ttl: 600
87
88 unblock: # The options in this section are only for when Unblock is enabled.
89 enabled: false
90 acceptApiCallsTo: ["localhost"] # Add your public facing domain here if you want to accept calls to the RESTful API when Unblock is enabled.
91 routeDomains: { # If traffic coming in on the tlsPort needs to be redirected to another application on the server then add it here
92 # Example: "mywebsite.com" : 9000 # This tells the server to send traffic meant to "mywebsite.com" to port 9000. It'll still be encrypted when it reaches port 9000
93 }
94
95 # WARNING: Do not change these settings unless you know exactly what you're doing.
96 # Read the source code, read the Bottleneck docs,
97 # make sure you understand how it might make your server complicit in DNS Amplification Attacks and your server might be taken down as a result.
98 rateLimiting:
99 dns:
100 maxConcurrent: 1
101 minTime: 200
102 highWater: 2
103 strategy: Bottleneck.strategy.BLOCK
104 penalty: 7000
105 http:
106 maxConcurrent: 2
107 minTime: 150
108 highWater: 10
109 strategy: Bottleneck.strategy.OVERFLOW
110 https:
111 maxConcurrent: 2
112 minTime: 150
113 highWater: 10
114 strategy: Bottleneck.strategy.OVERFLOW
115 }
116 # ===============================================
117 # END DNSCHAIN CONFIGURATION OPTIONS AND DEFAULTS
118 # ===============================================
119
1201 fileFormatOpts =
121 comments: ['#', ';']
122 sections: true
123 namespaces: true
124
1251 props.parse = _.partialRight props.parse, fileFormatOpts
1261 props.stringify = _.partialRight props.stringify, fileFormatOpts
127
1281 confTypes =
129 INI: props
130 JSON: JSON
131
132 # load our config
1331 nconf.argv().env('__')
1341 dnscConfLocs = [
1351 "#{process.env.HOME}/.dnschain/dnschain.conf", # the default
1361 "#{process.env.HOME}/.dnschain.conf",
137 "/etc/dnschain/dnschain.conf"
138 ]
1391 dnscConf = _.find dnscConfLocs, (x) -> fs.existsSync x
140
1411 if process.env.HOME and not fs.existsSync "#{process.env.HOME}/.dnschain"
142 # create this folder on UNIX based systems so that https.coffee
143 # can autogen the private/public key if they don't exist
1440 fs.mkdirSync "#{process.env.HOME}/.dnschain", 0o710
145
146 # we can't access `dnschain.globals.gLogger` here because it hasn't
147 # been defined yet unfortunately.
1481 if dnscConf
1491 console.info "[INFO] Loading DNSChain config from: #{dnscConf}"
1501 nconf.file 'user', {file: dnscConf, format: props}
151 else
1520 console.warn "[WARN] No DNSChain configuration file found. Using defaults!".bold.yellow
1530 nconf.file 'user', {file: dnscConfLocs[0], format: props}
154
1551 config =
156639 get: (key, store="dnschain") -> config.chains[store].get key
1575 set: (key, value, store="dnschain") -> config.chains[store].set key, value
158 chains:
159 dnschain: nconf.defaults defaults
160 add: (name, paths, type) ->
16112 log = dnschain.globals.gLogger
16212 gLineInfo = dnschain.globals.gLineInfo
16312 if config.chains[name]?
16410 log.warn gLineInfo "Not overwriting existing #{name} configuration"
16510 return config.chains[name]
166
1672 paths = [paths] unless Array.isArray(paths)
1682 type = confTypes[type] || confTypes['JSON']
169
170 # if dnschain's config specifies this chain's config path, prioritize it
171 # fixes: https://github.com/okTurtles/dnschain/issues/60
1722 customConfigPath = config.chains.dnschain.get "#{name}:config"
1732 if customConfigPath?
1740 paths = [customConfigPath]
1750 log.info "custom config path for #{name}: #{paths[0]}"
176
1772 confFile = _.find paths, (x) -> fs.existsSync x
178
1792 unless confFile
1800 log.warn "Couldn't find #{name} configuration:".bold.yellow, paths
1810 return
182
1832 conf = (new nconf.Provider()).argv().env()
1842 log.info "#{name} configuration path: #{confFile}"
1852 conf.file 'user', {file: confFile, format: type}
186 # if dnschain's config specifies this chain's config information, use it as default
1872 if config.chains.dnschain.get("#{name}")?
1881 conf.defaults config.chains.dnschain.get "#{name}"
1892 config.chains[name] = conf
190

src/lib/dns.coffee

65%
143
93
50
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14# TODO: go through 'TODO's!
15
161Packet = require('native-dns-packet')
17
181module.exports = (dnschain) ->
19 # expose these into our namespace
201 for k of dnschain.globals
2128 eval "var #{k} = dnschain.globals.#{k};"
22
231 QTYPE_NAME = dns2.consts.QTYPE_TO_NAME
241 NAME_QTYPE = dns2.consts.NAME_TO_QTYPE
251 NAME_RCODE = dns2.consts.NAME_TO_RCODE
261 RCODE_NAME = dns2.consts.RCODE_TO_NAME
27
281 class DNSServer
291 constructor: (@dnschain) ->
306 @log = gNewLogger 'DNS'
316 @log.debug "Loading DNSServer..."
326 @method = gConf.get 'dns:oldDNSMethod'
336 @rateLimiting = gConf.get 'rateLimiting:dns'
34
35 # this is just for development testing of NODE_DNS method
36 # dns.setServers ['8.8.8.8']
37
386 if @method is gConsts.oldDNS.NODE_DNS
390 @log.warn "Using".bold.red, "oldDNSMethod = NODE_DNS".bold, "method is strongly discouraged!".bold.red
400 if dns.getServers?
410 blacklist = _.intersection ['127.0.0.1', '::1', 'localhost'], dns.getServers()
420 if blacklist.length > 0
430 gErr "Cannot use NODE_DNS method when system DNS lists %j as a resolver! Would lead to infinite loop!", blacklist
44 else
450 gErr "Node's DNS module doesn't have 'getServers'. Please upgrade NodeJS."
466 else if @method is gConsts.oldDNS.NO_OLD_DNS
470 @log.warn "oldDNSMethod is set to refuse queries for traditional DNS!".bold
486 else if @method is gConsts.oldDNS.NO_OLD_DNS_EVER
490 @log.warn "oldDNSMethod is set to refuse *ALL* queries for traditional DNS (even if the blockchain wants us to)!".bold.red
506 else if @method isnt gConsts.oldDNS.NATIVE_DNS
510 gErr "No such oldDNSMethod: #{@method}"
52
536 gFillWithRunningChecks @
54
55 start: ->
566 @startCheck (cb) =>
576 @server = dns2.createServer() or gErr "dns2 create"
586 @server.on 'socketError', (err) -> gErr err
596 @server.on 'request', (req, res) =>
60120 domain = req.question[0]?.name
61120 if domain
62120 domain = domain.split(".")
63120 if domain.length > 3
64 # if there are more than 3 parts to the domain, we use the last
65 # letter of the fourth, and the full parts of the last three
66 # This isn't perfect, especially because of:
67 # https://publicsuffix.org/list/effective_tld_names.dat
68 #
69 # See: https://github.com/okTurtles/dnschain/issues/107 !
7013 domain = [domain[-4..][0][-1..]].concat(domain[-3..]).join '.'
71 else
72107 domain = domain[-3..].join '.'
73
74120 key = "dns-#{req.address.address}-#{domain}"
75120 @log.debug gLineInfo("creating bottleneck on: #{key}")
76120 limiter = gThrottle key, => new Bottleneck _.at(@rateLimiting, ['maxConcurrent', 'minTime', 'highWater', 'strategy'])...
77120 limiter.changePenalty(@rateLimiting.penalty).submit (@callback.bind @), req, res, null
78 else
790 @log.warn gLineInfo('received empty request!'), {req:req}
80 # // end on 'request'
816 @server.on 'listening', =>
826 @log.info 'started DNS', gConf.get 'dns'
836 cb()
846 @server.serve gConf.get('dns:port'), gConf.get('dns:host')
85
86 shutdown: ->
876 @shutdownCheck (cb) =>
886 if @server
896 @server.on 'close', cb
906 @server.close()
91 else
920 @log.warn gLineInfo '@server not defined!'
930 cb()
94
95 # (Notes on 'native-dns' version <=0.6.x, which I'd like to see changed.)
96 #
97 # Both `req` and `res` are of type `Packet` (the subclass, as explained next).
98 #
99 # The packet that's inside of 'native-dns' inherits from the one inside of 'native-dns-packet'.
100 # It adds two extra fields (at this time of writing):
101 #
102 # - address: added by Server.prototype.handleMessage and the Packet subclass constructor
103 # - _socket: added by the Packet subclass constructor in lib/packet.js
104 #
105 # `req` and `res` are both instances of this subclass of 'Packet'.
106 # They also have the same 'question' field.
107 #
108 # See also:
109 # - native-dns/lib/server.js
110 # - native-dns/lib/packet.js
111 # - native-dns-packet/packet.js
112 #
113 # Separately, there is a 'Request' class defined in 'native-dns/lib/client.js'.
114 # Like 'Packet', it has a 'send' method.
115 # To understand it see these functions in 'lib/pending.js':
116 #
117 # - SocketQueue.prototype._dequeue (sending)
118 # - SocketQueue.prototype._onmessage (receiving)
119 #
120 # When you create a 'new Request' and send it, it will first create a 'new Packet' and copy
121 # some of the values from the request into it, and then call 'send' on that.
122 # Similarly, in receiving a reply from a Request instance, handle the 'message' event, which
123 # will create a new Packet (the subclass, with the _socket field) from the received data.
124 #
125 # See also:
126 # - native-dns/lib/client.js
127 # - native-dns/lib/pending.js
128 #
129 # Ideally we want to be able to reuse the 'req' received here and pass it along
130 # to oldDNSLookup without having to recreate or copy any information.
131 # See: https://github.com/tjfontaine/node-dns/issues/69
132 #
133 # Even more ideally we want to be able to simply pass along the raw data without having to parse it.
134 # See: https://github.com/okTurtles/dnschain/issues/6
135 #
136 callback: (req, res, cb) ->
137 # answering multiple questions in a query appears to be problematic,
138 # and few servers do it, so we only answer the first question:
139 # https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query
140 # At some point we may still want to support this though.
141112 q = req.question[qIdx=0]
142112 q.name = q.name.toLowerCase()
143
144112 ttl = Math.floor(Math.random() * 3600) + 30 # TODO: pick an appropriate TTL value!
145112 @log.debug "received question", q
146
147112 if (datastore = @dnschain.chainsTLDs[q.name.split('.').pop()])
1487 @log.debug gLineInfo("resolving via #{datastore.name}..."), {domain:q.name, q:q}
149
1507 if not datastore.resources.key?
1510 @log.error gLineInfo "#{datastore.name} does not implement `key` resource!".bold
1520 return @sendErr(res, NAME_RCODE.SERVFAIL, cb)
1537 args = [datastore.name , "key", q.name, null, null, {}] # args conform to the datastore API
1547 resourceRequest = (cb) =>
1556 datastore.resources.key.call datastore, args[2..]..., cb
1567 @dnschain.cache.resolveResource datastore, resourceRequest, JSON.stringify(args), (err, result) =>
1577 if err? or !result
1580 @log.error gLineInfo("#{datastore.name} failed to resolve"), {err:err?.message, result:result, q:q}
1590 @sendErr res, null, cb
160 else
1617 @log.debug gLineInfo("#{datastore.name} resolved query"), {q:q, d:q.name, result:result}
162
1637 if not (handler = datastore.dnsHandler[QTYPE_NAME[q.type]])
1640 @log.warn gLineInfo("no such DNS handler!"), {datastore: datastore.name, q:q, type: QTYPE_NAME[q.type]}
1650 return @sendErr res, NAME_RCODE.NOTIMP, cb
166
1677 handler.call datastore, req, res, qIdx, result.data, (errCode) =>
1687 try
1697 if errCode
1700 @sendErr res, errCode, cb
171 else
1727 @sendRes res, cb
173 catch e
1740 @log.error e.stack
1750 @log.error gLineInfo("exception in handler"), {q:q, result:result}
1760 return @sendErr res, NAME_RCODE.SERVFAIL, cb
177
178105 else if S(q.name).endsWith '.dns'
1791 res.answer.push gIP2type(q.name,ttl,QTYPE_NAME[q.type])(gConf.get 'dns:externalIP')
1801 @log.debug gLineInfo('cb|.dns'), {q:q, answer:res.answer}
1811 @sendRes res, cb
182 else
183104 @log.debug gLineInfo("resolving #{q.name} via oldDNS"), {q:q}
184104 @dnschain.cache.resolveOldDNS req, (code, packet) =>
185104 _.assign res, packet
186104 if code
1870 @sendErr res, code, cb
188 else
189104 @sendRes res, cb
190 # / end callback
191
192 oldDNSLookup: (req, cb) ->
19383 res = new Packet()
19483 sig = "oldDNS{#{@method}}"
19583 q = req.question[0]
19683 filterRes = (p) ->
19783 _.pick p, ['edns_version', 'edns_options', 'edns', 'answer', 'authority', 'additional']
198
19983 @log.debug {fn:sig+':start', q:q}
200
20183 if @method is gConsts.oldDNS.NATIVE_DNS
20283 success = false
203 # TODO: retry in TCP-mode on truncated response (like `dig`)
204 # See: https://github.com/tjfontaine/node-dns/issues/70
20583 req2 = new dns2.Request
206 question: q
207 server : gConf.get 'dns:oldDNS'
208 try_edns: q.type is NAME_QTYPE.ANY or req.edns?
209
210 # 'answer' is a Packet subclass with the .address and ._socket fields
21183 req2.on 'message', (err, answer) =>
21283 if err?
2130 @log.error gLineInfo("should not have an error here!"), {err:err?.message, answer:answer}
2140 req2.DNSErr ?= err
215 else
21683 @log.debug gLineInfo('message'), {answer:answer}
21783 success = true
21883 res = answer
219
22083 req2.on 'error', (err={message:'unknown error'}) =>
2210 @log.error gLineInfo('oldDNS lookup error'), {err:err?.message}
2220 req2.DNSErr = err
223
22483 req2.on 'timeout', (err={message:'timeout'}) =>
2250 @log.warn gLineInfo('oldDNS timeout'), {err:err}
2260 req2.DNSErr = err
227
22883 req2.on 'end', =>
22983 if success
23083 @log.debug gLineInfo('success!'), {q:q, res: _.omit(res, '_socket')}
23183 cb null, filterRes(res)
232 else
233 # TODO: this is noisy.
234 # also make log output look good in journalctl
235 # you can log IP with: res._socket.remote.address
2360 @log.warn gLineInfo('oldDNS lookup failed'), {q:q, err:req2.DNSErr}
2370 cb NAME_RCODE.SERVFAIL, filterRes(res)
238 # @log.debug {fn:"beforesend", req:req2}
23983 req2.send()
2400 else if @method is gConsts.oldDNS.NODE_DNS
2410 dns.resolve q.name, QTYPE_NAME[q.type], (err, addrs) =>
2420 if err
2430 @log.debug {fn:sig+':fail', q:q, err:err?.message}
2440 cb NAME_RCODE.SERVFAIL, filterRes(res)
245 else
246 # USING THIS METHOD IS DISCOURAGED BECAUSE IT DOESN'T
247 # PROVIDE US WITH CORRECT TTL VALUES!!
248 # TODO: pick an appropriate TTL value!
2490 ttl = Math.floor(Math.random() * 3600) + 30
2500 res.answer.push (addrs.map gIP2type(q.name, ttl, QTYPE_NAME[q.type]))...
2510 @log.debug {fn:sig+':success', answer:res.answer, q:q.name}
2520 cb null, filterRes(res)
253 else
254 # refuse all such queries
2550 cb NAME_RCODE.REFUSED, res
256
257 sendRes: (res, cb) ->
258112 try
259112 @log.debug gLineInfo("sending response!"), {res:_.omit(res, '_socket')}
260112 res.send()
261112 cb()
262 catch e
2630 @log.error gLineInfo('error trying send response back!'), {msg:e.message, res:_.omit(res, '_socket'), stack:e.stack}
2640 cb e
265
266 sendErr: (res, code=NAME_RCODE.SERVFAIL, cb) ->
2670 try
2680 res.header.rcode = code
2690 @log.debug gLineInfo(), {code:code, name:RCODE_NAME[code]}
2700 res.send()
271 catch e
2720 @log.error gLineInfo('exception sending error back!'), e.stack
2730 cb()
2740 false # helps other functions pass back an error value
275

src/lib/dnschain.coffee

89%
48
43
5
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14# we don't 'use strict' because i really want to be able to use 'eval' to declare variables
15
16# expose global dependencies, functions, and constants into our namespace
171for k of require('./globals')(exports)
1828 eval "var #{k} = exports.globals.#{k};"
19
201exports.createServer = (a...) -> new DNSChain a...
21
221DNSServer = require('./dns')(exports)
231HTTPServer = require('./http')(exports)
241EncryptedServer = require('./https')(exports)
251ResolverCache = require('./cache')(exports)
26
271localhosts = ->
286 _.uniq [
29 "127.0.0.", "10.0.0.", "192.168.", "::1", "fe80::"
30 gConf.get('dns:host'), gConf.get('http:host')
31 gConf.get('dns:externalIP'), gExternalIP()
3218 _.map(gConf.chains, (c) -> c.get('host'))...
3372 ].filter (o)-> typeof(o) is 'string'
34
351exports.DNSChain = class DNSChain
361 constructor: ->
376 @log = gNewLogger 'DNSChain'
386 chainDir = path.join __dirname, 'blockchains'
396 @chains = _.omit(_.mapValues(_.indexBy(fs.readdirSync(chainDir), (file) =>
4024 S(file).chompRight('.coffee').s
41 ), (file) =>
4224 chain = new (require('./blockchains/'+file)(exports)) @
4324 chain.config()
44 ), (chain) =>
4524 not chain
46 )
476 @chainsTLDs = _.indexBy _.compact(_.map(@chains, (chain) ->
4824 chain if chain.tld?
49 )), 'tld'
506 gConf.localhosts = localhosts.call @
516 @dns = new DNSServer @
526 @http = new HTTPServer @
536 @encryptedserver = new EncryptedServer @
546 @cache = new ResolverCache @
55 # ordered this way to start all external facing servers last
566 @servers = _.values(@chains).concat [@cache, @http, @encryptedserver, @dns]
576 gFillWithRunningChecks @
58
59 start: ->
606 @startCheck (cb) =>
616 Promise.all(@servers.map (s)-> s.start()).then =>
626 [host, port] = ['dns:externalIP', 'dns:port'].map (x)-> gConf.get x
636 @log.info "DNSChain started and advertising DNS on: #{host}:#{port}"
64
656 if process.getuid() isnt 0 and port isnt 53 and require('tty').isatty process.stdout
666 @log.warn "DNS port isn't 53!".bold.red, "While testing you should either run me as root or make sure to set standard ports in the configuration!".bold
676 cb null
680 .catch (e) -> cb e
69 .catch (e) =>
700 @log.error "DNSChain failed to start:", e.stack
710 @shutdown()
720 throw e # re-throw to indicate that this promise failed
73
74 shutdown: ->
756 @shutdownCheck (cb) =>
76 # shutdown servers in the opposite order they were started
776 reversedServers = @servers[..].reverse()
786 Promise.settle reversedServers.map (s, idx) =>
7948 name = s?.name || s?.log?.transports.console?.label
8048 @log.debug "Shutting down server at idx:#{idx}: #{name}"
8148 s?.shutdown()
826 .then -> cb null
830 .catch (e) -> cb e
84

src/lib/globals.coffee

69%
91
63
28
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141module.exports = (dnschain) ->
15
16 # 1. global dependencies
17 #
18 # IMPORTANT: *ALL* DNSChain globals *MUST* be prefixed with a 'g'
19 # *EXCEPT* some of the commonly used global module dependencies below.
20
211 dnschain.globals =
22 rpc : 'json-rpc2'
23 _ : 'lodash'
24 S : 'string'
25 dns2 : 'native-dns'
26 gES : 'event-stream'
27 Bottleneck : 'bottleneck'
28 Promise : 'bluebird'
29
30 # Initialize the global rate limiter pool, only an interface will be visible from the outside
311 limiters = {}
32 # Garbage collect the unused limiters every minute.
33 #This is necessary or else the server will run out of RAM after a few days
341 setInterval ->
350 time = Date.now()
360 for key,limiter of limiters
37 # Unused in the last 5 minutes
380 if (limiter._nextRequest+(60*1000*5)) < time
390 delete limiters[key]
40 , 60*1000 # Every minute
41
42 # no renaming done for these
431 for d in ['dns', 'fs', 'http','net', 'os', 'path', 'tls', 'url', 'util', 'winston']
4410 dnschain.globals[d] = d
45
46 # replace all the string values in dnschain.globals with the module they represent
47 # and expose them into this function's namespace
481 for k,v of dnschain.globals
4917 eval "var #{k} = dnschain.globals.#{k} = require('#{v}');"
50
51 # 2. global constants
521 _.assign dnschain.globals, gConsts:
53 # for questions that the blockchain cannot answer (hopefully these will disappear with time)
54 # >> Use the keys (as string values) in your config! <<
55 # >> Do *NOT* use the numerical values in your config! <<
56 # For now they're supported but their use is DEPRECATED and they *WILL* be removed!
57 oldDNS:
58 # Recommended method
59 NATIVE_DNS: 0 # Use 'native-dns' module (current default). Hopefully merged into NodeJS in the future: https://github.com/joyent/node/issues/6864#issuecomment-32229852
60
61 # !! WARNING !!
62 # > ! USING 'NODE_DNS' IS __STRONGLY DISCOURAGED__ ! <
63 # > ! BECAUSE IT DOES NOT PROVIDE PROVIDE TTL VALUES ! <
64 # > ! ITS USE MAY BE DEPRECATED IN THE FUTURE ! <
65 NODE_DNS: 1 # Prior to node 0.11.x will ignore dnsOpts.oldDNS and use OS-specified DNS. Currently ignores 'dnsOpts.oldDNS' in favor of OS-specified DNS even in node 0.11.x (simply needs to be implemented). TODO: <- this!
66
67 # Refuse to resolve domains in the canonical DNS system
68 # unless the blockchain tells us to, in which case the
69 # NATIVE_DNS method will be used for that purpose.
70 NO_OLD_DNS: 2
71
72 # Never resolve domains in canonical DNS. Return REFUSED for all such requests.
73 NO_OLD_DNS_EVER: 3
74
75 # 3. create global functions, and then return the entire globals object
761 _.assign dnschain.globals, {
77 gExternalIP: do ->
781 cachedIP = null
791 faces = os.networkInterfaces()
801 default_iface = switch
811 when os.type() is 'Darwin' and faces.en0? then 'en0'
820 when os.type() is 'Linux' and faces.eth0? then 'eth0'
830 else _(faces).keys().find (x)-> !x.match /lo/
841 (iface=default_iface, cached=true,fam='IPv4',internal=false) ->
857 cachedIP = switch
866 when cached and cachedIP then cachedIP
87 else
881 unless ips = (faces = os.networkInterfaces())[iface]
890 throw new Error util.format("No such interface '%s'. Available: %j", iface, faces)
901 if (address = _.find(ips, {family:fam, internal:internal})?.address)
911 address
92 else
930 console.warn "Couldn't find 'address' in:".bold.red, ips
940 console.warn "Couldn't figure out external IPv4 IP! Make SURE to manually set it in your configuration!".bold.red
950 "NOT AN IP! SEE: https://github.com/okTurtles/dnschain/issues/111#issuecomment-71958236"
96
97 gNewLogger: (name) ->
9861 new winston.Logger
99 levels: winston.config.cli.levels
100 colors: winston.config.cli.lcolors
101 transports: [
102 new winston.transports.Console
103 label:name
104 level: gConf.get 'log:level'
105 colorize: gConf.get 'log:colors'
106 prettyPrint: gConf.get 'log:pretty'
107 timestamp: gConf.get 'log:timestamp'
108 ]
109
110 gErr: (args...) ->
1111 e = new Error util.format args...
1121 gLogger.error e.stack
1131 throw e
114
115 gFillWithRunningChecks: (server) ->
11655 gLineInfo = dnschain.globals.gLineInfo
11755 server.startCheck = (cb) ->
11855 if @running
1190 @log.warn gLineInfo "Already running!"
1200 Promise.reject()
121 else
12255 @log.debug "Starting up..."
12355 new Promise (resolve, reject) =>
12455 cb (err, args...) =>
12555 if err
1260 @log.error gLineInfo("failed to start"), err
1270 reject err
128 else
12955 @log.info "Server started.", args
13055 @running = true
13155 resolve args
13255 server.shutdownCheck = (cb) ->
13355 if @running
13455 @log.debug "Shutting down..."
13555 new Promise (resolve, reject) =>
13655 cb (err, args...) =>
13755 if err
1380 @log.error gLineInfo("failed to shutdown"), err
1390 reject err
140 else
14155 @log.info "Server shutdown successfully.", args
14255 @running = false
14355 resolve args
144 else
1450 @log.warn gLineInfo "Shutdown called when not running!"
1460 Promise.reject()
14755 server # as a convenience, return the server instance
148
149131 gThrottle: (key, makeLimiter) -> limiters[key] ? (limiters[key] = makeLimiter())
150
151 # TODO: this function should take one parameter: an IP string
152 # and return either 'A' or 'AAAA'
153 # A separate function called type2rec should do what the inner fn does
154 # https://github.com/okTurtles/dnschain/issues/7
155 gIP2type: (d, ttl, type='A') ->
1567 (ip)-> dns2[type] {name:d, address:ip, ttl:ttl}
157
158 # Currently this function is namecoin-specific.
159 # DANE/TLSA info: https://tools.ietf.org/html/rfc6698
160 # TODO: implement RRSIG from http://tools.ietf.org/html/rfc4034
161 # as in: dig @8.8.4.4 +dnssec TLSA _443._tcp.good.dane.verisignlabs.com
162 gTls2tlsa: (tls, ttl, queryname) ->
1630 [port, protocol] = (queryname.split '.')[0..1].map (o)-> o.replace '_',''
1640 if !tls?[protocol]?[port]?
1650 return []
1660 tls[protocol][port].map (certinfo) ->
1670 dns2.TLSA
168 name: queryname
169 ttl: ttl
170 usage: 3 # 3 = "Fuck CAs" :P
171 selector: 1 # SubjectPublicKeyInfo: DER-encoded [RFC5280]
172 matchingtype: certinfo[0]
173 buff: new Buffer certinfo[1], 'hex'
174
175 gLineInfo: (prefix='') ->
176625 stack = new Error().stack
177 # console.log stack.split('\n')[2]
178625 [file, line] = stack.split('\n')[2].split ':'
179625 [func, file] = file.split ' ('
180625 [func, file] = ['??', func] unless file # sometimes the function isn't specified
181625 [func, file] = [func.split(' ').pop(), path.basename(file)]
182625 [junk, func] = func.split('.')
183625 func = junk unless func
184625 func = if func is '??' or func is '<anonymous>' then ' (' else " (<#{func}> "
185625 prefix + func + file + ':' + line + ')'
186 }
187
188 # 4. vars for use within the map above and elsewhere
1891 gConf = dnschain.globals.gConf = require('./config')(dnschain)
1901 gLogger = dnschain.globals.gLogger = dnschain.globals.gNewLogger 'Global'
1911 gConsts = dnschain.globals.gConsts
192
193 # handle DEPRECATED numerical oldDNSMethod values
1941 method = gConf.get 'dns:oldDNSMethod'
1951 if typeof method isnt 'string'
1960 method = _.keys(_.pick gConsts.oldDNS, (v,k)-> v is method)[0]
1970 gLogger.warn "Specifying 'oldDNSMethod' as a number is DEPRECATED!".bold.red
1980 gLogger.warn "Please specify the string value instead:".bold.red, "#{method}".bold
199 else
2001 if _.isNumber (method_num = gConsts.oldDNS[method])
201 # kinda hackish... but makes for easy and quick comparisons
2021 gConf.set 'dns:oldDNSMethod', method_num
203 else
2040 gLogger.error "No such oldDNS method:".bold.red, method.bold
2050 process.exit 1
206
207
208 # 5. return the globals object
2091 dnschain.globals
210

src/lib/http.coffee

86%
74
64
10
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141express = require 'express'
15
161module.exports = (dnschain) ->
17 # expose these into our namespace
181 for k of dnschain.globals
1928 eval "var #{k} = dnschain.globals.#{k};"
20
211 class HTTPServer
221 constructor: (@dnschain) ->
236 @log = gNewLogger 'HTTP'
246 @log.debug "Loading HTTPServer..."
256 @rateLimiting = gConf.get 'rateLimiting:http'
266 app = express()
27
28 # Openname spec defined here:
29 # - https://github.com/okTurtles/openname-specifications/blob/resolvers/resolvers.md
30 # - https://github.com/openname/openname-specifications/blob/master/resolvers.md
31
326 opennameRoute = express.Router()
33
34 # Resolver specific API
356 opennameRoute.get /\/(?:resolver|dnschain)\/([^\/\.]+)(?:\.([a-z]+))?/, (req, res) =>
361 @log.debug gLineInfo("resolver API called"), {params: req.params}
371 [resource, format] = req.params
381 if resource == "fingerprint"
391 if !format or format is 'json'
401 res.json {fingerprint: @dnschain.encryptedserver.getFingerprint()}
41 else
420 @sendErr req, res, 400, "Unsupported format: #{format}"
43 else
440 @sendErr req, res, 400, "Bad resource: #{resource}"
45
46 # Datastore API
47 # Note: JavaScript doesn't have negative lookbehind support
48 # so we use the negative lookahead hacks mentioned here:
49 # https://stackoverflow.com/questions/977251/regular-expressions-and-negating-a-whole-character-group
506 opennameRoute.route(/// ^
51 \/(\w+) # the datastore name
52 \/(\w+) # the corresponding resource
53 (?:\/((?:(?!\.json|\.xml)[^\/])+))? # optional property (or action on resource)
54 (?:\/((?:(?!\.json|\.xml)[^\/])+))? # optional action on property
55 (?:\.(json|xml))? # optional response format
56 $ ///
57 ).get (req, res) =>
586 @log.debug gLineInfo("get v1"), {params: req.params, queryArgs: req.query}
596 @callback req, res, [_.values(req.params)..., req.query]
60
616 opennameRoute.use (req, res) =>
620 @sendErr req, res, 400, "Bad v1 request"
63
646 app.use "/v1", opennameRoute
656 app.get "*", (req, res) => # Old, deprecated API usage.
662 path = S(url.parse(req.originalUrl).pathname).chompLeft('/').s
672 options = url.parse(req.originalUrl, true).query
682 @log.debug gLineInfo('deprecated request'), {path:path, options:options, url:req.originalUrl}
69
702 [...,datastoreName] =
71 if S(header = req.headers.blockchain || req.headers.host).endsWith('.dns')
722 S(header).chompRight('.dns').s.split('.')
73 else
740 ['none']
75
762 @callback req, res, [datastoreName, "key", path, null, null, options]
77
786 app.use (err, req, res, next) =>
790 @log.warn gLineInfo('error handler triggered'),
80 errMessage: err?.message
81 stack: err?.stack
82 req: _.at(req, ['originalUrl','ip','ips','protocol','hostname','headers'])
830 res.status(500).send "Internal Error: #{err?.message}"
84
856 @server = http.createServer (req, res) =>
869 key = "http-#{req.connection?.remoteAddress}"
879 @log.debug gLineInfo("creating bottleneck on: #{key}")
889 limiter = gThrottle key, => new Bottleneck _.at(@rateLimiting, ['maxConcurrent','minTime','highWater','strategy'])...
89
90 # Since Express doesn't take a callback function
91 # we capture the callback that Bottleneck requires
92 # in `bottleCB` and call it by hooking into `res.end`
939 savedEnd = res.end.bind(res)
949 bottleCB = null
959 res.end = (args...) =>
969 savedEnd args...
979 bottleCB()
98
999 limiter.submit (cb) ->
1009 bottleCB = cb
1019 app req, res
102 , null
103
1046 gErr("http create") unless @server
105
1066 @server.on 'error', (err) -> gErr err
1076 gFillWithRunningChecks @
108
109 start: ->
1106 @startCheck (cb) =>
1116 @server.listen gConf.get('http:port'), gConf.get('http:host'), =>
1126 cb null, gConf.get 'http'
113
114 shutdown: ->
1156 @shutdownCheck (cb) =>
1166 @log.debug 'shutting down!'
1176 @server.close cb
118
119 callback: (req, res, args) ->
1208 [datastoreName, resourceName, propOrAction] = args
1218 if not (datastore = @dnschain.chains[datastoreName])
1220 return @sendErr req, res, 400, "Unsupported datastore: #{datastoreName}"
1238 if not (resourceFn = datastore.resources[resourceName])
1241 return @sendErr req, res, 400, "Unsupported resource: #{resourceName}"
1257 resourceRequest = (cb) =>
1267 resourceFn.call datastore, args[2..]..., cb
1277 @dnschain.cache.resolveResource datastore, resourceRequest, JSON.stringify(args), (err,result) =>
1287 if err
1290 err.httpCode ?= 404
1300 @log.debug gLineInfo('resolver failed'), {err:err.message}
1310 @sendErr req, res, err.httpCode, "Not Found: #{propOrAction}"
132 else
1337 @log.debug gLineInfo('postResolve'), {path:propOrAction, result:result}
1347 res.json result
135
136 sendErr: (req, res, code=404, comment="Not Found") ->
1371 if req.originalUrl != '/favicon.ico' # avoid logging these
1381 @log.warn gLineInfo('sendErr'),
139 comment: comment
140 code: code
141 req: _.at(req, ['originalUrl','protocol','hostname'])
1421 res.status(code).send comment
143

src/lib/https.coffee

77%
106
82
24
LineHitsSource
1###
2
3dnschain
4http://dnschain.org
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14###
15This file contains the logic to handle connections on port 443
16These connections can be naked HTTPS or wrapped inside of TLS
17###
18
19###
20NOTE: There can be any number of EncryptedServers. A good example of that is when running the tests.
21The TLSServers are shared between EncryptedServers.
22
23 __________________________ ________________________
24443 traffic | | *--->| TLSServer | ______________
25----------->| EncryptedServer |--* | (Dumb decrypter) |---->| HTTPServer |----> Multiple destinations
26 |(Categorization/Routing)| * | (One of many) | ______________
27 __________________________ * | (Unique destination) |
28 * _______________________|
29 * _____________ Soon
30 *-->| TLSServer |----------> Unblock (Vastly simplified)
31 _____________
32###
33
341module.exports = (dnschain) ->
35 # expose these into our namespace
363 for k of dnschain.globals
3784 eval "var #{k} = dnschain.globals.#{k};"
38
393 libHTTPS = new ((require "./httpsUtils")(dnschain)) # TODO: httpsUtils doesn't need to be a class
403 pem = (require './pem')(dnschain)
413 httpSettings = gConf.get "http"
423 unblockSettings = gConf.get "unblock"
433 tlsLog = gNewLogger "TLSServer"
44
453 httpsVars = tls: Promise.resolve()
46
473 keyMaterial = _(httpSettings).pick(['tlsKey', 'tlsCert']).transform((o, v, k)->
486 o[k] = { key:k, path:v, exists: fs.existsSync(v) }
49 ).value()
50
51 # Auto-generate public/private key pair if they don't exist
523 if _.some(keyMaterial, exists:false)
532 missing = _.find(keyMaterial, exists:false)
542 tlsLog.warn "File for http:#{missing.key} does not exist: #{missing.path}".bold.red
552 tlsLog.warn "Vist this link for information on how to generate this file:".bold
562 tlsLog.warn "https://github.com/okTurtles/dnschain/blob/master/docs/How-do-I-run-my-own.md#getting-started".bold
57
58 # In the case where one file exists but the other does not
59 # we do not auto-generate them for the user (so as to not overwrite anything)
602 if exists = _.find(keyMaterial, exists:true)
611 tlsLog.error "\nhttp:#{exists.key} exists at:\n\t".bold.yellow, exists.path.bold, "\nbut http:#{missing.key} does not exist at:\n\t".bold.red, missing.path.bold
621 gErr "Missing file for http:#{missing.key}"
63
641 tlsLog.warn "Auto-generating private key and certificate for you...".bold.yellow
65
661 {tlsKey, tlsCert} = gConf.chains.dnschain.stores.defaults.get('http')
671 unless httpSettings.tlsKey is tlsKey and httpSettings.tlsCert is tlsCert
681 msg = "Can't autogen keys for you because you've customized their paths"
691 if process.env.TEST_DNSCHAIN
701 tlsLog.warn "Test detected. Not throwing error:".bold, msg.bold.yellow
71 else
720 gErr msg
731 [tlsKey, tlsCert] = [httpSettings.tlsKey, httpSettings.tlsCert]
741 httpsVars.tls = pem.genKeyCertPair(tlsKey, tlsCert).then ->
751 tlsLog.info "Successfully autogenerated", {key:tlsKey, cert:tlsCert}
76
77 # Fetch the public key fingerprint of the cert we're using and log to console
782 httpsVars.tls = httpsVars.tls.then ->
792 httpsVars.tlsOptions =
80 key: fs.readFileSync httpSettings.tlsKey
81 cert: fs.readFileSync httpSettings.tlsCert
82
832 pem.certFingerprint(httpSettings.tlsCert).then (f) ->
842 httpsVars.fingerprint = f
852 tlsLog.info "Your certificate fingerprint is:", f.bold
86
872 class EncryptedServer
882 constructor: (@dnschain) ->
897 @log = gNewLogger "HTTPS"
907 @log.debug gLineInfo "Loading HTTPS..."
917 @rateLimiting = gConf.get 'rateLimiting:https'
92
937 @server = net.createServer (c) =>
942 key = "https-#{c.remoteAddress}"
952 limiter = gThrottle key, => new Bottleneck @rateLimiting.maxConcurrent, @rateLimiting.minTime, @rateLimiting.highWater, @rateLimiting.strategy
962 limiter.submit (@callback.bind @), c, null
977 @server.on "error", (err) -> gErr err
987 @server.on "close", => @log.info "HTTPS server received close event."
997 gFillWithRunningChecks @
100
101 start: ->
1027 @startCheck (cb) =>
1037 listen = =>
1047 @server.listen httpSettings.tlsPort, httpSettings.host, =>
1057 cb null, httpSettings
106
1077 if httpsVars.tls.then
1087 httpsVars.tls.then =>
1097 httpsVars.tls = tls.createServer httpsVars.tlsOptions, (c) =>
1102 libHTTPS.getStream "127.0.0.1", httpSettings.port, (err, stream) ->
1112 if err?
1120 tlsLog.error gLineInfo "Tunnel failed: Could not connect to HTTP Server"
1130 c?.destroy()
1140 return stream?.destroy()
1152 c.pipe(stream).pipe(c)
1167 httpsVars.tls.on "error", (err) ->
1170 tlsLog.error gLineInfo(), err
1180 gErr err.message
1197 httpsVars.tls.listen httpSettings.internalTLSPort, "127.0.0.1", ->
1207 tlsLog.info "Listening"
1217 listen()
122 else
1230 listen()
124
125 shutdown: ->
1267 @shutdownCheck (cb) =>
1277 httpsVars.tls.close() # node docs don't indicate this takes a callback
1287 httpsVars.tls = Promise.resolve() # TODO: Simon, this hack is necessary
129 # because without it test/https.coffee
130 # breaks if it is not run first.
131 # This happens because of the
132 # `if httpsVars.tls.then` above
133 # in `start:`.
1347 @server.close cb
135
136 callback: (c, cb) ->
1372 libHTTPS.getClientHello c, (err, category, host, buf) =>
1382 @log.debug err, category, host, buf?.length
1392 if err?
1400 @log.debug gLineInfo "TCP handling: "+err.message
1410 cb()
1420 return c?.destroy()
143
144 # UNBLOCK: Check if needs to be hijacked
145
1462 isRouted = false # unblockSettings.enabled and unblockSettings.routeDomains[host]?
1472 isDNSChain = (
1482 (category == libHTTPS.categories.NO_SNI) or
1490 ((not unblockSettings.enabled) and category == libHTTPS.categories.SNI) or
1500 (unblockSettings.enabled and (host in unblockSettings.acceptApiCallsTo)) or
1510 ((host?.split(".")[-1..][0]) == "dns")
152 )
1532 isUnblock = false
154
1552 [destination, port, error] = if isRouted
1560 ["127.0.0.1", unblockSettings.routeDomains[host], false]
1572 else if isDNSChain
1582 ["127.0.0.1", httpSettings.internalTLSPort, false]
1590 else if isUnblock
1600 [host, 443, false]
161 else
1620 ["", -1, true]
163
1642 if error
1650 @log.error "Illegal domain (#{host})"
1660 cb()
1670 return c?.destroy()
168
1692 libHTTPS.getStream destination, port, (err, stream) =>
1702 if err?
1710 @log.error gLineInfo "Tunnel failed: Could not connect to internal TLS Server"
1720 c?.destroy()
1730 cb()
1740 return stream?.destroy()
1752 stream.write buf
1762 c.pipe(stream).pipe(c)
1772 c.resume()
1782 @log.debug gLineInfo "Tunnel: #{host}"
1792 cb()
180
1811 getFingerprint: -> httpsVars.fingerprint
182

src/lib/httpsUtils.coffee

76%
88
67
21
LineHitsSource
1###
2
3dnschain
4http://dnschain.org
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
131module.exports = (dnschain) ->
14 # expose these into our namespace
153 for k of dnschain.globals
1684 eval "var #{k} = dnschain.globals.#{k};"
17
18 # http://tools.ietf.org/html/rfc5246#section-7.4.1
19 # http://stackoverflow.com/questions/17832592/extract-server-name-indication-sni-from-tls-client-hello
203 class HTTPSUtils
213 categories: {
22 SNI : 0
23 NO_SNI : 1
24 NOT_HTTPS : 2
25 INCOMPLETE : 3
26 }
27
28 parseHTTPS: (packet) ->
292 res = {}
302 try
312 res.contentType = packet.readUInt8 0
322 if res.contentType != 22 then return [@categories.NOT_HTTPS, {}]
33
342 res.recordVersionMajor = packet.readUInt8 1
352 if res.recordVersionMajor >= 7 then return [@categories.NOT_HTTPS, {}]
36
372 res.recordVersionMinor = packet.readUInt8 2
382 if res.recordVersionMinor >= 7 then return [@categories.NOT_HTTPS, {}]
39
402 res.recordLength = packet.readUInt16BE 3
41
422 res.handshakeType = packet.readUInt8 5
432 if res.handshakeType != 1 then return [@categories.NOT_HTTPS, {}]
44
452 res.handshakeLength = packet[6..8]
462 res.handshakeVersion = packet.readUInt16BE 9
472 res.random = packet[11..42]
48
492 res.sessionIDlength = packet.readUInt8 43
502 pos = res.sessionIDlength + 43 + 1
51
522 res.cipherSuitesLength = packet.readUInt16BE pos
532 pos += res.cipherSuitesLength + 2
54
552 res.compressionMethodsLength = packet.readUInt8 pos
562 pos += res.compressionMethodsLength + 1
57
582 res.extensionsLength = packet.readUInt16BE pos
592 pos += 2
60
612 extensionsEnd = pos + res.extensionsLength - 1
622 jump = 0
63
642 res.extensions = {}
65
662 while pos < extensionsEnd
676 ext = {}
686 ext.type = packet.readUInt16BE pos
696 ext.length = packet.readUInt16BE (pos+2)
706 jump = ext.length+4
716 ext.body = packet[pos..(pos+jump-1)]
726 res.extensions[ext.type] = ext
736 pos += jump
74
752 if res.extensions["0"]?
760 sniPos = 0
770 sni = res.extensions["0"]
780 sni.sniType = sni.body.readUInt16BE 0
790 sni.sniLength = sni.body.readUInt16BE 2
800 sni.sniList = sni.body.readUInt16BE 4
810 sni.sniNameType = sni.body.readUInt8 6
820 sni.sniNameLength = sni.body.readUInt16BE 7
830 sni.sniName = sni.body[9..(9+sni.sniNameLength)]
840 res.host = sni.sniName.toString "utf8"
850 if res.host.length != sni.sniNameLength then return [@categories.NOT_HTTPS, {}]
860 return [@categories.SNI, res]
87 else
882 return [@categories.NO_SNI, {}]
89 catch ex
900 return [@categories.INCOMPLETE, {}]
91
92
93 # Open a TCP socket to a remote host.
94 getStream: (host, port, cb) ->
954 try
964 done = (err, s) ->
974 done = ->
984 cb err, s
994 s = net.createConnection {host, port}, ->
1004 done null, s
1014 s.on "error", (err) -> s.destroy(); done err
1024 s.on "close", -> s.destroy()
1034 s.on "timeout", -> s.destroy(); done new Error "getStream timed out"
104 catch err
1050 done err
106
107 # Received raw TCP data in chunked mode and attempt to extract Hello data
108 # after every chunk. Return as soon as the Hello data has been obtained.
109 getClientHello: (c, cb) ->
1102 received = []
1112 buf = new Buffer []
1122 done = (err, category, host, buf) ->
1132 c.removeAllListeners("data")
1142 done = ->
1152 cb err, category, host, buf
1162 c.on "data", (data) =>
1172 c.pause()
1182 received.push data
1192 buf = Buffer.concat received
120
1212 [category, parsed] = @parseHTTPS buf
1222 switch category
123 when @categories.SNI
1240 done null, category, parsed.host, buf
125 when @categories.NO_SNI
1262 done null, category, null, buf
127 when @categories.NOT_HTTPS
1280 done new Error "NOT HTTPS", category, null, buf
129 when @categories.INCOMPLETE
1300 c.resume()
131 else
1320 done new Error "Unimplemented", category, null, buf
1332 c.on "timeout", ->
1340 c.destroy()
1350 done new Error "HTTPS getClientHello timeout"
1362 c.on "error", (err) ->
1370 c.destroy()
1380 done err
1392 c.on "close", ->
1402 c.destroy()
1412 done new Error "HTTPS socket closed"

src/lib/pem.coffee

89%
19
17
2
LineHitsSource
1###
2
3dnschain
4http://dnschain.org
5
6Copyright (c) 2015 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141module.exports = (dnschain) ->
15 # expose these into our namespace
163 for k of dnschain.globals
1784 eval "var #{k} = dnschain.globals.#{k};"
18
193 execAsync = Promise.promisify require('child_process').exec
20
213 pem =
22 certFingerprint: (cert, opts={}) ->
232 opts = _.defaults opts, {timeout:1000, encoding:'utf8'}
242 cmd = "openssl x509 -fingerprint -sha256 -text -noout -in \"#{cert}\""
252 gLogger.debug "running: #{cmd}"
262 execAsync(cmd, opts).spread (stdout, stderr) ->
272 stdout.match(/SHA256 Fingerprint=([0-9A-F:]{95})$/m)[1]
28 .catch (err) ->
290 gErr "Failed to read public key fingerprint: #{err?.message}"
30
31 genKeyCertPair: (key, cert, opts={}) ->
321 opts = _.defaults opts, {timeout:10000, encoding:'utf8'}
331 cmd = """
34 openssl req -new -newkey rsa:4096 -days 730 -nodes -sha256 -x509 \
351 -subj "/CN=#{os.hostname()}" \
361 -keyout "#{key}" -out "#{cert}"
37 """
381 gLogger.debug "running: #{cmd}"
391 execAsync(cmd, opts).spread (stdout, stderr) ->
401 fs.chmodSync key, 0o600 # rw for owner, 0 for everyone else
41 .catch (err) ->
420 gErr "Failed to generate key material: #{err?.message}"
43

src/lib/resolver-stream.coffee

74%
81
60
21
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
141Transform = require('stream').Transform
15
16# objectMode is on by default
17
181module.exports = (dnschain) ->
1930 StackedScheduler = require('./stacked-scheduler')(dnschain)
20
21 # expose these into our namespace
2230 for k of dnschain.globals
23840 eval "var #{k} = dnschain.globals.#{k};"
24
2530 NAME_RCODE = dns2.consts.NAME_TO_RCODE
2630 RCODE_NAME = dns2.consts.RCODE_TO_NAME
27
2830 defaults =
29 name : 'RS'
30 stackedDelay: 0
31 resolver : gConf.get 'dns:oldDNS'
323 answerFilter: (a) -> a.address
33 reqMaker : (cname) ->
342 dns2.Request
35 question: dns2.Question {name:cname, type:'A'} # TODO: is type=A always correct?
36 server: @resolver
37
3830 class ResolverStream extends Transform
3930 constructor: (@opts) ->
402 @opts = _.cloneDeep @opts # clone these for safety in case outside code updates them
412 @opts.objectMode ?= true
422 defaultProps = _.keys defaults
43 # copy property values in @opts for those keys in 'defaults' into this object
442 _.assign @, _.pick(_.defaults(@opts, defaults), defaultProps)
452 super _.omit @opts, defaultProps
46
472 @scheduler = new StackedScheduler @opts
482 @requests = {}
492 @reqCounter = 0
502 @errCount = 0
512 @log = gNewLogger @name
52
53 cancelRequests: (andStop=false) ->
542 @log.debug 'cancelling requests%s', if andStop then ' and stopping' else ''
55
56 # cancell all active requests (those that were sent)
572 for id,req of @requests
580 @log.debug "cancelling req-#{req.rsReqID} (should have same id: #{id})"
590 req.cancel()
600 delete @requests[id]
61
62 # cancell all pending requests (those to be sent)
632 @scheduler.cancelAll()
642 @stopped = andStop
65
66 # 'callback' should only be called in req.on 'end'
67 _transform: (cname, encoding, callback) ->
684 if typeof cname != 'string'
690 gErr "cname isn't a string!", cname
70
714 if @stopped
721 @log.debug gLineInfo("stopped. not scheduling req for '#{cname}'")
731 return callback()
74
753 req = @reqMaker(cname)
763 req.rsReqID = @reqCounter++
773 q = req.question
783 answers = []
793 success = false
803 reqErr = undefined
81
823 reqErrFn = (code, msg...) =>
830 reqErr = new Error util.format msg...
840 reqErr.code = code
850 @log.debug gLineInfo('reqErrfn'), reqErr.message
860 @errCount += 1
87
883 req.on 'message', (err, answer) =>
893 if err
900 @log.error gLineInfo("should not have an error here!"), {err:err?.message, answer:answer, q:q}
910 reqErrFn NAME_RCODE.SERVFAIL, "msg err %j: %j", q, err.message ? err
92 else
933 @log.debug gLineInfo("req-#{req.rsReqID} message"), {resolved_q:q, to:answer.answer}
943 if answer.answer.length > 0
953 try
963 answers.push answer.answer...
973 answer.answer.forEach (a) => @push @answerFilter a
983 success = true
99 catch e
100 # See: https://github.com/okTurtles/dnschain/issues/43
1010 @log.warn gLineInfo("couldn't push answer"), {error: e.message, q:q, cname:cname, answer:answer}
1020 reqErrFn NAME_RCODE.SERVFAIL, "push after EOF for '%j'?: %j", cname, e.message
103 else
1040 reqErrFn NAME_RCODE.NOTFOUND, "cname not found: %j", cname
105
1063 req.on 'timeout', =>
1070 @log.debug gLineInfo("req-#{req.rsReqID} timeout"), {q:q}
108 # TODO: better rcode value? am not aware of a timeout value
109 # https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
1100 reqErrFn NAME_RCODE.SERVFAIL, "timeout for '%j': %j", cname, req
111
1123 req.on 'cancelled', =>
1130 @log.debug gLineInfo("req-#{req.rsReqID} cancelled"), {q:q}
1140 success = false
115
1163 req.on 'error', (err) =>
1170 @log.warn gLineInfo("req-#{req.rsReqID} error"), {q:q, err:err?.message}
1180 reqErrFn NAME_RCODE.SERVFAIL, "error for '%j': %j", cname, err
119
1203 req.on 'end', =>
1213 @log.debug gLineInfo("req-#{req.rsReqID} end"), {q:q, answers:answers, success:success}
1223 delete @requests[req.rsReqID]
123 # it is possible that neither 'success' is true
124 # nor is 'reqError' defined. This can happen
125 # when 'cancelRequests' is called on us.
1263 if reqErr
1270 @log.debug gLineInfo("req-#{req.rsReqID} failed"), {err:reqErr.message, q:q}
128 # we emit 'failed' instead of 'error' so that we can continue
129 # processing other `cname`s in the pipeline.
1300 @emit 'failed', reqErr
1313 else if success
1323 @emit 'answers', answers
133
134 # we always call `callback` without an error value for the same reason that
135 # that we emit 'failed' instead of 'error' (to continue processing)
1363 callback()
137
1383 @log.debug gLineInfo("scheduling req-#{req.rsReqID}"), {q:q, cname:cname}
139
1403 @scheduler.schedule =>
141 # add it to the active requests when it's actually sent
1423 @requests[req.rsReqID] = req
1433 @log.debug gLineInfo("sending req-#{req.rsReqID}"), {q:q}
1443 req.send()
145

src/lib/stacked-scheduler.coffee

84%
19
16
3
LineHitsSource
1###
2
3dnschain
4http://dnschain.net
5
6Copyright (c) 2014 okTurtles Foundation
7
8This Source Code Form is subject to the terms of the Mozilla Public
9License, v. 2.0. If a copy of the MPL was not distributed with this
10file, You can obtain one at http://mozilla.org/MPL/2.0/.
11
12###
13
14# TODO: go through 'TODO's!
15
161module.exports = (dnschain) ->
17 # expose these into our namespace
1830 for k of dnschain.globals
19840 eval "var #{k} = dnschain.globals.#{k};"
20
2130 class StackedScheduler
2230 constructor: ({@stackedDelay}) ->
232 @stackedDelay ?= 2000 # 2 seconds by default
242 @tasks = {}
252 @nextRunTime = Date.now()
262 @taskCounter = 0
27
28 cancelAll: (runCallback=false)->
292 for key, task of @tasks
300 clearTimeout(task.tid)
310 task.callback() if runCallback
320 delete @tasks[key]
33
34 schedule: (callback) ->
353 diffMillis = @nextRunTime - Date.now()
363 @nextRunTime += @stackedDelay - Math.min(diffMillis, 0)
373 nonce = @taskCounter++
383 @tasks[nonce] =
39 callback: callback # for 'cancelAll'
40 tid: setTimeout =>
413 delete @tasks[nonce]
423 callback()
43 , Math.max diffMillis, 0
44