This repository has been archived by the owner on May 15, 2020. It is now read-only.
/
configuration.coffee
245 lines (214 loc) · 9.45 KB
/
configuration.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# The `Configuration` class encapsulates various options for a Pow
# daemon (port numbers, directories, etc.). It's also responsible for
# creating `Logger` instances and mapping hostnames to application
# root paths.
fs = require "fs"
path = require "path"
async = require "async"
Logger = require "./logger"
{mkdirp} = require "./util"
{sourceScriptEnv} = require "./util"
{getUserEnv} = require "./util"
module.exports = class Configuration
# The user configuration file, `~/.powconfig`, is evaluated on
# boot. You can configure options such as the top-level domain,
# number of workers, the worker idle timeout, and listening ports.
#
# export POW_DOMAINS=dev,test
# export POW_WORKERS=3
#
# See the `Configuration` constructor for a complete list of
# environment options.
@userConfigurationPath: path.join process.env.HOME, ".powconfig"
# Evaluates the user configuration script and calls the `callback`
# with the environment variables if the config file exists. Any
# script errors are passed along in the first argument. (No error
# occurs if the file does not exist.)
@loadUserConfigurationEnvironment: (callback) ->
getUserEnv (err, env) =>
if err
callback err
else
path.exists p = @userConfigurationPath, (exists) ->
if exists
sourceScriptEnv p, env, callback
else
callback null, env
# Creates a Configuration object after evaluating the user
# configuration file. Any environment variables in `~/.powconfig`
# affect the process environment and will be copied to spawned
# subprocesses.
@getUserConfiguration: (callback) ->
@loadUserConfigurationEnvironment (err, env) ->
if err
callback err
else
callback null, new Configuration env
# A list of option names accessible on `Configuration` instances.
@optionNames: [
"bin", "dstPort", "httpPort", "dnsPort", "timeout", "workers",
"domains", "extDomains", "hostRoot", "logRoot", "rvmPath"
]
# Pass in any environment variables you'd like to override when
# creating a `Configuration` instance.
constructor: (env = process.env) ->
@loggers = {}
@initialize env
# Valid environment variables and their defaults:
initialize: (@env) ->
# `POW_BIN`: the path to the `pow` binary. (This should be
# correctly configured for you.)
@bin = env.POW_BIN ? path.join __dirname, "../bin/pow"
# `POW_DST_PORT`: the public port Pow expects to be forwarded or
# otherwise proxied for incoming HTTP requests. Defaults to `80`.
@dstPort = env.POW_DST_PORT ? 80
# `POW_HTTP_PORT`: the TCP port Pow opens for accepting incoming
# HTTP requests. Defaults to `20559`.
@httpPort = env.POW_HTTP_PORT ? 20559
# `POW_DNS_PORT`: the UDP port Pow listens on for incoming DNS
# queries. Defaults to `20560`.
@dnsPort = env.POW_DNS_PORT ? 20560
# `POW_TIMEOUT`: how long (in seconds) to leave inactive Rack
# applications running before they're killed. Defaults to 15
# minutes (900 seconds).
@timeout = env.POW_TIMEOUT ? 15 * 60
# `POW_WORKERS`: the maximum number of worker processes to spawn
# for any given application. Defaults to `2`.
@workers = env.POW_WORKERS ? 2
# `POW_DOMAINS`: the top-level domains for which Pow will respond
# to DNS `A` queries with `127.0.0.1`. Defaults to `dev`. If you
# configure this in your `~/.powconfig` you will need to re-run
# `sudo pow --install-system` to make `/etc/resolver` aware of
# the new TLDs.
@domains = env.POW_DOMAINS ? env.POW_DOMAIN ? "dev"
# `POW_EXT_DOMAINS`: additional top-level domains for which Pow
# will serve HTTP requests (but not DNS requests -- hence the
# "ext").
@extDomains = env.POW_EXT_DOMAINS ? []
# Allow for comma-separated domain lists, e.g. `POW_DOMAINS=dev,test`
@domains = @domains.split?(",") ? @domains
@extDomains = @extDomains.split?(",") ? @extDomains
@allDomains = @domains.concat @extDomains
# Support *.xip.io top-level domains.
@allDomains.push /\d+\.\d+\.\d+\.\d+\.xip\.io$/, /[0-9a-z]{1,7}\.xip\.io$/
# `POW_HOST_ROOT`: path to the directory containing symlinks to
# applications that will be served by Pow. Defaults to
# `~/Library/Application Support/Pow/Hosts`.
@hostRoot = env.POW_HOST_ROOT ? libraryPath "Application Support", "Pow", "Hosts"
# `POW_LOG_ROOT`: path to the directory that Pow will use to store
# its log files. Defaults to `~/Library/Logs/Pow`.
@logRoot = env.POW_LOG_ROOT ? libraryPath "Logs", "Pow"
# `POW_RVM_PATH`: path to the rvm initialization script. Defaults
# to `~/.rvm/scripts/rvm`.
@rvmPath = env.POW_RVM_PATH ? path.join process.env.HOME, ".rvm/scripts/rvm"
# ---
# Precompile regular expressions for matching domain names to be
# served by the DNS server and hosts to be served by the HTTP
# server.
@dnsDomainPattern = compilePattern @domains
@httpDomainPattern = compilePattern @allDomains
# Gets an object of the `Configuration` instance's options that can
# be passed to `JSON.stringify`.
toJSON: ->
result = {}
result[key] = @[key] for key in @constructor.optionNames
result
# Retrieve a `Logger` instance with the given `name`.
getLogger: (name) ->
@loggers[name] ||= new Logger path.join @logRoot, name + ".log"
# Search `hostRoot` for files, symlinks or directories matching the
# domain specified by `host`. If a match is found, the matching domain
# name and its configuration are passed as second and third arguments
# to `callback`. The configuration will either have a `root` or
# a `port` property. If no match is found, `callback` is called
# without any arguments. If an error is raised, `callback` is called
# with the error as its first argument.
findHostConfiguration: (host = "", callback) ->
@gatherHostConfigurations (err, hosts) =>
return callback err if err
for domain in @allDomains
for file in getFilenamesForHost host, domain
if config = hosts[file]
return callback null, domain, config
if config = hosts["default"]
return callback null, @allDomains[0], config
callback null
# Asynchronously build a mapping of entries in `hostRoot` to
# application root paths and proxy ports. For each symlink, store the
# symlink's name and the real path of the application it points to.
# For each directory, store the directory's name and its full path.
# For each file that contains a port number, store the file's name and
# the port. The mapping is passed as an object to the second argument
# of `callback`. If an error is raised, `callback` is called with the
# error as its first argument.
#
# The mapping object will look something like this:
#
# {
# "basecamp": { "root": "/Volumes/37signals/basecamp" },
# "launchpad": { "root": "/Volumes/37signals/launchpad" },
# "37img": { "root": "/Volumes/37signals/portfolio" },
# "couchdb": { "url": "http://localhost:5984" }
# }
gatherHostConfigurations: (callback) ->
hosts = {}
mkdirp @hostRoot, (err) =>
return callback err if err
fs.readdir @hostRoot, (err, files) =>
return callback err if err
async.forEach files, (file, next) =>
root = path.join @hostRoot, file
name = file.toLowerCase()
rstat root, (err, stats, path) ->
if stats?.isDirectory()
hosts[name] = root: path
next()
else if stats?.isFile()
fs.readFile path, 'utf-8', (err, data) ->
return next() if err
data = data.trim()
if data.length < 10 and not isNaN(parseInt(data))
hosts[name] = {url: "http://localhost:#{parseInt(data)}"}
else if data.match("https?://")
hosts[name] = {url: data}
next()
else
next()
, (err) ->
callback err, hosts
# Convenience wrapper for constructing paths to subdirectories of
# `~/Library`.
libraryPath = (args...) ->
path.join process.env.HOME, "Library", args...
# Strip a trailing `domain` from the given `host`, then generate a
# sorted array of possible entry names for finding which application
# should serve the host. For example, a `host` of
# `asset0.37s.basecamp.dev` will produce `["asset0.37s.basecamp",
# "37s.basecamp", "basecamp"]`, and `basecamp.dev` will produce
# `["basecamp"]`.
getFilenamesForHost = (host, domain) ->
host = host.toLowerCase()
domain = host.match(domain)?[0] ? "" if domain.test?
if host.slice(-domain.length - 1) is ".#{domain}"
parts = host.slice(0, -domain.length - 1).split "."
length = parts.length
for i in [0...length]
parts.slice(i, length).join "."
else
[]
# Similar to `fs.stat`, but passes the realpath of the file as the
# third argument to the callback.
rstat = (path, callback) ->
fs.lstat path, (err, stats) ->
if err
callback err
else if stats?.isSymbolicLink()
fs.realpath path, (err, realpath) ->
if err then callback err
else rstat realpath, callback
else
callback err, stats, path
# Helper function for compiling a list of top-level domains into a
# regular expression for matching purposes.
compilePattern = (domains) ->
/// ( (^|\.) (#{domains.join("|")}) ) \.? $ ///i