potluck-nginx 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 599537fcab6df50b0126d73460295be4b366101667ad4c2317199b361153a5c1
4
+ data.tar.gz: 6a38b16b915efa971b1b93361dd1b995937b67a61031376879cf0aae857438be
5
+ SHA512:
6
+ metadata.gz: f007969613094559c4b68707b8bf87f570def7da9b5122673de84ca2a4c45409450ba348f7d8ff522191188afacdf1cf1a94c15f1a0b4cc36419e446ca921644
7
+ data.tar.gz: 6c3672941908c91d9151e7863ed0cb93c7e7ebf4463bcc2b7c8d997ac3baf529ef124bad4a1733c33ee3221a12cfad0cd41c60f72f85f9f2c1921f4d2646a436
data/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright 2021 Nate Pickens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
+ documentation files (the "Software"), to deal in the Software without restriction, including without
5
+ limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
6
+ the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
7
+ conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
13
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
14
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
15
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
16
+ OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Potluck - Nginx
2
+
3
+ An extension to the Potluck gem that provides control over the Nginx process and its configuration files
4
+ from Ruby.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your Gemfile:
9
+
10
+ ```ruby
11
+ gem('potluck-nginx')
12
+ ```
13
+
14
+ Or install manually on the command line:
15
+
16
+ ```bash
17
+ gem install potluck-nginx
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ [Coming soon.]
23
+
24
+ ## Contributing
25
+
26
+ Bug reports and pull requests are welcome on GitHub at https://github.com/npickens/potluck.
27
+
28
+ ## License
29
+
30
+ The gem is available as open source under the terms of the
31
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('fileutils')
4
+ require('potluck')
5
+ require_relative('nginx/ssl')
6
+ require_relative('nginx/util')
7
+
8
+ module Potluck
9
+ class Nginx < Dish
10
+ CONFIG_NAME_ACTIVE = 'nginx.conf'
11
+ CONFIG_NAME_INACTIVE = 'nginx-stopped.conf'
12
+ ACTIVE_CONFIG_PATTERN = File.join(DIR, '*', CONFIG_NAME_ACTIVE).freeze
13
+
14
+ TEST_CONFIG_REGEX = /nginx: configuration file (?<config>.+) test (failed|is successful)/.freeze
15
+ INCLUDE_REGEX = /^ *include +#{Regexp.escape(ACTIVE_CONFIG_PATTERN)} *;/.freeze
16
+
17
+ 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)
19
+ super(**args)
20
+
21
+ @hosts = Array(hosts).map { |h| h.sub(/^www\./, '') }.uniq
22
+ @hosts += @hosts.map { |h| "www.#{h}" }
23
+ @host = @hosts.first
24
+ @port = port
25
+
26
+ @dir = File.join(DIR, @host)
27
+ @ssl = SSL.new(self, @dir, @host, **ssl) if ssl
28
+
29
+ @scheme = @ssl ? 'https' : 'http'
30
+ @other_scheme = @ssl ? 'http' : 'https'
31
+ @one_host = !!one_host
32
+ @subdomains = Array(subdomains)
33
+ @www = www
34
+ @multiple_slashes = multiple_slashes
35
+ @multiple_question_marks = multiple_question_marks
36
+ @trailing_slash = trailing_slash
37
+ @trailing_question_mark = trailing_question_mark
38
+ @additional_config = config
39
+
40
+ FileUtils.mkdir_p(DIR)
41
+ FileUtils.mkdir_p(@dir)
42
+
43
+ @config_file_active = File.join(@dir, CONFIG_NAME_ACTIVE).freeze
44
+ @config_file_inactive = File.join(@dir, CONFIG_NAME_INACTIVE).freeze
45
+ end
46
+
47
+ def start
48
+ @ssl&.ensure_files
49
+ ensure_host_entries
50
+ ensure_include
51
+
52
+ write_config
53
+ activate_config
54
+
55
+ run('nginx -t')
56
+
57
+ status == :active ? reload : super
58
+ end
59
+
60
+ def stop(hard = false)
61
+ deactivate_config
62
+
63
+ hard || status != :active ? super() : reload
64
+ end
65
+
66
+ def reload
67
+ run('nginx -s reload')
68
+ end
69
+
70
+ private
71
+
72
+ def config
73
+ host_subdomains_regex = ([@host] + @subdomains).join('|')
74
+ hosts_subdomains_regex = (@hosts + @subdomains).join('|')
75
+
76
+ config = {
77
+ "upstream #{@host}" => {
78
+ 'server' => "127.0.0.1:#{@port}",
79
+ },
80
+
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(' '),
94
+
95
+ 'gzip' => 'on',
96
+ 'gzip_types' => 'application/javascript application/json text/css text/plain',
97
+
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' => {
158
+ 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',
164
+ },
165
+ },
166
+ ),
167
+ }
168
+
169
+ Util.deep_merge!(config['server'], @additional_config)
170
+
171
+ config
172
+ end
173
+
174
+ def write_config
175
+ File.open(@config_file_inactive, 'w') do |file|
176
+ file.write(self.class.to_nginx_config(config))
177
+ end
178
+ end
179
+
180
+ def activate_config
181
+ FileUtils.mv(@config_file_inactive, @config_file_active)
182
+ end
183
+
184
+ def deactivate_config
185
+ FileUtils.mv(@config_file_active, @config_file_inactive) if File.exists?(@config_file_active)
186
+ end
187
+
188
+ def ensure_host_entries
189
+ content = File.read('/etc/hosts')
190
+ missing_entries = (@hosts + @subdomains).each_with_object([]) do |h, a|
191
+ a << h unless content.include?(" #{h}\n")
192
+ end
193
+
194
+ return if missing_entries.empty?
195
+
196
+ log('Writing host entries to /etc/hosts...')
197
+
198
+ run(
199
+ <<~CMD
200
+ sudo sh -c 'printf "
201
+ #{missing_entries.map { |h| "127.0.0.1 #{h}\n::1 #{h}"}.join("\n")}
202
+ " >> /etc/hosts'
203
+ CMD
204
+ )
205
+ end
206
+
207
+ def ensure_include
208
+ config_file = `nginx -t 2>&1`[TEST_CONFIG_REGEX, :config]
209
+ config_content = File.read(config_file)
210
+
211
+ if config_content !~ INCLUDE_REGEX
212
+ File.write(config_file, config_content.sub(/^( *http *{)( *\n?)( *)/,
213
+ "\\1\\2\\3include #{ACTIVE_CONFIG_PATTERN};\n\n\\3"))
214
+ end
215
+ 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
+ end
255
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('time')
4
+
5
+ module Potluck
6
+ class Nginx < Dish
7
+ class SSL
8
+ # Based on https://hackernoon.com/how-properly-configure-nginx-server-for-tls-sg1d3udt
9
+ DEFAULT_CONFIG = {
10
+ 'ssl_ciphers' => 'ECDH+AESGCM:ECDH+AES256-CBC:ECDH+AES128-CBC:DH+3DES:!ADH:!AECDH:!MD5',
11
+ 'ssl_prefer_server_ciphers' => 'on',
12
+ 'ssl_protocols' => 'TLSv1.2 TLSv1.3',
13
+ 'ssl_session_cache' => 'shared:SSL:40m',
14
+ 'ssl_session_tickets' => 'on',
15
+ 'ssl_session_timeout' => '4h',
16
+ 'add_header' => {
17
+ repeat: true,
18
+ 'Strict-Transport-Security' => '\'max-age=31536000; includeSubDomains\' always',
19
+ }.freeze,
20
+ }.freeze
21
+
22
+ CERT_DAYS = 365
23
+ CERT_RENEW_DAYS = 14
24
+
25
+ attr_reader(:csr_file, :key_file, :crt_file, :dhparam_file, :config)
26
+
27
+ def initialize(nginx, dir, host, crt_file: nil, key_file: nil, dhparam_file: nil,
28
+ config: {})
29
+ @nginx = nginx
30
+ @dir = dir
31
+ @host = host
32
+
33
+ @auto_generated = !crt_file && !key_file && !dhparam_file
34
+
35
+ if !@auto_generated && (!crt_file || !key_file || !dhparam_file)
36
+ raise('Must supply values for all three or none: crt_file, key_file, dhparam_file')
37
+ end
38
+
39
+ @csr_file = File.join(@dir, "#{@host}.csr").freeze
40
+ @crt_file = crt_file || File.join(@dir, "#{@host}.crt").freeze
41
+ @key_file = key_file || File.join(@dir, "#{@host}.key").freeze
42
+ @dhparam_file = dhparam_file || File.join(@dir, 'dhparam.pem').freeze
43
+
44
+ @config = {
45
+ 'ssl_certificate' => @crt_file,
46
+ 'ssl_certificate_key' => @key_file,
47
+ 'ssl_dhparam' => @dhparam_file,
48
+ 'ssl_stapling' => ('on' unless @auto_generated),
49
+ 'ssl_stapling_verify' => ('on' unless @auto_generated),
50
+ }.merge!(DEFAULT_CONFIG).merge!(config)
51
+ end
52
+
53
+ def ensure_files
54
+ return if !@auto_generated || (
55
+ File.exists?(@csr_file) &&
56
+ File.exists?(@key_file) &&
57
+ File.exists?(@crt_file) &&
58
+ File.exists?(@dhparam_file) &&
59
+ (Time.parse(
60
+ @nginx.run("openssl x509 -enddate -noout -in #{@crt_file}").sub('notAfter=', '')
61
+ ) - Time.now) >= CERT_RENEW_DAYS * 24 * 60 * 60
62
+ )
63
+
64
+ @nginx.log('Generating SSL files...')
65
+
66
+ @nginx.run("openssl genrsa -out #{@key_file} 4096", redirect_stderr: false)
67
+ @nginx.run("openssl req -out #{@csr_file} -key #{@key_file} -new -sha256 -config /dev/stdin <<< "\
68
+ "'#{openssl_config}'", redirect_stderr: false)
69
+ @nginx.run("openssl x509 -in #{@csr_file} -out #{@crt_file} -signkey #{@key_file} -days "\
70
+ "#{CERT_DAYS} -req -sha256 -extensions req_ext -extfile /dev/stdin <<< '#{openssl_config}'",
71
+ redirect_stderr: false)
72
+ @nginx.run("openssl dhparam -out #{@dhparam_file} 2048", redirect_stderr: false)
73
+
74
+ if IS_MACOS
75
+ @nginx.log('Adding cert to keychain...')
76
+
77
+ @nginx.run(
78
+ "sudo security delete-certificate -t -c #{@host} 2>&1 || "\
79
+ "sudo security delete-certificate -c #{@host} 2>&1 || :"
80
+ )
81
+
82
+ @nginx.run("sudo security add-trusted-cert -d -r trustRoot -k "\
83
+ "/Library/Keychains/System.keychain #{@crt_file}")
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def openssl_config
90
+ <<~EOS
91
+ [ req ]
92
+ prompt = no
93
+ default_bits = 4096
94
+ distinguished_name = req_distinguished_name
95
+ req_extensions = req_ext
96
+
97
+ [ req_distinguished_name ]
98
+ commonName = #{@host}
99
+
100
+ [ req_ext ]
101
+ subjectAltName = @alt_names
102
+
103
+ [alt_names]
104
+ DNS.1 = #{@host}
105
+ DNS.2 = *.#{@host}
106
+ EOS
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Potluck
4
+ class Nginx
5
+ class Util
6
+ def self.deep_merge!(*hashes, arrays: false)
7
+ hash = hashes[0]
8
+
9
+ hashes[1..-1].each do |other_hash|
10
+ other_hash.each do |key, other_value|
11
+ this_value = hash[key]
12
+
13
+ if this_value.kind_of?(Hash) && other_value.kind_of?(Hash)
14
+ deep_merge!(this_value, other_value, arrays: arrays)
15
+ elsif arrays && this_value.kind_of?(Array)
16
+ hash[key] |= Array(other_value)
17
+ else
18
+ hash[key] = other_value
19
+ end
20
+ end
21
+ end
22
+
23
+ hash
24
+ end
25
+ end
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: potluck-nginx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nate Pickens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-03-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: potluck
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 5.11.2
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 6.0.0
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 5.11.2
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 6.0.0
61
+ description: An extension to the Potluck gem that provides control over the Nginx
62
+ process and its configuration files from Ruby.
63
+ email:
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - LICENSE
69
+ - README.md
70
+ - lib/potluck/nginx.rb
71
+ - lib/potluck/nginx/ssl.rb
72
+ - lib/potluck/nginx/util.rb
73
+ homepage: https://github.com/npickens/potluck/tree/master/potluck-nginx
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ allowed_push_host: https://rubygems.org
78
+ homepage_uri: https://github.com/npickens/potluck/tree/master/potluck-nginx
79
+ source_code_uri: https://github.com/npickens/potluck/tree/master/potluck-nginx
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.5.8
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.2.3
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: A Ruby manager for Nginx.
99
+ test_files: []