potluck-nginx 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -0
- data/lib/potluck/nginx/nginx_config.rb +388 -0
- data/lib/potluck/nginx/version.rb +7 -0
- data/lib/potluck/nginx.rb +485 -286
- metadata +30 -25
- data/lib/potluck/nginx/ssl.rb +0 -134
- data/lib/potluck/nginx/util.rb +0 -55
data/lib/potluck/nginx.rb
CHANGED
@@ -2,24 +2,40 @@
|
|
2
2
|
|
3
3
|
require('fileutils')
|
4
4
|
require('potluck')
|
5
|
-
require_relative('nginx/
|
6
|
-
require_relative('nginx/
|
5
|
+
require_relative('nginx/nginx_config')
|
6
|
+
require_relative('nginx/version')
|
7
7
|
|
8
8
|
module Potluck
|
9
|
-
|
10
|
-
# A Ruby interface for configuring and controlling Nginx. Each instance of this class manages a separate
|
11
|
-
# Nginx configuration file, which is loaded and unloaded from the base Nginx configuration when #start and
|
12
|
-
# #stop are called, respectively. Any number of Ruby processes can thus each manage their own Nginx
|
13
|
-
# configuration and control whether or not it is active without interfering with any other instances or
|
14
|
-
# non-Ruby processes leveraging Nginx.
|
9
|
+
# Public: A Ruby interface for configuring and controlling Nginx.
|
15
10
|
#
|
11
|
+
# Each instance of this class manages a separate Nginx configuration file, which is loaded and unloaded
|
12
|
+
# from the base Nginx configuration when #start and #stop are called, respectively. Any number of Ruby
|
13
|
+
# processes can thus each manage their own Nginx configuration and control whether or not it is active
|
14
|
+
# without interfering with any other instances or non-Ruby processes leveraging Nginx.
|
15
|
+
#
|
16
|
+
# A standard set of Nginx config directives are generated automatically, and a flexible DSL can be used to
|
17
|
+
# modify and/or embellish them.
|
18
|
+
#
|
19
|
+
# Examples
|
20
|
+
#
|
21
|
+
# Nginx.new('hello.world', 1234) do |c|
|
22
|
+
# c.server do
|
23
|
+
# c.access_log('/path/to/access.log')
|
24
|
+
# c.gzip('off')
|
25
|
+
#
|
26
|
+
# c.location('/hello') do
|
27
|
+
# c.try_files('world.html')
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
16
31
|
class Nginx < Service
|
17
32
|
CONFIG_NAME_ACTIVE = 'nginx.conf'
|
18
33
|
CONFIG_NAME_INACTIVE = 'nginx-stopped.conf'
|
19
|
-
ACTIVE_CONFIG_PATTERN = File.join(
|
34
|
+
ACTIVE_CONFIG_PATTERN = File.join(Potluck.config.dir, '*', CONFIG_NAME_ACTIVE).freeze
|
20
35
|
|
21
|
-
TEST_CONFIG_REGEX = /nginx: configuration file (?<config>.+) test (failed|is successful)
|
22
|
-
INCLUDE_REGEX = /^ *include +#{Regexp.escape(ACTIVE_CONFIG_PATTERN)}
|
36
|
+
TEST_CONFIG_REGEX = /nginx: configuration file (?<config>.+) test (?:failed|is successful)/
|
37
|
+
INCLUDE_REGEX = /^ *include +#{Regexp.escape(ACTIVE_CONFIG_PATTERN)} *;/
|
38
|
+
HTTP_BLOCK_REGEX = /^( *http *{)( *\n?)( *)/
|
23
39
|
|
24
40
|
NON_LAUNCHCTL_COMMANDS = {
|
25
41
|
status: 'ps aux | grep \'[n]ginx: master process\'',
|
@@ -27,76 +43,148 @@ module Potluck
|
|
27
43
|
stop: 'nginx -s stop',
|
28
44
|
}.freeze
|
29
45
|
|
30
|
-
|
31
|
-
|
46
|
+
DEFAULT_HTTP_PORT = 8080
|
47
|
+
DEFAULT_HTTPS_PORT = 4433
|
48
|
+
|
49
|
+
SSL_CERT_DAYS = 365
|
50
|
+
SSL_CERT_RENEW_DAYS = 14
|
51
|
+
|
52
|
+
# Public: Get the content of the launchctl plist file.
|
53
|
+
#
|
54
|
+
# Returns the String content.
|
55
|
+
def self.plist
|
56
|
+
super(
|
57
|
+
<<~XML
|
58
|
+
<key>ProgramArguments</key>
|
59
|
+
<array>
|
60
|
+
<string>#{Potluck.config.homebrew_prefix}/opt/nginx/bin/nginx</string>
|
61
|
+
<string>-g</string>
|
62
|
+
<string>daemon off;</string>
|
63
|
+
</array>
|
64
|
+
<key>StandardOutPath</key>
|
65
|
+
<string>#{Potluck.config.homebrew_prefix}/var/log/nginx/access.log</string>
|
66
|
+
<key>StandardErrorPath</key>
|
67
|
+
<string>#{Potluck.config.homebrew_prefix}/var/log/nginx/error.log</string>
|
68
|
+
XML
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Internal: Print a warning with the file and line number of a deprecated call.
|
32
73
|
#
|
33
|
-
#
|
34
|
-
# * +port+ - Port that the upstream (Ruby web server) is running on.
|
35
|
-
# * +subdomains+ - One or more subdomains (optional).
|
36
|
-
# * +ssl+ - SSL configuration arguments to pass to SSL.new (optional).
|
37
|
-
# * +one_host+ - True if URLs should be normalized to the first host in +hosts+ (optional, default:
|
38
|
-
# false).
|
39
|
-
# * +www+ - +true+ if URLs should be normalized to include 'www.' prefix, +false+ to exclude 'www.', and
|
40
|
-
# +nil+ if either is acceptable (optional, default: +nil+).
|
41
|
-
# * +multiple_slashes+ - +false+ if any occurrence of multiple slashes in the path portion of the URL
|
42
|
-
# should be normalized to a single slash (optional, default: +nil+).
|
43
|
-
# * +multiple_question_marks+ - +false+ if multiple question marks in the URL signifying the start of
|
44
|
-
# the query string should be normalized to a single question mark (optional, default: +nil+).
|
45
|
-
# * +trailing_slash+ - +true+ if URLs should be normalized to include a trailing slash at the end of the
|
46
|
-
# path portion, +false+ to strip any trailing slash, and +nil+ if either is acceptable (optional,
|
47
|
-
# default: +nil+).
|
48
|
-
# * +trailing_question_mark+ - +true+ if URLs should be normalized to include a trailing question mark
|
49
|
-
# when the query string is empty, +false+ to strip any trailing question mark, and +nil+ if either is
|
50
|
-
# acceptable (optional, default: +nil+).
|
51
|
-
# * +config+ - Nginx configuration hash; see #config (optional).
|
52
|
-
# * +ensure_host_entries+ - True if +hosts+ should be added to system /etc/hosts file as mappings to
|
53
|
-
# localhost (optional, default: false).
|
54
|
-
# * +args+ - Arguments to pass to Potluck::Service.new (optional).
|
74
|
+
# message - String deprecation message.
|
55
75
|
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
76
|
+
# Returns nothing.
|
77
|
+
def self.deprecated(message)
|
78
|
+
location = caller_locations(2, 1).first
|
79
|
+
|
80
|
+
warn("#{location.path}:#{location.lineno}: #{message}")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Public: Create a new instance.
|
84
|
+
#
|
85
|
+
# hosts - String or Array of String hosts.
|
86
|
+
# port - Integer port that the upstream (Ruby web server) is running on.
|
87
|
+
# subdomains: - String or Array of String fully qualified subdomains (e.g. 'sub.hello.com',
|
88
|
+
# not 'sub.').
|
89
|
+
# ssl: - Boolean indicating if SSL should be used. If true and SSL files are not
|
90
|
+
# configured, a self-signed certificate will be generated.
|
91
|
+
# one_host: - Boolean specifying if URLs should be normalized to the first item in hosts.
|
92
|
+
# www: - Boolean specifying if URLs should be normalized to include 'www.' (true),
|
93
|
+
# exclude it (false), or allow either (nil).
|
94
|
+
# multiple_slashes: - Boolean specifying if URLs should be normalized to reduce multiple
|
95
|
+
# slashes in a row to a single one (false) or leave them alone (true or nil).
|
96
|
+
# multiple_question_marks: - Boolean specifying if URLs should be normalized to reduce multiple question
|
97
|
+
# marks in a row to a single one (false) or leave them alone (true or nil).
|
98
|
+
# trailing_slash: - Boolean specifying if URLs should be normalized to include a trailing slash
|
99
|
+
# (true), exclude it (false), or allow either (nil).
|
100
|
+
# trailing_question_mark: - Boolean specifying if URLs should be normalized to include a trailing
|
101
|
+
# question mark (true), exclude it (false), or allow either (nil).
|
102
|
+
# config: - Deprecated: Nginx server block configuration Hash. Use a block instead.
|
103
|
+
# ensure_host_entries: - Boolean specifying if hosts should be added to system /etc/hosts file as
|
104
|
+
# mappings to localhost.
|
105
|
+
# kwargs - Hash of keyword arguments to pass to Service.new.
|
106
|
+
# block - Block for making modifications to the Nginx config. See NginxConfig#modify.
|
107
|
+
def initialize(hosts, port, subdomains: nil, ssl: false, one_host: false, www: nil,
|
108
|
+
multiple_slashes: nil, multiple_question_marks: nil, trailing_slash: nil,
|
109
|
+
trailing_question_mark: nil, config: nil, ensure_host_entries: false, **kwargs, &block)
|
110
|
+
if kwargs[:manage] && !kwargs[:manage].kind_of?(Hash) && !self.class.launchctl?
|
111
|
+
kwargs[:manage] = NON_LAUNCHCTL_COMMANDS
|
61
112
|
end
|
62
113
|
|
63
|
-
super(**
|
114
|
+
super(**kwargs)
|
64
115
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
@port = port
|
116
|
+
if config
|
117
|
+
self.class.deprecated("Passing config: {...} to #{self.class.name}.new is deprecated: use a " \
|
118
|
+
'block instead')
|
69
119
|
|
70
|
-
|
71
|
-
|
72
|
-
|
120
|
+
@deprecated_additional_config = config
|
121
|
+
end
|
122
|
+
|
123
|
+
if ssl.kind_of?(Hash)
|
124
|
+
self.class.deprecated("Passing ssl: {...} to #{self.class.name}.new is deprecated: pass ssl: " \
|
125
|
+
'true and use a block to configure SSL instead')
|
126
|
+
|
127
|
+
@deprecated_ssl_crt_file = ssl[:crt_file]
|
128
|
+
@deprecated_ssl_key_file = ssl[:key_file]
|
129
|
+
@deprecated_ssl_dhparam_file = ssl[:dhparam_file]
|
130
|
+
@deprecated_ssl_config = ssl[:config]
|
131
|
+
|
132
|
+
all_given = @deprecated_ssl_crt_file && @deprecated_ssl_key_file && @deprecated_ssl_dhparam_file
|
133
|
+
none_given = !@deprecated_ssl_crt_file && !@deprecated_ssl_key_file && !@deprecated_ssl_dhparam_file
|
73
134
|
|
74
|
-
|
75
|
-
|
76
|
-
|
135
|
+
unless all_given || none_given
|
136
|
+
raise(ArgumentError, 'Must supply values for all SSL files or none: crt_file, key_file, ' \
|
137
|
+
'dhparam_file')
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
@hosts = Array(hosts).map { |h| h.sub(/^www\./, '') }.uniq
|
77
142
|
@subdomains = Array(subdomains)
|
143
|
+
@host = @hosts.first || @subdomains.first
|
144
|
+
@server_names = @hosts + @hosts.map { |h| "www.#{h}" } + @subdomains
|
145
|
+
@var_prefix = "$#{@host.downcase.gsub(/[^a-z0-9]/, '_')}"
|
146
|
+
@port = port
|
147
|
+
@ssl = ssl
|
148
|
+
@one_host = one_host
|
78
149
|
@www = www
|
79
150
|
@multiple_slashes = multiple_slashes
|
80
151
|
@multiple_question_marks = multiple_question_marks
|
81
152
|
@trailing_slash = trailing_slash
|
82
153
|
@trailing_question_mark = trailing_question_mark
|
83
|
-
@
|
84
|
-
|
85
|
-
FileUtils.mkdir_p(@dir)
|
154
|
+
@ensure_host_entries = ensure_host_entries
|
86
155
|
|
156
|
+
@dir = File.join(Potluck.config.dir, @host).freeze
|
87
157
|
@config_file_active = File.join(@dir, CONFIG_NAME_ACTIVE).freeze
|
88
158
|
@config_file_inactive = File.join(@dir, CONFIG_NAME_INACTIVE).freeze
|
159
|
+
|
160
|
+
@default_ssl_certificate = File.join(@dir, "#{@host}.crt")
|
161
|
+
@default_ssl_certificate_key = File.join(@dir, "#{@host}.key")
|
162
|
+
@default_ssl_dhparam = File.join(@dir, 'dhparam.pem')
|
163
|
+
|
164
|
+
FileUtils.mkdir_p(@dir)
|
165
|
+
|
166
|
+
@config = NginxConfig.new
|
167
|
+
|
168
|
+
add_upstream_config
|
169
|
+
add_host_map_config
|
170
|
+
add_port_map_config
|
171
|
+
add_path_map_config
|
172
|
+
add_query_map_config
|
173
|
+
add_server_config
|
174
|
+
|
175
|
+
config(&block) if block
|
89
176
|
end
|
90
177
|
|
91
|
-
|
92
|
-
#
|
93
|
-
#
|
178
|
+
# Public: Ensure this instance's configuration file is active and start Nginx. If Nginx is already
|
179
|
+
# running, send a reload signal to the process after activating the configuration file. Does nothing if
|
180
|
+
# Nginx is not managed.
|
94
181
|
#
|
182
|
+
# Returns nothing.
|
95
183
|
def start
|
96
184
|
return unless manage?
|
97
185
|
|
98
|
-
|
99
|
-
ensure_host_entries
|
186
|
+
ensure_ssl_files
|
187
|
+
ensure_host_entries
|
100
188
|
ensure_include
|
101
189
|
|
102
190
|
write_config
|
@@ -107,13 +195,13 @@ module Potluck
|
|
107
195
|
status == :active ? reload : super
|
108
196
|
end
|
109
197
|
|
110
|
-
|
111
|
-
#
|
112
|
-
# managed.
|
198
|
+
# Public: Ensure this instance's configuration file is inactive and optionally stop Nginx. Does nothing
|
199
|
+
# if Nginx is not managed.
|
113
200
|
#
|
114
|
-
#
|
115
|
-
#
|
201
|
+
# hard - Boolean specifying if the Nginx process should be stopped (true) or this instance's
|
202
|
+
# configuration file simply inactivated (false).
|
116
203
|
#
|
204
|
+
# Returns nothing.
|
117
205
|
def stop(hard = false)
|
118
206
|
return unless manage?
|
119
207
|
|
@@ -122,261 +210,235 @@ module Potluck
|
|
122
210
|
hard || status != :active ? super() : reload
|
123
211
|
end
|
124
212
|
|
125
|
-
|
126
|
-
# Reloads Nginx if it's managed.
|
213
|
+
# Public: Reload Nginx. Does nothing if Nginx is not managed.
|
127
214
|
#
|
215
|
+
# Returns nothing.
|
128
216
|
def reload
|
129
217
|
return unless manage?
|
130
218
|
|
131
219
|
run('nginx -s reload')
|
132
220
|
end
|
133
221
|
|
134
|
-
|
135
|
-
# Returns the content for the Nginx configuration file as a string.
|
222
|
+
# Public: Get or modify the config.
|
136
223
|
#
|
137
|
-
|
138
|
-
|
224
|
+
# block - Block for making modifications. See NginxConfig#modify.
|
225
|
+
#
|
226
|
+
# Returns the NginxConfig instance.
|
227
|
+
def config(&block)
|
228
|
+
block ? @config.modify(&block) : @config
|
139
229
|
end
|
140
230
|
|
141
|
-
|
142
|
-
|
231
|
+
private
|
232
|
+
|
233
|
+
# Internal: Add upstream directive to the config.
|
143
234
|
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
<string>/usr/local/opt/nginx/bin/nginx</string>
|
150
|
-
<string>-g</string>
|
151
|
-
<string>daemon off;</string>
|
152
|
-
</array>
|
153
|
-
<key>StandardOutPath</key>
|
154
|
-
<string>/usr/local/var/log/nginx/access.log</string>
|
155
|
-
<key>StandardErrorPath</key>
|
156
|
-
<string>/usr/local/var/log/nginx/error.log</string>
|
157
|
-
EOS
|
158
|
-
)
|
235
|
+
# Returns nothing.
|
236
|
+
def add_upstream_config
|
237
|
+
config.upstream(@host) do |c|
|
238
|
+
c.server("127.0.0.1:#{@port}")
|
239
|
+
end
|
159
240
|
end
|
160
241
|
|
161
|
-
|
162
|
-
# Converts a hash to an Nginx configuration file content string. Keys should be strings and values
|
163
|
-
# either strings or hashes. A +nil+ value in a hash will result in that key-value pair being omitted.
|
164
|
-
#
|
165
|
-
# * +hash+ - Hash to convert to the string content of an Nginx configuration file.
|
166
|
-
# * +indent+ - Number of spaces to indent; used when the method is called recursively and should not be
|
167
|
-
# set explicitly (optional, default: 0).
|
168
|
-
# * +repeat+ - Value to prepend to each entry of the hash; used when the method is called recursively
|
169
|
-
# and should not be set explicitly (optional).
|
170
|
-
#
|
171
|
-
# Symbol keys in hashes are used as special directives. Including <tt>repeat: true</tt> will cause the
|
172
|
-
# parent hash's key for the child hash to be prefixed to each line of the output. Example:
|
173
|
-
#
|
174
|
-
# {
|
175
|
-
# # ...
|
242
|
+
# Internal: Add map directive for URL host normalization to the config.
|
176
243
|
#
|
177
|
-
#
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
#
|
199
|
-
# if ($scheme = https) { ... }
|
200
|
-
# if ($host ~ ^www.) { ... }
|
201
|
-
# """,
|
202
|
-
# }
|
203
|
-
# }
|
204
|
-
#
|
205
|
-
# Result:
|
244
|
+
# Returns nothing.
|
245
|
+
def add_host_map_config
|
246
|
+
host_map = @hosts.each.with_object({}) do |host, map|
|
247
|
+
www_key = "www.#{host}"
|
248
|
+
www_value = "#{'www.' unless @www == false}#{@one_host ? @host : host}"
|
249
|
+
|
250
|
+
non_www_key = host
|
251
|
+
non_www_value = "#{'www.' if @www}#{@one_host ? @host : host}"
|
252
|
+
|
253
|
+
map[www_key] = www_value unless www_key == www_value
|
254
|
+
map[non_www_key] = non_www_value unless non_www_key == non_www_value
|
255
|
+
end
|
256
|
+
|
257
|
+
config.map("$host #{@var_prefix}_host") do |c|
|
258
|
+
c << {
|
259
|
+
'default' => '$host',
|
260
|
+
**host_map,
|
261
|
+
}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Internal: Add map directives for URL port normalization to the config.
|
206
266
|
#
|
207
|
-
#
|
208
|
-
|
209
|
-
|
210
|
-
|
267
|
+
# Returns nothing.
|
268
|
+
def add_port_map_config
|
269
|
+
normalized_port = @ssl ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
|
270
|
+
|
271
|
+
config.map("$http_host #{@var_prefix}_request_port") do |c|
|
272
|
+
c << {
|
273
|
+
'default' => "''",
|
274
|
+
'~(:[0-9]+)$' => '$1',
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
config.map("$http_host #{@var_prefix}_port") do |c|
|
279
|
+
c << {
|
280
|
+
'default' => "''",
|
281
|
+
'~:[0-9]+$' => ":#{normalized_port}",
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
config.map("$http_host #{@var_prefix}_x_forwarded_port") do |c|
|
286
|
+
c << {
|
287
|
+
'default' => normalized_port,
|
288
|
+
'~:([0-9]+)$' => '$1',
|
289
|
+
}
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Internal: Add map directive for URL path normalization to the config.
|
211
294
|
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
if
|
219
|
-
|
220
|
-
to_nginx_config(v, indent: indent, repeat: k)
|
221
|
-
else
|
222
|
-
"#{' ' * indent}#{k} {\n#{to_nginx_config(v, indent: indent + 2)}#{' ' * indent}}\n"
|
223
|
-
end
|
224
|
-
elsif k == :raw
|
225
|
-
"#{v.gsub(/^(?=.)/, ' ' * indent)}\n\n"
|
226
|
-
else
|
227
|
-
"#{' ' * indent}#{"#{repeat} " if repeat}#{k}#{" #{v}" unless v == true};\n"
|
228
|
-
end
|
229
|
-
)
|
295
|
+
# Returns nothing.
|
296
|
+
def add_path_map_config
|
297
|
+
config.map("$uri #{@var_prefix}_uri") do |c|
|
298
|
+
c << {
|
299
|
+
'default' => '$uri',
|
300
|
+
'~^(.*/[^/.]+)/+$' => ('$1' if @trailing_slash == false),
|
301
|
+
'~^(.*/[^/.]+)$' => ('$1/' if @trailing_slash),
|
302
|
+
}
|
230
303
|
end
|
231
304
|
end
|
232
305
|
|
233
|
-
|
306
|
+
# Internal: Add map directives for URL query string normalization to the config.
|
307
|
+
#
|
308
|
+
# Returns nothing.
|
309
|
+
def add_query_map_config
|
310
|
+
config.map("$request_uri #{@var_prefix}_q") do |c|
|
311
|
+
c << {
|
312
|
+
'default' => "''",
|
313
|
+
'~\\?' => '?',
|
314
|
+
}
|
315
|
+
end
|
316
|
+
|
317
|
+
config.map("#{@var_prefix}_q$query_string #{@var_prefix}_query") do |c|
|
318
|
+
c << {
|
319
|
+
'default' => "#{@var_prefix}_q$query_string",
|
320
|
+
'~^\\?+([^\\?].*)$' => ('?$1' if @multiple_question_marks == false),
|
321
|
+
'~^(\\?+)$' =>
|
322
|
+
if @trailing_question_mark == false
|
323
|
+
"''"
|
324
|
+
elsif @multiple_question_marks == false
|
325
|
+
'?'
|
326
|
+
end,
|
327
|
+
"''" => ('?' if @trailing_question_mark),
|
328
|
+
}
|
329
|
+
end
|
330
|
+
end
|
234
331
|
|
235
|
-
|
236
|
-
# Returns a hash representation of the Nginx configuration file content. Any configuration passed to
|
237
|
-
# Nginx.new is deep-merged into a base configuration hash, meaning nested hashes are merged rather than
|
238
|
-
# overwritten (see Util.deep_merge).
|
332
|
+
# Internal: Add base/default server definition to the config.
|
239
333
|
#
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
{
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
'
|
265
|
-
'
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
elsif @trailing_question_mark == true
|
316
|
-
'if ($q !~ .) { set $q ?; set $r 1; }'
|
317
|
-
end}
|
318
|
-
#{if @trailing_slash == false
|
319
|
-
'if ($u ~ (.+?)/+$) { set $u $1; set $r 1; }'
|
320
|
-
elsif @trailing_slash == true
|
321
|
-
'if ($u ~ [^/]$) { set $u $u/; set $r 1; }'
|
322
|
-
end}
|
323
|
-
|
324
|
-
set $mr $request_method$r;
|
325
|
-
|
326
|
-
if ($mr ~ ^(GET|HEAD)1$) { return 301 $s://$h$p$u$q; }
|
327
|
-
if ($mr ~ 1$) { return 308 $s://$h$p$u$q; }
|
328
|
-
""".strip.gsub(/^ +/, '').gsub(/\n{3,}/, "\n\n"),
|
329
|
-
|
330
|
-
'proxy_pass' => "http://#{@host}",
|
331
|
-
'proxy_redirect' => 'off',
|
332
|
-
'proxy_set_header' => {
|
333
|
-
repeat: true,
|
334
|
-
'Host' => '$http_host',
|
335
|
-
'X-Real-IP' => '$remote_addr',
|
336
|
-
'X-Forwarded-For' => '$proxy_add_x_forwarded_for',
|
337
|
-
'X-Forwarded-Proto' => @ssl ? 'https' : 'http',
|
338
|
-
'X-Forwarded-Port' => '$port',
|
339
|
-
},
|
340
|
-
},
|
341
|
-
},
|
342
|
-
|
343
|
-
@additional_config,
|
344
|
-
)
|
345
|
-
}
|
346
|
-
|
347
|
-
config
|
334
|
+
# Returns nothing.
|
335
|
+
def add_server_config
|
336
|
+
config.server do |c|
|
337
|
+
c.server_name(@server_names)
|
338
|
+
|
339
|
+
c.listen(DEFAULT_HTTP_PORT, soft: true)
|
340
|
+
c.listen("[::]:#{DEFAULT_HTTP_PORT}", soft: true)
|
341
|
+
|
342
|
+
if @ssl
|
343
|
+
c.listen(DEFAULT_HTTPS_PORT, 'ssl', soft: true)
|
344
|
+
c.listen("[::]:#{DEFAULT_HTTPS_PORT}", 'ssl', soft: true)
|
345
|
+
c.http2('on', soft: true)
|
346
|
+
|
347
|
+
if @deprecated_ssl_crt_file
|
348
|
+
c.ssl_certificate(@deprecated_ssl_crt_file)
|
349
|
+
c.ssl_certificate_key(@deprecated_ssl_key_file)
|
350
|
+
c.ssl_dhparam(@deprecated_ssl_dhparam_file)
|
351
|
+
else
|
352
|
+
c.ssl_certificate(@default_ssl_certificate, soft: true)
|
353
|
+
c.ssl_certificate_key(@default_ssl_certificate_key, soft: true)
|
354
|
+
c.ssl_dhparam(@default_ssl_dhparam, soft: true)
|
355
|
+
end
|
356
|
+
|
357
|
+
c.ssl_ciphers('ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-' \
|
358
|
+
'GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-' \
|
359
|
+
'POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384', soft: true)
|
360
|
+
c.ssl_prefer_server_ciphers('off', soft: true)
|
361
|
+
c.ssl_protocols('TLSv1.2 TLSv1.3', soft: true)
|
362
|
+
c.ssl_session_cache('shared:SSL:10m', soft: true)
|
363
|
+
c.ssl_session_tickets('off', soft: true)
|
364
|
+
c.ssl_session_timeout('1d', soft: true)
|
365
|
+
c.ssl_stapling('on', soft: true)
|
366
|
+
c.ssl_stapling_verify('on', soft: true)
|
367
|
+
|
368
|
+
add_deprecated_config(@deprecated_ssl_config)
|
369
|
+
end
|
370
|
+
|
371
|
+
c.charset('UTF-8', soft: true)
|
372
|
+
|
373
|
+
c.access_log(File.join(@dir, 'nginx-access.log'), soft: true)
|
374
|
+
c.error_log(File.join(@dir, 'nginx-error.log'), soft: true)
|
375
|
+
|
376
|
+
c.merge_slashes(@multiple_slashes == false ? 'on' : 'off', soft: true)
|
377
|
+
|
378
|
+
c.gzip('on', soft: true)
|
379
|
+
c.gzip_types('application/javascript application/json application/xml text/css text/javascript ' \
|
380
|
+
'text/plain', soft: true)
|
381
|
+
|
382
|
+
c.add_header('Referrer-Policy', "'same-origin' always")
|
383
|
+
c.add_header('Strict-Transport-Security', "'max-age=31536000; includeSubDomains' always") if @ssl
|
384
|
+
c.add_header('X-Content-Type-Options', "'nosniff' always")
|
385
|
+
c.add_header('X-Frame-Options', "'DENY' always")
|
386
|
+
c.add_header('X-XSS-Protection', "'1; mode=block' always")
|
387
|
+
|
388
|
+
c.set('$normalized', "http#{'s' if @ssl}://#{@var_prefix}_host#{@var_prefix}_port" \
|
389
|
+
"#{@var_prefix}_uri#{@var_prefix}_query")
|
390
|
+
|
391
|
+
c << <<~CONFIG
|
392
|
+
if ($normalized != '$scheme://$host#{@var_prefix}_request_port$request_uri') {
|
393
|
+
return 308 $normalized;
|
394
|
+
}
|
395
|
+
CONFIG
|
396
|
+
|
397
|
+
c.location('/') do
|
398
|
+
c.proxy_pass("http://#{@host}")
|
399
|
+
c.proxy_redirect('off')
|
400
|
+
c.proxy_set_header('Host', '$http_host')
|
401
|
+
c.proxy_set_header('X-Real-IP', '$remote_addr')
|
402
|
+
c.proxy_set_header('X-Forwarded-For', '$proxy_add_x_forwarded_for')
|
403
|
+
c.proxy_set_header('X-Forwarded-Proto', @ssl ? 'https' : 'http')
|
404
|
+
c.proxy_set_header('X-Forwarded-Port', "#{@var_prefix}_x_forwarded_port")
|
405
|
+
end
|
406
|
+
|
407
|
+
add_deprecated_config(@deprecated_additional_config)
|
408
|
+
end
|
348
409
|
end
|
349
410
|
|
350
|
-
|
351
|
-
# Writes the Nginx configuration to the (inactive) configuration file.
|
411
|
+
# Internal: Write the Nginx configuration to the (inactive) configuration file.
|
352
412
|
#
|
413
|
+
# Returns nothing.
|
353
414
|
def write_config
|
354
|
-
File.
|
355
|
-
file.write(config_file_content)
|
356
|
-
end
|
415
|
+
File.write(@config_file_inactive, config.to_s)
|
357
416
|
end
|
358
417
|
|
359
|
-
|
360
|
-
# Renames the inactive Nginx configuration file to its active name.
|
418
|
+
# Internal: Rename the inactive Nginx configuration file to its active name.
|
361
419
|
#
|
420
|
+
# Returns nothing.
|
362
421
|
def activate_config
|
363
422
|
FileUtils.mv(@config_file_inactive, @config_file_active)
|
364
423
|
end
|
365
424
|
|
366
|
-
|
367
|
-
# Renames the active Nginx configuration file to its inactive name.
|
425
|
+
# Internal: Rename the active Nginx configuration file to its inactive name.
|
368
426
|
#
|
427
|
+
# Returns nothing.
|
369
428
|
def deactivate_config
|
370
|
-
FileUtils.mv(@config_file_active, @config_file_inactive) if File.
|
429
|
+
FileUtils.mv(@config_file_active, @config_file_inactive) if File.exist?(@config_file_active)
|
371
430
|
end
|
372
431
|
|
373
|
-
|
374
|
-
#
|
375
|
-
#
|
432
|
+
# Internal: Ensure hosts are mapped to localhost in the system /etc/hosts file if ensure_host_entries is
|
433
|
+
# enabled. Useful in development. Uses sudo to perform the write, which will prompt for the system
|
434
|
+
# user's password.
|
376
435
|
#
|
436
|
+
# Returns nothing.
|
377
437
|
def ensure_host_entries
|
438
|
+
return unless @ensure_host_entries
|
439
|
+
|
378
440
|
content = File.read('/etc/hosts')
|
379
|
-
missing_entries =
|
441
|
+
missing_entries = @server_names.each_with_object([]) do |h, a|
|
380
442
|
a << h unless content.include?(" #{h}\n")
|
381
443
|
end
|
382
444
|
|
@@ -387,24 +449,161 @@ module Potluck
|
|
387
449
|
run(
|
388
450
|
<<~CMD
|
389
451
|
sudo sh -c 'printf "
|
390
|
-
#{missing_entries.map { |h| "127.0.0.1 #{h}\n::1 #{h}"}.join("\n")}
|
452
|
+
#{missing_entries.map { |h| "127.0.0.1 #{h}\n::1 #{h}" }.join("\n")}
|
391
453
|
" >> /etc/hosts'
|
392
454
|
CMD
|
393
455
|
)
|
394
456
|
end
|
395
457
|
|
396
|
-
|
397
|
-
# Ensures Nginx's base configuration file contains an include statement for Potluck's Nginx
|
458
|
+
# Internal: Ensure Nginx's base configuration file contains an include statement for Potluck's Nginx
|
398
459
|
# configuration files. Sudo is not used, so Nginx's base configuration file must be writable by the
|
399
460
|
# system user running this Ruby process.
|
400
461
|
#
|
462
|
+
# Returns nothing.
|
401
463
|
def ensure_include
|
402
464
|
config_file = `nginx -t 2>&1`[TEST_CONFIG_REGEX, :config]
|
403
465
|
config_content = File.read(config_file)
|
404
466
|
|
405
|
-
|
406
|
-
|
407
|
-
|
467
|
+
unless config_content.match?(INCLUDE_REGEX)
|
468
|
+
config_content.sub!(HTTP_BLOCK_REGEX, "\\1\\2\\3include #{ACTIVE_CONFIG_PATTERN};\n\n\\3")
|
469
|
+
|
470
|
+
File.write(config_file, config_content)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# Internal: Generate self-signed certificate files if SSL is enabled, custom certificate files are not
|
475
|
+
# configured, and self-signed certificate files don't exist, will expire soon, or already did expire.
|
476
|
+
#
|
477
|
+
# Returns nothing.
|
478
|
+
def ensure_ssl_files
|
479
|
+
return unless @ssl
|
480
|
+
|
481
|
+
csr = File.join(@dir, "#{@host}.csr")
|
482
|
+
ssl_certificate, ssl_certificate_key, ssl_dhparam, auto_generated = ssl_config_values
|
483
|
+
|
484
|
+
if auto_generated
|
485
|
+
config.server(0) do |c|
|
486
|
+
c.ssl_stapling('off')
|
487
|
+
c.ssl_stapling_verify('off')
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
return if !auto_generated || (
|
492
|
+
csr && File.exist?(csr) &&
|
493
|
+
ssl_certificate && File.exist?(ssl_certificate) &&
|
494
|
+
ssl_certificate_key && File.exist?(ssl_certificate_key) &&
|
495
|
+
ssl_dhparam && File.exist?(ssl_dhparam) && (
|
496
|
+
Time.parse(run("openssl x509 -enddate -noout -in #{ssl_certificate}").sub('notAfter=', '')) -
|
497
|
+
Time.now
|
498
|
+
) >= SSL_CERT_RENEW_DAYS * 24 * 60 * 60
|
499
|
+
)
|
500
|
+
|
501
|
+
log('Generating SSL files...')
|
502
|
+
|
503
|
+
run("openssl genrsa -out #{ssl_certificate_key} 4096", capture_stderr: false)
|
504
|
+
run("openssl req -out #{csr} -key #{ssl_certificate_key} -new -sha256 -config /dev/stdin <<< " \
|
505
|
+
"'#{openssl_config}'", capture_stderr: false)
|
506
|
+
run("openssl x509 -in #{csr} -out #{ssl_certificate} -signkey #{ssl_certificate_key} -days " \
|
507
|
+
"#{SSL_CERT_DAYS} -req -sha256 -extensions req_ext -extfile /dev/stdin <<< '#{openssl_config}'",
|
508
|
+
capture_stderr: false)
|
509
|
+
run("openssl dhparam -out #{ssl_dhparam} 2048", capture_stderr: false)
|
510
|
+
|
511
|
+
add_cert_to_keychain(ssl_certificate)
|
512
|
+
end
|
513
|
+
|
514
|
+
# Internal: Add a self-signed SSL certificate file to the system keychain if running on macOS. Uses sudo
|
515
|
+
# to perform the write, which will prompt for the system user's password.
|
516
|
+
#
|
517
|
+
# ssl_certificate - String path to the SSL certificate file.
|
518
|
+
#
|
519
|
+
# Returns nothing.
|
520
|
+
def add_cert_to_keychain(ssl_certificate)
|
521
|
+
return unless RUBY_PLATFORM[/darwin/]
|
522
|
+
|
523
|
+
log('Adding cert to keychain...')
|
524
|
+
|
525
|
+
run(
|
526
|
+
"sudo security delete-certificate -t -c #{@host} 2>&1 || " \
|
527
|
+
"sudo security delete-certificate -c #{@host} 2>&1 || :"
|
528
|
+
)
|
529
|
+
|
530
|
+
run('sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ' \
|
531
|
+
"#{ssl_certificate}")
|
532
|
+
end
|
533
|
+
|
534
|
+
# Internal: Get and validate the current configured SSL certificate files.
|
535
|
+
#
|
536
|
+
# Returns an Array of String paths to the SSL certificate, key, and DH parameter files, plus a boolean
|
537
|
+
# indicating if the files are auto-generated.
|
538
|
+
def ssl_config_values
|
539
|
+
ssl_certificate = config.dig('server', 0, 'ssl_certificate', 0)
|
540
|
+
ssl_certificate_key = config.dig('server', 0, 'ssl_certificate_key', 0)
|
541
|
+
ssl_dhparam = config.dig('server', 0, 'ssl_dhparam', 0)
|
542
|
+
|
543
|
+
auto_generated = ssl_certificate == @default_ssl_certificate &&
|
544
|
+
ssl_certificate_key == @default_ssl_certificate_key &&
|
545
|
+
ssl_dhparam == @default_ssl_dhparam
|
546
|
+
|
547
|
+
if !auto_generated && (
|
548
|
+
ssl_certificate == @default_ssl_certificate ||
|
549
|
+
ssl_certificate.nil? ||
|
550
|
+
ssl_certificate_key == @default_ssl_certificate_key ||
|
551
|
+
ssl_certificate_key.nil? ||
|
552
|
+
ssl_dhparam == @default_ssl_dhparam ||
|
553
|
+
ssl_dhparam.nil?
|
554
|
+
)
|
555
|
+
raise('Nginx configuration must provide all three SSL file directives (or none): ' \
|
556
|
+
'ssl_certificate, ssl_certificate_key, ssl_dhparam')
|
557
|
+
end
|
558
|
+
|
559
|
+
[ssl_certificate, ssl_certificate_key, ssl_dhparam, auto_generated]
|
560
|
+
end
|
561
|
+
|
562
|
+
# Internal: Get the OpenSSL configuration content used when auto-generating an SSL certificate.
|
563
|
+
#
|
564
|
+
# Returns the String configuration.
|
565
|
+
def openssl_config
|
566
|
+
<<~OPENSSL
|
567
|
+
[ req ]
|
568
|
+
prompt = no
|
569
|
+
default_bits = 4096
|
570
|
+
distinguished_name = req_distinguished_name
|
571
|
+
req_extensions = req_ext
|
572
|
+
|
573
|
+
[ req_distinguished_name ]
|
574
|
+
commonName = #{@host}
|
575
|
+
|
576
|
+
[ req_ext ]
|
577
|
+
subjectAltName = @alt_names
|
578
|
+
|
579
|
+
[alt_names]
|
580
|
+
DNS.1 = #{@host}
|
581
|
+
DNS.2 = *.#{@host}
|
582
|
+
OPENSSL
|
583
|
+
end
|
584
|
+
|
585
|
+
# Internal: Iterate over a hash of config directives in the old deprecated style and add them.
|
586
|
+
#
|
587
|
+
# hash - Hash of config directives.
|
588
|
+
#
|
589
|
+
# Returns nothing.
|
590
|
+
def add_deprecated_config(hash)
|
591
|
+
return unless hash
|
592
|
+
|
593
|
+
config do |c|
|
594
|
+
hash.each do |directive, value|
|
595
|
+
if !value.kind_of?(Hash)
|
596
|
+
c.send(directive, value)
|
597
|
+
elsif value[:repeat]
|
598
|
+
value.each do |k, v|
|
599
|
+
c.send(directive, k, v) unless k == :repeat
|
600
|
+
end
|
601
|
+
else
|
602
|
+
c.send(directive) do
|
603
|
+
add_deprecated_config(value)
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
408
607
|
end
|
409
608
|
end
|
410
609
|
end
|