potluck-nginx 0.0.7 → 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 -1
- data/lib/potluck/nginx/nginx_config.rb +388 -0
- data/lib/potluck/nginx/version.rb +1 -1
- data/lib/potluck/nginx.rb +483 -283
- metadata +28 -25
- data/lib/potluck/nginx/ssl.rb +0 -134
- data/lib/potluck/nginx/util.rb +0 -55
data/lib/potluck/nginx.rb
CHANGED
@@ -2,25 +2,40 @@
|
|
2
2
|
|
3
3
|
require('fileutils')
|
4
4
|
require('potluck')
|
5
|
-
require_relative('nginx/
|
6
|
-
require_relative('nginx/util')
|
5
|
+
require_relative('nginx/nginx_config')
|
7
6
|
require_relative('nginx/version')
|
8
7
|
|
9
8
|
module Potluck
|
10
|
-
|
11
|
-
# A Ruby interface for configuring and controlling Nginx. Each instance of this class manages a separate
|
12
|
-
# Nginx configuration file, which is loaded and unloaded from the base Nginx configuration when #start and
|
13
|
-
# #stop are called, respectively. Any number of Ruby processes can thus each manage their own Nginx
|
14
|
-
# configuration and control whether or not it is active without interfering with any other instances or
|
15
|
-
# non-Ruby processes leveraging Nginx.
|
9
|
+
# Public: A Ruby interface for configuring and controlling Nginx.
|
16
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
|
17
31
|
class Nginx < Service
|
18
32
|
CONFIG_NAME_ACTIVE = 'nginx.conf'
|
19
33
|
CONFIG_NAME_INACTIVE = 'nginx-stopped.conf'
|
20
|
-
ACTIVE_CONFIG_PATTERN = File.join(
|
34
|
+
ACTIVE_CONFIG_PATTERN = File.join(Potluck.config.dir, '*', CONFIG_NAME_ACTIVE).freeze
|
21
35
|
|
22
|
-
TEST_CONFIG_REGEX = /nginx: configuration file (?<config>.+) test (failed|is successful)
|
23
|
-
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?)( *)/
|
24
39
|
|
25
40
|
NON_LAUNCHCTL_COMMANDS = {
|
26
41
|
status: 'ps aux | grep \'[n]ginx: master process\'',
|
@@ -28,76 +43,148 @@ module Potluck
|
|
28
43
|
stop: 'nginx -s stop',
|
29
44
|
}.freeze
|
30
45
|
|
31
|
-
|
32
|
-
|
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.
|
33
73
|
#
|
34
|
-
#
|
35
|
-
# * +port+ - Port that the upstream (Ruby web server) is running on.
|
36
|
-
# * +subdomains+ - One or more subdomains (optional).
|
37
|
-
# * +ssl+ - SSL configuration arguments to pass to SSL.new (optional).
|
38
|
-
# * +one_host+ - True if URLs should be normalized to the first host in +hosts+ (optional, default:
|
39
|
-
# false).
|
40
|
-
# * +www+ - +true+ if URLs should be normalized to include 'www.' prefix, +false+ to exclude 'www.', and
|
41
|
-
# +nil+ if either is acceptable (optional, default: +nil+).
|
42
|
-
# * +multiple_slashes+ - +false+ if any occurrence of multiple slashes in the path portion of the URL
|
43
|
-
# should be normalized to a single slash (optional, default: +nil+).
|
44
|
-
# * +multiple_question_marks+ - +false+ if multiple question marks in the URL signifying the start of
|
45
|
-
# the query string should be normalized to a single question mark (optional, default: +nil+).
|
46
|
-
# * +trailing_slash+ - +true+ if URLs should be normalized to include a trailing slash at the end of the
|
47
|
-
# path portion, +false+ to strip any trailing slash, and +nil+ if either is acceptable (optional,
|
48
|
-
# default: +nil+).
|
49
|
-
# * +trailing_question_mark+ - +true+ if URLs should be normalized to include a trailing question mark
|
50
|
-
# when the query string is empty, +false+ to strip any trailing question mark, and +nil+ if either is
|
51
|
-
# acceptable (optional, default: +nil+).
|
52
|
-
# * +config+ - Nginx configuration hash; see #config (optional).
|
53
|
-
# * +ensure_host_entries+ - True if +hosts+ should be added to system /etc/hosts file as mappings to
|
54
|
-
# localhost (optional, default: false).
|
55
|
-
# * +args+ - Arguments to pass to Potluck::Service.new (optional).
|
74
|
+
# message - String deprecation message.
|
56
75
|
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
62
112
|
end
|
63
113
|
|
64
|
-
super(**
|
114
|
+
super(**kwargs)
|
65
115
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
@port = port
|
116
|
+
if config
|
117
|
+
self.class.deprecated("Passing config: {...} to #{self.class.name}.new is deprecated: use a " \
|
118
|
+
'block instead')
|
70
119
|
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
74
134
|
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
78
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
|
79
149
|
@www = www
|
80
150
|
@multiple_slashes = multiple_slashes
|
81
151
|
@multiple_question_marks = multiple_question_marks
|
82
152
|
@trailing_slash = trailing_slash
|
83
153
|
@trailing_question_mark = trailing_question_mark
|
84
|
-
@
|
85
|
-
|
86
|
-
FileUtils.mkdir_p(@dir)
|
154
|
+
@ensure_host_entries = ensure_host_entries
|
87
155
|
|
156
|
+
@dir = File.join(Potluck.config.dir, @host).freeze
|
88
157
|
@config_file_active = File.join(@dir, CONFIG_NAME_ACTIVE).freeze
|
89
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
|
90
176
|
end
|
91
177
|
|
92
|
-
|
93
|
-
#
|
94
|
-
#
|
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.
|
95
181
|
#
|
182
|
+
# Returns nothing.
|
96
183
|
def start
|
97
184
|
return unless manage?
|
98
185
|
|
99
|
-
|
100
|
-
ensure_host_entries
|
186
|
+
ensure_ssl_files
|
187
|
+
ensure_host_entries
|
101
188
|
ensure_include
|
102
189
|
|
103
190
|
write_config
|
@@ -108,13 +195,13 @@ module Potluck
|
|
108
195
|
status == :active ? reload : super
|
109
196
|
end
|
110
197
|
|
111
|
-
|
112
|
-
#
|
113
|
-
# managed.
|
198
|
+
# Public: Ensure this instance's configuration file is inactive and optionally stop Nginx. Does nothing
|
199
|
+
# if Nginx is not managed.
|
114
200
|
#
|
115
|
-
#
|
116
|
-
#
|
201
|
+
# hard - Boolean specifying if the Nginx process should be stopped (true) or this instance's
|
202
|
+
# configuration file simply inactivated (false).
|
117
203
|
#
|
204
|
+
# Returns nothing.
|
118
205
|
def stop(hard = false)
|
119
206
|
return unless manage?
|
120
207
|
|
@@ -123,259 +210,235 @@ module Potluck
|
|
123
210
|
hard || status != :active ? super() : reload
|
124
211
|
end
|
125
212
|
|
126
|
-
|
127
|
-
# Reloads Nginx if it's managed.
|
213
|
+
# Public: Reload Nginx. Does nothing if Nginx is not managed.
|
128
214
|
#
|
215
|
+
# Returns nothing.
|
129
216
|
def reload
|
130
217
|
return unless manage?
|
131
218
|
|
132
219
|
run('nginx -s reload')
|
133
220
|
end
|
134
221
|
|
135
|
-
|
136
|
-
# Returns the content for the Nginx configuration file as a string.
|
222
|
+
# Public: Get or modify the config.
|
137
223
|
#
|
138
|
-
|
139
|
-
|
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
|
140
229
|
end
|
141
230
|
|
142
|
-
|
143
|
-
|
231
|
+
private
|
232
|
+
|
233
|
+
# Internal: Add upstream directive to the config.
|
144
234
|
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
<string>#{HOMEBREW_PREFIX}/opt/nginx/bin/nginx</string>
|
151
|
-
<string>-g</string>
|
152
|
-
<string>daemon off;</string>
|
153
|
-
</array>
|
154
|
-
<key>StandardOutPath</key>
|
155
|
-
<string>#{HOMEBREW_PREFIX}/var/log/nginx/access.log</string>
|
156
|
-
<key>StandardErrorPath</key>
|
157
|
-
<string>#{HOMEBREW_PREFIX}/var/log/nginx/error.log</string>
|
158
|
-
EOS
|
159
|
-
)
|
235
|
+
# Returns nothing.
|
236
|
+
def add_upstream_config
|
237
|
+
config.upstream(@host) do |c|
|
238
|
+
c.server("127.0.0.1:#{@port}")
|
239
|
+
end
|
160
240
|
end
|
161
241
|
|
162
|
-
|
163
|
-
# Converts a hash to an Nginx configuration file content string. Keys should be strings and values
|
164
|
-
# either strings or hashes. A +nil+ value in a hash will result in that key-value pair being omitted.
|
165
|
-
#
|
166
|
-
# * +hash+ - Hash to convert to the string content of an Nginx configuration file.
|
167
|
-
# * +indent+ - Number of spaces to indent; used when the method is called recursively and should not be
|
168
|
-
# set explicitly (optional, default: 0).
|
169
|
-
# * +repeat+ - Value to prepend to each entry of the hash; used when the method is called recursively
|
170
|
-
# and should not be set explicitly (optional).
|
171
|
-
#
|
172
|
-
# Symbol keys in hashes are used as special directives. Including <tt>repeat: true</tt> will cause the
|
173
|
-
# parent hash's key for the child hash to be prefixed to each line of the output. Example:
|
174
|
-
#
|
175
|
-
# {
|
176
|
-
# # ...
|
177
|
-
#
|
178
|
-
# 'add_header' => {
|
179
|
-
# repeat: true,
|
180
|
-
# 'X-Frame-Options' => 'DENY',
|
181
|
-
# 'X-Content-Type-Options' => 'nosniff',
|
182
|
-
# }
|
183
|
-
# }
|
184
|
-
#
|
185
|
-
# Result:
|
186
|
-
#
|
187
|
-
# # ...
|
188
|
-
#
|
189
|
-
# add_header X-Frame-Options DENY;
|
190
|
-
# add_header X-Content-Type-Options nosniff;
|
191
|
-
#
|
192
|
-
# A hash containing <tt>raw: '...'</tt> can be used to include a raw chunk of text rather than key-value
|
193
|
-
# pairs. Example:
|
194
|
-
#
|
195
|
-
# {
|
196
|
-
# # ...
|
197
|
-
#
|
198
|
-
# 'location /' => {
|
199
|
-
# raw: """
|
200
|
-
# if ($scheme = https) { ... }
|
201
|
-
# if ($host ~ ^www.) { ... }
|
202
|
-
# """,
|
203
|
-
# }
|
204
|
-
# }
|
242
|
+
# Internal: Add map directive for URL host normalization to the config.
|
205
243
|
#
|
206
|
-
#
|
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.
|
207
266
|
#
|
208
|
-
#
|
209
|
-
|
210
|
-
|
211
|
-
|
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.
|
212
294
|
#
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
if
|
220
|
-
|
221
|
-
to_nginx_config(v, indent: indent, repeat: k)
|
222
|
-
else
|
223
|
-
"#{' ' * indent}#{k} {\n#{to_nginx_config(v, indent: indent + 2)}#{' ' * indent}}\n"
|
224
|
-
end
|
225
|
-
elsif k == :raw
|
226
|
-
"#{v.gsub(/^(?=.)/, ' ' * indent)}\n\n"
|
227
|
-
else
|
228
|
-
"#{' ' * indent}#{"#{repeat} " if repeat}#{k}#{" #{v}" unless v == true};\n"
|
229
|
-
end
|
230
|
-
)
|
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
|
+
}
|
231
303
|
end
|
232
304
|
end
|
233
305
|
|
234
|
-
|
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
|
235
316
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
331
|
+
|
332
|
+
# Internal: Add base/default server definition to the config.
|
240
333
|
#
|
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
|
-
|
316
|
-
elsif @trailing_question_mark == true
|
317
|
-
'if ($q !~ .) { set $q ?; set $r 1; }'
|
318
|
-
end}
|
319
|
-
#{if @trailing_slash == false
|
320
|
-
'if ($u ~ (.+?)/+$) { set $u $1; set $r 1; }'
|
321
|
-
elsif @trailing_slash == true
|
322
|
-
'if ($u ~ [^/]$) { set $u $u/; set $r 1; }'
|
323
|
-
end}
|
324
|
-
|
325
|
-
set $mr $request_method$r;
|
326
|
-
|
327
|
-
if ($mr ~ ^(GET|HEAD)1$) { return 301 $s://$h$p$u$q; }
|
328
|
-
if ($mr ~ 1$) { return 308 $s://$h$p$u$q; }
|
329
|
-
""".strip.gsub(/^ +/, '').gsub(/\n{3,}/, "\n\n"),
|
330
|
-
|
331
|
-
'proxy_pass' => "http://#{@host}",
|
332
|
-
'proxy_redirect' => 'off',
|
333
|
-
'proxy_set_header' => {
|
334
|
-
repeat: true,
|
335
|
-
'Host' => '$http_host',
|
336
|
-
'X-Real-IP' => '$remote_addr',
|
337
|
-
'X-Forwarded-For' => '$proxy_add_x_forwarded_for',
|
338
|
-
'X-Forwarded-Proto' => @ssl ? 'https' : 'http',
|
339
|
-
'X-Forwarded-Port' => '$port',
|
340
|
-
},
|
341
|
-
},
|
342
|
-
},
|
343
|
-
|
344
|
-
@additional_config,
|
345
|
-
)
|
346
|
-
}
|
347
|
-
|
348
|
-
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
|
349
409
|
end
|
350
410
|
|
351
|
-
|
352
|
-
# Writes the Nginx configuration to the (inactive) configuration file.
|
411
|
+
# Internal: Write the Nginx configuration to the (inactive) configuration file.
|
353
412
|
#
|
413
|
+
# Returns nothing.
|
354
414
|
def write_config
|
355
|
-
File.write(@config_file_inactive,
|
415
|
+
File.write(@config_file_inactive, config.to_s)
|
356
416
|
end
|
357
417
|
|
358
|
-
|
359
|
-
# Renames the inactive Nginx configuration file to its active name.
|
418
|
+
# Internal: Rename the inactive Nginx configuration file to its active name.
|
360
419
|
#
|
420
|
+
# Returns nothing.
|
361
421
|
def activate_config
|
362
422
|
FileUtils.mv(@config_file_inactive, @config_file_active)
|
363
423
|
end
|
364
424
|
|
365
|
-
|
366
|
-
# Renames the active Nginx configuration file to its inactive name.
|
425
|
+
# Internal: Rename the active Nginx configuration file to its inactive name.
|
367
426
|
#
|
427
|
+
# Returns nothing.
|
368
428
|
def deactivate_config
|
369
429
|
FileUtils.mv(@config_file_active, @config_file_inactive) if File.exist?(@config_file_active)
|
370
430
|
end
|
371
431
|
|
372
|
-
|
373
|
-
#
|
374
|
-
#
|
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.
|
375
435
|
#
|
436
|
+
# Returns nothing.
|
376
437
|
def ensure_host_entries
|
438
|
+
return unless @ensure_host_entries
|
439
|
+
|
377
440
|
content = File.read('/etc/hosts')
|
378
|
-
missing_entries =
|
441
|
+
missing_entries = @server_names.each_with_object([]) do |h, a|
|
379
442
|
a << h unless content.include?(" #{h}\n")
|
380
443
|
end
|
381
444
|
|
@@ -386,24 +449,161 @@ module Potluck
|
|
386
449
|
run(
|
387
450
|
<<~CMD
|
388
451
|
sudo sh -c 'printf "
|
389
|
-
#{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")}
|
390
453
|
" >> /etc/hosts'
|
391
454
|
CMD
|
392
455
|
)
|
393
456
|
end
|
394
457
|
|
395
|
-
|
396
|
-
# 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
|
397
459
|
# configuration files. Sudo is not used, so Nginx's base configuration file must be writable by the
|
398
460
|
# system user running this Ruby process.
|
399
461
|
#
|
462
|
+
# Returns nothing.
|
400
463
|
def ensure_include
|
401
464
|
config_file = `nginx -t 2>&1`[TEST_CONFIG_REGEX, :config]
|
402
465
|
config_content = File.read(config_file)
|
403
466
|
|
404
|
-
|
405
|
-
|
406
|
-
|
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
|
407
607
|
end
|
408
608
|
end
|
409
609
|
end
|