potluck-nginx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []