potluck-nginx 0.0.2 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc33a9aab81524eb41a2c734599b947bf3893cf62c72de1edeaee25850961500
4
- data.tar.gz: 4802c81ccc6c0fabdb2bbfe2c77db4d9798ac9af9f4a827fb5816ca81c991295
3
+ metadata.gz: 27f10a23324844c7c0e3733f2ce1d7d48c0fd4fd61d0444e45f7db62a3a4d58a
4
+ data.tar.gz: '094a492be3e1efa5c5919f14111d03b86de1ab809e8482635b313671d658bb12'
5
5
  SHA512:
6
- metadata.gz: 64c6f24f5a322452320f942ea7feefab6afa9970c96786cf0807f261f2209e1f29c4229454e1e65629d408b282abc304821e7163ccf6d622c26802109bb6d2f5
7
- data.tar.gz: c5133b05bd80ec716eaf60f7ea41cc0c44c643d54ea729dcc36c6e8df3fdbc0cdeb4037d34097f25233b31f6138468efc64762397338803b4ae5091dfab333d1
6
+ metadata.gz: 9216e7234a2ccfcb0ae3589b517b76b2bc4ba7800cd73cd9fc5330b516511ab0ad41ae80b0daf3266dada155dd1e8005866cccbab1a28afabdd6a2473d41a62f
7
+ data.tar.gz: 5bce05141ee5156f2cf1b9d83d062bb9c4fbea5226a8b666ecce8dbb54e5d0bd273e027c014e847a9f24cf11c22ad77a4bc68c733b8e9f9f56908e279382a0fc
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2021 Nate Pickens
1
+ Copyright 2021-2022 Nate Pickens
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
4
  documentation files (the "Software"), to deal in the Software without restriction, including without
@@ -3,7 +3,11 @@
3
3
  require('time')
4
4
 
5
5
  module Potluck
6
- class Nginx < Dish
6
+ class Nginx < Service
7
+ ##
8
+ # SSL-specific configuration for Nginx. Provides self-signed certificate generation for use in
9
+ # developemnt.
10
+ #
7
11
  class SSL
8
12
  # Reference: https://ssl-config.mozilla.org/#server=nginx&config=intermediate&guideline=5.6
9
13
  DEFAULT_CONFIG = {
@@ -26,8 +30,18 @@ module Potluck
26
30
 
27
31
  attr_reader(:csr_file, :key_file, :crt_file, :dhparam_file, :config)
28
32
 
29
- def initialize(nginx, dir, host, crt_file: nil, key_file: nil, dhparam_file: nil,
30
- config: {})
33
+ ##
34
+ # Creates a new instance. Providing no SSL files will cue generation of a self-signed certificate.
35
+ #
36
+ # * +nginx+ - Nginx instance.
37
+ # * +dir+ - Directory where SSL files are located or should be written to.
38
+ # * +host+ - Name of the host for determining file names and generating a self-signed certificate.
39
+ # * +crt_file+ - Path to the CRT file (optional).
40
+ # * +key_file+ - Path to the KEY file (optional).
41
+ # * +dhparam_file+ - Path to the DH parameters file (optional).
42
+ # * +config+ - Nginx configuration hash (optional).
43
+ #
44
+ def initialize(nginx, dir, host, crt_file: nil, key_file: nil, dhparam_file: nil, config: {})
31
45
  @nginx = nginx
32
46
  @dir = dir
33
47
  @host = host
@@ -35,7 +49,7 @@ module Potluck
35
49
  @auto_generated = !crt_file && !key_file && !dhparam_file
36
50
 
37
51
  if !@auto_generated && (!crt_file || !key_file || !dhparam_file)
38
- raise('Must supply values for all three or none: crt_file, key_file, dhparam_file')
52
+ raise(ArgumentError, 'Must supply values for all three or none: crt_file, key_file, dhparam_file')
39
53
  end
40
54
 
41
55
  @csr_file = File.join(@dir, "#{@host}.csr").freeze
@@ -43,15 +57,20 @@ module Potluck
43
57
  @key_file = key_file || File.join(@dir, "#{@host}.key").freeze
44
58
  @dhparam_file = dhparam_file || File.join(@dir, 'dhparam.pem').freeze
45
59
 
46
- @config = {
60
+ @config = Util.deep_merge({
47
61
  'ssl_certificate' => @crt_file,
48
62
  'ssl_certificate_key' => @key_file,
49
63
  'ssl_dhparam' => @dhparam_file,
50
64
  'ssl_stapling' => ('on' unless @auto_generated),
51
65
  'ssl_stapling_verify' => ('on' unless @auto_generated),
52
- }.merge!(DEFAULT_CONFIG).merge!(config)
66
+ }, DEFAULT_CONFIG, config)
53
67
  end
54
68
 
69
+ ##
70
+ # If SSL files were passed to SSL.new, does nothing. Otherwise checks if auto-generated SSL files
71
+ # exist and generates them if not. If they do exist, the expiration for the certificate is checked and
72
+ # the certificate regenerated if the expiration date is soon or in the past.
73
+ #
55
74
  def ensure_files
56
75
  return if !@auto_generated || (
57
76
  File.exists?(@csr_file) &&
@@ -65,13 +84,13 @@ module Potluck
65
84
 
66
85
  @nginx.log('Generating SSL files...')
67
86
 
68
- @nginx.run("openssl genrsa -out #{@key_file} 4096", redirect_stderr: false)
87
+ @nginx.run("openssl genrsa -out #{@key_file} 4096", capture_stderr: false)
69
88
  @nginx.run("openssl req -out #{@csr_file} -key #{@key_file} -new -sha256 -config /dev/stdin <<< "\
70
- "'#{openssl_config}'", redirect_stderr: false)
89
+ "'#{openssl_config}'", capture_stderr: false)
71
90
  @nginx.run("openssl x509 -in #{@csr_file} -out #{@crt_file} -signkey #{@key_file} -days "\
72
91
  "#{CERT_DAYS} -req -sha256 -extensions req_ext -extfile /dev/stdin <<< '#{openssl_config}'",
73
- redirect_stderr: false)
74
- @nginx.run("openssl dhparam -out #{@dhparam_file} 2048", redirect_stderr: false)
92
+ capture_stderr: false)
93
+ @nginx.run("openssl dhparam -out #{@dhparam_file} 2048", capture_stderr: false)
75
94
 
76
95
  if IS_MACOS
77
96
  @nginx.log('Adding cert to keychain...')
@@ -88,6 +107,9 @@ module Potluck
88
107
 
89
108
  private
90
109
 
110
+ ##
111
+ # OpenSSL configuration content used when auto-generating an SSL certificate.
112
+ #
91
113
  def openssl_config
92
114
  <<~EOS
93
115
  [ req ]
@@ -1,17 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Potluck
4
- class Nginx
4
+ class Nginx < Service
5
+ ##
6
+ # Utility methods for Nginx class.
7
+ #
5
8
  class Util
6
- def self.deep_merge!(*hashes, arrays: false)
7
- hash = hashes[0]
9
+ ##
10
+ # Merges N hashes by merging nested hashes rather than overwriting them as is the case with
11
+ # <tt>Hash#merge</tt>.
12
+ #
13
+ # * +hashes+ - Hashes to deep merge.
14
+ # * +arrays+ - True if arrays should be merged rather than overwritten (optional, default: false).
15
+ #
16
+ # Example:
17
+ #
18
+ # h1 = {hello: {item1: 'world'}}
19
+ # h2 = {hello: {item2: 'friend'}}
20
+ #
21
+ # Util.deep_merge(h1, h2)
22
+ # # => {hello: {item1: 'world', item2: 'friend'}}
23
+ #
24
+ # By default only hashes are merged and arrays are still overwritten as they are with
25
+ # <tt>Hash#merge</tt>. Passing <tt>arrays: true</tt> will result in arrays being merged similarly to
26
+ # hashes. Example:
27
+ #
28
+ # h1 = {hello: {item1: ['world']}}
29
+ # h2 = {hello: {item1: ['friend']}}
30
+ #
31
+ # Util.deep_merge(h1, h2, arrays: true)
32
+ # # => {hello: {item1: ['world', 'friend']}}
33
+ #
34
+ def self.deep_merge(*hashes, arrays: false)
35
+ hash = hashes[0].dup
8
36
 
9
37
  hashes[1..-1].each do |other_hash|
10
38
  other_hash.each do |key, other_value|
11
39
  this_value = hash[key]
12
40
 
13
41
  if this_value.kind_of?(Hash) && other_value.kind_of?(Hash)
14
- deep_merge!(this_value, other_value, arrays: arrays)
42
+ hash[key] = deep_merge(this_value, other_value, arrays: arrays)
15
43
  elsif arrays && this_value.kind_of?(Array)
16
44
  hash[key] |= Array(other_value)
17
45
  else
data/lib/potluck/nginx.rb CHANGED
@@ -6,7 +6,14 @@ require_relative('nginx/ssl')
6
6
  require_relative('nginx/util')
7
7
 
8
8
  module Potluck
9
- class Nginx < Dish
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.
15
+ #
16
+ class Nginx < Service
10
17
  CONFIG_NAME_ACTIVE = 'nginx.conf'
11
18
  CONFIG_NAME_INACTIVE = 'nginx-stopped.conf'
12
19
  ACTIVE_CONFIG_PATTERN = File.join(DIR, '*', CONFIG_NAME_ACTIVE).freeze
@@ -14,8 +21,45 @@ module Potluck
14
21
  TEST_CONFIG_REGEX = /nginx: configuration file (?<config>.+) test (failed|is successful)/.freeze
15
22
  INCLUDE_REGEX = /^ *include +#{Regexp.escape(ACTIVE_CONFIG_PATTERN)} *;/.freeze
16
23
 
24
+ NON_LAUNCHCTL_COMMANDS = {
25
+ status: 'ps aux | grep \'[n]ginx: master process\'',
26
+ start: 'nginx',
27
+ stop: 'nginx -s stop',
28
+ }.freeze
29
+
30
+ ##
31
+ # Creates a new instance.
32
+ #
33
+ # * +hosts+ - One or more hosts.
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).
55
+ #
17
56
  def initialize(hosts, port, subdomains: nil, ssl: nil, one_host: false, www: nil, multiple_slashes: nil,
18
- multiple_question_marks: nil, trailing_slash: nil, trailing_question_mark: nil, config: {}, **args)
57
+ multiple_question_marks: nil, trailing_slash: nil, trailing_question_mark: nil, config: {},
58
+ ensure_host_entries: false, **args)
59
+ if args[:manage] && !args[:manage].kind_of?(Hash) && !self.class.launchctl?
60
+ args[:manage] = NON_LAUNCHCTL_COMMANDS
61
+ end
62
+
19
63
  super(**args)
20
64
 
21
65
  @hosts = Array(hosts).map { |h| h.sub(/^www\./, '') }.uniq
@@ -23,6 +67,7 @@ module Potluck
23
67
  @host = @hosts.first
24
68
  @port = port
25
69
 
70
+ @ensure_host_entries = ensure_host_entries
26
71
  @dir = File.join(DIR, @host)
27
72
  @ssl = SSL.new(self, @dir, @host, **ssl) if ssl
28
73
 
@@ -37,16 +82,21 @@ module Potluck
37
82
  @trailing_question_mark = trailing_question_mark
38
83
  @additional_config = config
39
84
 
40
- FileUtils.mkdir_p(DIR)
41
85
  FileUtils.mkdir_p(@dir)
42
86
 
43
87
  @config_file_active = File.join(@dir, CONFIG_NAME_ACTIVE).freeze
44
88
  @config_file_inactive = File.join(@dir, CONFIG_NAME_INACTIVE).freeze
45
89
  end
46
90
 
91
+ ##
92
+ # Ensures this instance's configuration file is active and starts Nginx if it's managed. If Nginx is
93
+ # already running, a reload signal is sent to the process after activating the configuration file.
94
+ #
47
95
  def start
96
+ return unless manage?
97
+
48
98
  @ssl&.ensure_files
49
- ensure_host_entries
99
+ ensure_host_entries if @ensure_host_entries
50
100
  ensure_include
51
101
 
52
102
  write_config
@@ -57,18 +107,136 @@ module Potluck
57
107
  status == :active ? reload : super
58
108
  end
59
109
 
110
+ ##
111
+ # Ensures this instance's configuration file is inactive and optionally stops the Nginx process if it's
112
+ # managed.
113
+ #
114
+ # * +hard+ - True if the Nginx process should be stopped, false to just inactivate this instance's
115
+ # configuration file and leave Nginx running (optional, default: false).
116
+ #
60
117
  def stop(hard = false)
118
+ return unless manage?
119
+
61
120
  deactivate_config
62
121
 
63
122
  hard || status != :active ? super() : reload
64
123
  end
65
124
 
125
+ ##
126
+ # Reloads Nginx if it's managed.
127
+ #
66
128
  def reload
129
+ return unless manage?
130
+
67
131
  run('nginx -s reload')
68
132
  end
69
133
 
134
+ ##
135
+ # Returns the content for the Nginx configuration file as a string.
136
+ #
137
+ def config_file_content
138
+ self.class.to_nginx_config(config)
139
+ end
140
+
141
+ ##
142
+ # Content of the launchctl plist file.
143
+ #
144
+ def self.plist
145
+ super(
146
+ <<~EOS
147
+ <key>ProgramArguments</key>
148
+ <array>
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
+ )
159
+ end
160
+
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
+ # # ...
176
+ #
177
+ # 'add_header' => {
178
+ # repeat: true,
179
+ # 'X-Frame-Options' => 'DENY',
180
+ # 'X-Content-Type-Options' => 'nosniff',
181
+ # }
182
+ # }
183
+ #
184
+ # Result:
185
+ #
186
+ # # ...
187
+ #
188
+ # add_header X-Frame-Options DENY;
189
+ # add_header X-Content-Type-Options nosniff;
190
+ #
191
+ # A hash containing <tt>raw: '...'</tt> can be used to include a raw chunk of text rather than key-value
192
+ # pairs. Example:
193
+ #
194
+ # {
195
+ # # ...
196
+ #
197
+ # 'location /' => {
198
+ # raw: """
199
+ # if ($scheme = https) { ... }
200
+ # if ($host ~ ^www.) { ... }
201
+ # """,
202
+ # }
203
+ # }
204
+ #
205
+ # Result:
206
+ #
207
+ # location / {
208
+ # if ($scheme = https) { ... }
209
+ # if ($host ~ ^www.) { ... }
210
+ # }
211
+ #
212
+ def self.to_nginx_config(hash, indent: 0, repeat: nil)
213
+ hash.each_with_object(+'') do |(k, v), config|
214
+ next if v.nil?
215
+ next if k == :repeat
216
+
217
+ config << (
218
+ if v.kind_of?(Hash)
219
+ if v[:repeat]
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
+ )
230
+ end
231
+ end
232
+
70
233
  private
71
234
 
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).
239
+ #
72
240
  def config
73
241
  host_subdomains_regex = ([@host] + @subdomains).join('|')
74
242
  hosts_subdomains_regex = (@hosts + @subdomains).join('|')
@@ -78,113 +246,134 @@ module Potluck
78
246
  'server' => "127.0.0.1:#{@port}",
79
247
  },
80
248
 
81
- 'server' => Util.deep_merge!({
82
- 'charset' => 'UTF-8',
83
- 'access_log' => File.join(@dir, 'nginx-access.log'),
84
- 'error_log' => File.join(@dir, 'nginx-error.log'),
85
-
86
- 'listen' => {
87
- repeat: true,
88
- '8080' => true,
89
- '[::]:8080' => true,
90
- '4433 ssl http2' => @ssl ? true : nil,
91
- '[::]:4433 ssl http2' => @ssl ? true : nil,
92
- },
93
- 'server_name' => (@hosts + @subdomains).join(' '),
249
+ 'server' => Util.deep_merge(
250
+ {
251
+ 'charset' => 'UTF-8',
252
+ 'access_log' => File.join(@dir, 'nginx-access.log'),
253
+ 'error_log' => File.join(@dir, 'nginx-error.log'),
254
+
255
+ 'listen' => {
256
+ repeat: true,
257
+ '8080' => true,
258
+ '[::]:8080' => true,
259
+ '4433 ssl http2' => @ssl ? true : nil,
260
+ '[::]:4433 ssl http2' => @ssl ? true : nil,
261
+ },
262
+ 'server_name' => (@hosts + @subdomains).join(' '),
94
263
 
95
- 'gzip' => 'on',
96
- 'gzip_types' => 'application/javascript application/json text/css text/plain',
264
+ 'gzip' => 'on',
265
+ 'gzip_types' => 'application/javascript application/json application/xml text/css '\
266
+ 'text/javascript text/plain',
97
267
 
98
- 'add_header' => {
99
- repeat: true,
100
- 'Referrer-Policy' => 'same-origin',
101
- 'X-Frame-Options' => 'DENY',
102
- 'X-XSS-Protection' => '\'1; mode=block\'',
103
- 'X-Content-Type-Options' => 'nosniff',
104
- },
105
- }, @ssl ? @ssl.config : {}).merge!(
106
- 'location /' => {
107
- raw: """
108
- if ($host !~ ^#{hosts_subdomains_regex}$) { return 404; }
109
-
110
- set $r 0;
111
- set $s $scheme;
112
- set $h $host;
113
- set $p '';
114
- set $u '';
115
- set $q '';
116
-
117
- #{if @www.nil? && @one_host == false
118
- nil
119
- elsif @www.nil? && @one_host == true
120
- "if ($host !~ ^(www.)?#{host_subdomains_regex}$) { set $h $1#{@host}; set $r 1; }"
121
- elsif @www == false && @one_host == false
122
- "if ($host ~ ^www.(.+)$) { set $h $1; set $r 1; }"
123
- elsif @www == false && @one_host == true
124
- "if ($host !~ ^#{host_subdomains_regex}$) { set $h #{@host}; set $r 1; }"
125
- elsif @www == true && @one_host == false
126
- "if ($host !~ ^www.(.+)$) { set $h $1; set $r 1; }"
127
- elsif @www == true && @one_host == true
128
- "if ($host !~ ^www.#{host_subdomains_regex}$) { set $h www.#{@host}; set $r 1; }"
129
- end}
130
-
131
- if ($scheme = #{@other_scheme}) { set $s #{@scheme}; set $r 1; }
132
- if ($http_host ~ :[0-9]+$) { set $p :#{@ssl ? '4433' : '8080'}; }
133
- if ($request_uri ~ ^([^\\?]+)(\\?+.*)?$) { set $u $1; set $q $2; }
134
-
135
- #{'if ($u ~ //) { set $u $uri; set $r 1; }' if @multiple_slashes == false}
136
- #{'if ($q ~ ^\?\?+(.*)$) { set $q ?$1; set $r 1; }' if @multiple_question_marks == false}
137
-
138
- #{if @trailing_question_mark == false
139
- 'if ($q ~ \?+$) { set $q \'\'; set $r 1; }'
140
- elsif @trailing_question_mark == true
141
- 'if ($q !~ .) { set $q ?; set $r 1; }'
142
- end}
143
- #{if @trailing_slash == false
144
- 'if ($u ~ (.+?)/+$) { set $u $1; set $r 1; }'
145
- elsif @trailing_slash == true
146
- 'if ($u ~ [^/]$) { set $u $u/; set $r 1; }'
147
- end}
148
-
149
- set $mr $request_method$r;
150
-
151
- if ($mr ~ ^(GET|HEAD)1$) { return 301 $s://$h$p$u$q; }
152
- if ($mr ~ 1$) { return 308 $s://$h$p$u$q; }
153
- """.strip.gsub(/^ +/, '').gsub(/\n{3,}/, "\n\n"),
154
-
155
- 'proxy_pass' => "http://#{@host}",
156
- 'proxy_redirect' => 'off',
157
- 'proxy_set_header' => {
268
+ 'add_header' => {
158
269
  repeat: true,
159
- 'Host' => @host,
160
- 'X-Real-IP' => '$remote_addr',
161
- 'X-Forwarded-For' => '$proxy_add_x_forwarded_for',
162
- 'X-Forwarded-Proto' => @ssl ? 'https' : 'http',
163
- 'X-Forwarded-Port' => @ssl ? '443' : '80',
270
+ 'Referrer-Policy' => '\'same-origin\' always',
271
+ 'X-Frame-Options' => '\'DENY\' always',
272
+ 'X-XSS-Protection' => '\'1; mode=block\' always',
273
+ 'X-Content-Type-Options' => '\'nosniff\' always',
274
+ },
275
+ },
276
+
277
+ @ssl ? @ssl.config : {},
278
+
279
+ {
280
+ 'location /' => {
281
+ raw: """
282
+ if ($host !~ ^#{hosts_subdomains_regex}$) { return 404; }
283
+
284
+ set $r 0;
285
+ set $s $scheme;
286
+ set $h $host;
287
+ set $port #{@ssl ? '443' : '80'};
288
+ set $p '';
289
+ set $u '';
290
+ set $q '';
291
+
292
+ #{if @www.nil? && @one_host == false
293
+ nil
294
+ elsif @www.nil? && @one_host == true
295
+ "if ($host !~ ^(www.)?#{host_subdomains_regex}$) { set $h $1#{@host}; set $r 1; }"
296
+ elsif @www == false && @one_host == false
297
+ "if ($host ~ ^www.(.+)$) { set $h $1; set $r 1; }"
298
+ elsif @www == false && @one_host == true
299
+ "if ($host !~ ^#{host_subdomains_regex}$) { set $h #{@host}; set $r 1; }"
300
+ elsif @www == true && @one_host == false
301
+ "if ($host !~ ^www.(.+)$) { set $h $1; set $r 1; }"
302
+ elsif @www == true && @one_host == true
303
+ "if ($host !~ ^www.#{host_subdomains_regex}$) { set $h www.#{@host}; set $r 1; }"
304
+ end}
305
+
306
+ if ($scheme = #{@other_scheme}) { set $s #{@scheme}; set $r 1; }
307
+ if ($http_host ~ :([0-9]+)$) { set $p :$1; set $port $1; }
308
+ if ($request_uri ~ ^([^\\?]+)(\\?+.*)?$) { set $u $1; set $q $2; }
309
+
310
+ #{'if ($u ~ //) { set $u $uri; set $r 1; }' if @multiple_slashes == false}
311
+ #{'if ($q ~ ^\?\?+(.*)$) { set $q ?$1; set $r 1; }' if @multiple_question_marks == false}
312
+
313
+ #{if @trailing_question_mark == false
314
+ 'if ($q ~ \?+$) { set $q \'\'; set $r 1; }'
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
+ },
164
340
  },
165
341
  },
166
- ),
167
- }
168
342
 
169
- Util.deep_merge!(config['server'], @additional_config)
343
+ @additional_config,
344
+ )
345
+ }
170
346
 
171
347
  config
172
348
  end
173
349
 
350
+ ##
351
+ # Writes the Nginx configuration to the (inactive) configuration file.
352
+ #
174
353
  def write_config
175
354
  File.open(@config_file_inactive, 'w') do |file|
176
- file.write(self.class.to_nginx_config(config))
355
+ file.write(config_file_content)
177
356
  end
178
357
  end
179
358
 
359
+ ##
360
+ # Renames the inactive Nginx configuration file to its active name.
361
+ #
180
362
  def activate_config
181
363
  FileUtils.mv(@config_file_inactive, @config_file_active)
182
364
  end
183
365
 
366
+ ##
367
+ # Renames the active Nginx configuration file to its inactive name.
368
+ #
184
369
  def deactivate_config
185
370
  FileUtils.mv(@config_file_active, @config_file_inactive) if File.exists?(@config_file_active)
186
371
  end
187
372
 
373
+ ##
374
+ # Ensures hosts are mapped to localhost in the system /etc/hosts file. Useful in development. Uses sudo
375
+ # to perform the write, which will prompt for the system user's password.
376
+ #
188
377
  def ensure_host_entries
189
378
  content = File.read('/etc/hosts')
190
379
  missing_entries = (@hosts + @subdomains).each_with_object([]) do |h, a|
@@ -204,6 +393,11 @@ module Potluck
204
393
  )
205
394
  end
206
395
 
396
+ ##
397
+ # Ensures Nginx's base configuration file contains an include statement for Potluck's Nginx
398
+ # configuration files. Sudo is not used, so Nginx's base configuration file must be writable by the
399
+ # system user running this Ruby process.
400
+ #
207
401
  def ensure_include
208
402
  config_file = `nginx -t 2>&1`[TEST_CONFIG_REGEX, :config]
209
403
  config_content = File.read(config_file)
@@ -213,43 +407,5 @@ module Potluck
213
407
  "\\1\\2\\3include #{ACTIVE_CONFIG_PATTERN};\n\n\\3"))
214
408
  end
215
409
  end
216
-
217
- def self.to_nginx_config(hash, indent: 0, repeat: nil)
218
- hash.each_with_object(+'') do |(k, v), config|
219
- next if v.nil?
220
- next if k == :repeat
221
-
222
- config << (
223
- if v.kind_of?(Hash)
224
- if v[:repeat]
225
- to_nginx_config(v, indent: indent, repeat: k)
226
- else
227
- "#{' ' * indent}#{k} {\n#{to_nginx_config(v, indent: indent + 2)}#{' ' * indent}}\n"
228
- end
229
- elsif k == :raw
230
- "#{v.gsub(/^(?=.)/, ' ' * indent)}\n\n"
231
- else
232
- "#{' ' * indent}#{"#{repeat} " if repeat}#{k}#{" #{v}" unless v == true};\n"
233
- end
234
- )
235
- end
236
- end
237
-
238
- def self.plist
239
- super(
240
- <<~EOS
241
- <key>ProgramArguments</key>
242
- <array>
243
- <string>/usr/local/opt/nginx/bin/nginx</string>
244
- <string>-g</string>
245
- <string>daemon off;</string>
246
- </array>
247
- <key>StandardOutPath</key>
248
- <string>/usr/local/var/log/nginx/access.log</string>
249
- <key>StandardErrorPath</key>
250
- <string>/usr/local/var/log/nginx/error.log</string>
251
- EOS
252
- )
253
- end
254
410
  end
255
411
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: potluck-nginx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-13 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: potluck
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.2
19
+ version: 0.0.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.2
26
+ version: 0.0.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -92,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
94
  requirements: []
95
- rubygems_version: 3.2.3
95
+ rubygems_version: 3.2.32
96
96
  signing_key:
97
97
  specification_version: 4
98
98
  summary: A Ruby manager for Nginx.