potluck-nginx 0.0.2 → 0.0.6

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 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.