rails-threaded-proxy 0.2.0 → 0.4.0
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/.rubocop.yml +39 -0
- data/Gemfile +10 -9
- data/Gemfile.lock +24 -0
- data/LICENSE +1 -1
- data/README.md +46 -0
- data/Rakefile +10 -10
- data/VERSION +1 -1
- data/bin/rubocop +27 -0
- data/lib/rails-threaded-proxy.rb +3 -1
- data/lib/threaded-proxy.rb +3 -1
- data/lib/threaded_proxy/client.rb +80 -36
- data/lib/threaded_proxy/controller.rb +6 -6
- data/lib/threaded_proxy/http.rb +4 -1
- data/lib/threaded_proxy.rb +3 -3
- data/rails-threaded-proxy.gemspec +10 -5
- data/spec/spec_helper.rb +2 -0
- data/spec/threaded_proxy/client_spec.rb +13 -9
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2aae2ccc6678cd9afd9e3068a996bfe1d163f0569512a9e37b958c749aeb8096
|
4
|
+
data.tar.gz: 14079d5dfc8a7319c54469d859d2cb5e055ba4fae94d80cebda8cb9968bcd076
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb6a536989c9b554b79ce943ed143e65a1985b7d535dac729cc7b90f01961eda02325134201e6a027d34775cafde12aa084264cce5a9c59c4c88b9b9a31b147f
|
7
|
+
data.tar.gz: a4c6056afd3971cf18cd9d2ab5e2e3739b0c76a922c655024d1e34f00069b8cbe42f376c1611c040948798b767663e1a0eb7ad7ec6753af90b4e0fc3727808f9
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# The behavior of RuboCop can be controlled via the .rubocop.yml
|
2
|
+
# configuration file. It makes it possible to enable/disable
|
3
|
+
# certain cops (checks) and to alter their behavior if they accept
|
4
|
+
# any parameters. The file can be placed either in your home
|
5
|
+
# directory or in some project directory.
|
6
|
+
#
|
7
|
+
# RuboCop will start looking for the configuration file in the directory
|
8
|
+
# where the inspected file is and continue its way up to the root directory.
|
9
|
+
#
|
10
|
+
# See https://docs.rubocop.org/rubocop/configuration
|
11
|
+
|
12
|
+
AllCops:
|
13
|
+
Exclude:
|
14
|
+
- '*.gemspec'
|
15
|
+
- 'bin/*'
|
16
|
+
|
17
|
+
Metrics/BlockLength:
|
18
|
+
Exclude:
|
19
|
+
- 'spec/**/*.rb'
|
20
|
+
|
21
|
+
Metrics/PerceivedComplexity:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/MethodLength:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/AbcSize:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Metrics/CyclomaticComplexity:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Style/Documentation:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Naming/FileName:
|
37
|
+
Exclude:
|
38
|
+
- 'lib/rails-threaded-proxy.rb'
|
39
|
+
- 'lib/threaded-proxy.rb'
|
data/Gemfile
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
|
-
gem
|
6
|
-
gem
|
5
|
+
gem 'actionpack'
|
6
|
+
gem 'addressable'
|
7
7
|
|
8
8
|
group :development do
|
9
|
-
gem
|
10
|
-
gem
|
11
|
-
gem
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
9
|
+
gem 'bundler', '~> 2.0'
|
10
|
+
gem 'jeweler', '~> 2.3.9'
|
11
|
+
gem 'nokogiri', '>= 1.16.7'
|
12
|
+
gem 'rdoc', '~> 6.7.0'
|
13
|
+
gem 'rspec', '>= 0'
|
14
|
+
gem 'rubocop', '>= 0'
|
15
|
+
gem 'webrick', '>= 0'
|
15
16
|
end
|
data/Gemfile.lock
CHANGED
@@ -30,6 +30,7 @@ GEM
|
|
30
30
|
securerandom (>= 0.3)
|
31
31
|
tzinfo (~> 2.0, >= 2.0.5)
|
32
32
|
addressable (2.4.0)
|
33
|
+
ast (2.4.2)
|
33
34
|
base64 (0.2.0)
|
34
35
|
bigdecimal (3.1.8)
|
35
36
|
builder (3.3.0)
|
@@ -69,8 +70,10 @@ GEM
|
|
69
70
|
rake
|
70
71
|
rdoc
|
71
72
|
semver2
|
73
|
+
json (2.7.2)
|
72
74
|
jwt (2.9.3)
|
73
75
|
base64
|
76
|
+
language_server-protocol (3.17.0.3)
|
74
77
|
logger (1.6.1)
|
75
78
|
loofah (2.22.0)
|
76
79
|
crass (~> 1.0.2)
|
@@ -99,6 +102,10 @@ GEM
|
|
99
102
|
multi_json (~> 1.3)
|
100
103
|
multi_xml (~> 0.5)
|
101
104
|
rack (>= 1.2, < 3)
|
105
|
+
parallel (1.26.3)
|
106
|
+
parser (3.3.5.0)
|
107
|
+
ast (~> 2.4.1)
|
108
|
+
racc
|
102
109
|
psych (5.1.2)
|
103
110
|
stringio
|
104
111
|
racc (1.8.1)
|
@@ -114,10 +121,12 @@ GEM
|
|
114
121
|
rails-html-sanitizer (1.6.0)
|
115
122
|
loofah (~> 2.21)
|
116
123
|
nokogiri (~> 1.14)
|
124
|
+
rainbow (3.1.1)
|
117
125
|
rake (13.2.1)
|
118
126
|
rchardet (1.8.0)
|
119
127
|
rdoc (6.7.0)
|
120
128
|
psych (>= 4.0.0)
|
129
|
+
regexp_parser (2.9.2)
|
121
130
|
reline (0.5.10)
|
122
131
|
io-console (~> 0.5)
|
123
132
|
rspec (3.13.0)
|
@@ -133,12 +142,26 @@ GEM
|
|
133
142
|
diff-lcs (>= 1.2.0, < 2.0)
|
134
143
|
rspec-support (~> 3.13.0)
|
135
144
|
rspec-support (3.13.1)
|
145
|
+
rubocop (1.66.1)
|
146
|
+
json (~> 2.3)
|
147
|
+
language_server-protocol (>= 3.17.0)
|
148
|
+
parallel (~> 1.10)
|
149
|
+
parser (>= 3.3.0.2)
|
150
|
+
rainbow (>= 2.2.2, < 4.0)
|
151
|
+
regexp_parser (>= 2.4, < 3.0)
|
152
|
+
rubocop-ast (>= 1.32.2, < 2.0)
|
153
|
+
ruby-progressbar (~> 1.7)
|
154
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
155
|
+
rubocop-ast (1.32.3)
|
156
|
+
parser (>= 3.3.1.0)
|
157
|
+
ruby-progressbar (1.13.0)
|
136
158
|
securerandom (0.3.1)
|
137
159
|
semver2 (3.4.2)
|
138
160
|
stringio (3.1.1)
|
139
161
|
thread_safe (0.3.6)
|
140
162
|
tzinfo (2.0.6)
|
141
163
|
concurrent-ruby (~> 1.0)
|
164
|
+
unicode-display_width (2.6.0)
|
142
165
|
useragent (0.16.10)
|
143
166
|
webrick (1.8.2)
|
144
167
|
|
@@ -158,6 +181,7 @@ DEPENDENCIES
|
|
158
181
|
nokogiri (>= 1.16.7)
|
159
182
|
rdoc (~> 6.7.0)
|
160
183
|
rspec
|
184
|
+
rubocop
|
161
185
|
webrick
|
162
186
|
|
163
187
|
BUNDLED WITH
|
data/LICENSE
CHANGED
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# rails-threaded-proxy
|
2
|
+
|
3
|
+
Asynchronous high throughput reverse proxy for rails
|
4
|
+
|
5
|
+
*Warning: experimental. Use at your own risk.*
|
6
|
+
|
7
|
+
## About
|
8
|
+
|
9
|
+
Rails concurrency is often limited to running many processes, which can be memory-intensive. Even for servers that support threads, it can be difficult running dozens or hundreds of threads. But you may have backend services that are slow to respond, and/or return very large responses. It is useful to put these services behind rails for authentication, but slow responses can tie up your rails workers preventing them from serving other clients.
|
10
|
+
|
11
|
+
`rails-threaded-proxy` disconnects the proxying from the rack request/response cycle, freeing up workers to serve other clients. It does this by running the origin request in a thread. But running in a thread is not enough: we need to be able to respond to the rails request, but rack owns the socket. So it hijacks the request: rack completes immediately but dissociates from the socket. Then we're free to manage the socket ourselves. Copying between sockets, we can achieve high throughput (100MB/s+) with minimal CPU and memory overhead.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class MyController
|
17
|
+
include ThreadedProxy::Controller
|
18
|
+
|
19
|
+
def my_backend
|
20
|
+
proxy_fetch "http://backend.service/path/to/endpoint", method: :post do |config|
|
21
|
+
config.on_headers do |client_response|
|
22
|
+
# override some response headers coming from the backend
|
23
|
+
client_response['content-security-policy'] = "sandbox;"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Requirements
|
31
|
+
|
32
|
+
Tested with Rails 7, but probably works in Rails 6+. Needs an application server that supports `rack.hijack`. (only tested on [https://puma.io/](Puma) so far)
|
33
|
+
|
34
|
+
## Caveats
|
35
|
+
|
36
|
+
* There isn't currently a way to limit concurrency. It is possible to run your server out of file descriptors, memory, etc.
|
37
|
+
* Since the proxying happens in a thread, callbacks are also run inside of the thread. Don't do anything non-threadsafe in callbacks.
|
38
|
+
* There is currently probably not sufficient error handling for edge cases. This is experimental.
|
39
|
+
|
40
|
+
## Attribution
|
41
|
+
|
42
|
+
Inspired by [https://github.com/axsuul/rails-reverse-proxy](rails-reverse-proxy), and tries to use similar API structure where possible. If you don't care about the specific benefits of `rails-threaded-proxy`, you should consider using `rails-reverse-proxy` instead.
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
See LICENSE
|
data/Rakefile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'bundler'
|
5
5
|
begin
|
6
6
|
Bundler.setup(:default, :development)
|
7
7
|
rescue Bundler::BundlerError => e
|
8
|
-
|
9
|
-
|
8
|
+
warn e.message
|
9
|
+
warn 'Run `bundle install` to install missing gems'
|
10
10
|
exit e.status_code
|
11
11
|
end
|
12
12
|
require 'rake'
|
@@ -14,13 +14,13 @@ require 'rake'
|
|
14
14
|
require 'jeweler'
|
15
15
|
Jeweler::Tasks.new do |gem|
|
16
16
|
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
-
gem.name =
|
18
|
-
gem.homepage =
|
19
|
-
gem.license =
|
20
|
-
gem.summary = %
|
21
|
-
gem.description = %
|
22
|
-
gem.email =
|
23
|
-
gem.authors = [
|
17
|
+
gem.name = 'rails-threaded-proxy'
|
18
|
+
gem.homepage = 'http://github.com/mnutt/rails-threaded-proxy'
|
19
|
+
gem.license = 'MIT'
|
20
|
+
gem.summary = %(Threaded reverse proxy for Ruby on Rails)
|
21
|
+
gem.description = %(Threaded reverse proxy for Ruby on Rails)
|
22
|
+
gem.email = 'michael@nuttnet.net'
|
23
|
+
gem.authors = ['Michael Nutt']
|
24
24
|
# dependencies defined in Gemfile
|
25
25
|
end
|
26
26
|
Jeweler::RubygemsDotOrgTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/bin/rubocop
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
|
13
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
14
|
+
|
15
|
+
if File.file?(bundle_binstub)
|
16
|
+
if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
|
17
|
+
load(bundle_binstub)
|
18
|
+
else
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'bundler/setup'
|
26
|
+
|
27
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/lib/rails-threaded-proxy.rb
CHANGED
data/lib/threaded-proxy.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'addressable/uri'
|
4
|
+
require 'active_support/notifications'
|
2
5
|
require 'net/http'
|
3
6
|
require_relative 'http'
|
4
7
|
|
5
8
|
module ThreadedProxy
|
6
9
|
class Client
|
7
|
-
DISALLOWED_RESPONSE_HEADERS = %w[keep-alive]
|
10
|
+
DISALLOWED_RESPONSE_HEADERS = %w[keep-alive].freeze
|
8
11
|
|
9
|
-
|
12
|
+
HTTP_METHODS = {
|
10
13
|
'get' => Net::HTTP::Get,
|
11
14
|
'post' => Net::HTTP::Post,
|
12
15
|
'put' => Net::HTTP::Put,
|
@@ -14,64 +17,107 @@ module ThreadedProxy
|
|
14
17
|
'head' => Net::HTTP::Head,
|
15
18
|
'options' => Net::HTTP::Options,
|
16
19
|
'trace' => Net::HTTP::Trace
|
17
|
-
}
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
CALLBACK_METHODS = %i[
|
23
|
+
on_response
|
24
|
+
on_headers
|
25
|
+
on_body
|
26
|
+
on_complete
|
27
|
+
on_error
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
CALLBACK_METHODS.each do |method_name|
|
31
|
+
define_method(method_name) do |&block|
|
32
|
+
@callbacks[method_name] = block
|
33
|
+
end
|
34
|
+
end
|
18
35
|
|
19
36
|
DEFAULT_OPTIONS = {
|
20
37
|
headers: {},
|
21
|
-
debug: false
|
22
|
-
|
38
|
+
debug: false,
|
39
|
+
method: :get
|
40
|
+
}.freeze
|
23
41
|
|
24
|
-
def initialize(origin_url, options={})
|
42
|
+
def initialize(origin_url, options = {})
|
25
43
|
@origin_url = Addressable::URI.parse(origin_url)
|
26
44
|
@options = DEFAULT_OPTIONS.merge(options)
|
45
|
+
|
46
|
+
@callbacks = {}
|
47
|
+
CALLBACK_METHODS.each do |method_name|
|
48
|
+
@callbacks[method_name] = proc {}
|
49
|
+
end
|
50
|
+
|
51
|
+
yield(self) if block_given?
|
27
52
|
end
|
28
53
|
|
29
54
|
def log(message)
|
30
|
-
|
55
|
+
warn message if @options[:debug]
|
31
56
|
end
|
32
57
|
|
33
58
|
def start(socket)
|
34
|
-
request_method =
|
35
|
-
|
59
|
+
request_method = @options[:method].to_s.downcase
|
60
|
+
request_headers = @options[:headers].merge('Connection' => 'close')
|
61
|
+
|
62
|
+
request_class = HTTP_METHODS[request_method]
|
63
|
+
http_request = request_class.new(@origin_url, request_headers)
|
36
64
|
if @options[:body].respond_to?(:read)
|
37
65
|
http_request.body_stream = @options[:body]
|
38
|
-
elsif
|
66
|
+
elsif @options[:body].is_a?(String)
|
39
67
|
http_request.body = @options[:body]
|
40
68
|
end
|
41
69
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
70
|
+
ActiveSupport::Notifications.instrument('threaded_proxy.fetch', method: request_method, url: @origin_url.to_s,
|
71
|
+
headers: request_headers) do
|
72
|
+
http = HTTP.new(@origin_url.host, @origin_url.port || default_port(@origin_url))
|
73
|
+
http.use_ssl = (@origin_url.scheme == 'https')
|
74
|
+
http.set_debug_output($stderr) if @options[:debug]
|
75
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options[:ignore_ssl_errors]
|
46
76
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
77
|
+
http.start do
|
78
|
+
http.request(http_request) do |client_response|
|
79
|
+
@callbacks[:on_response].call(client_response, socket)
|
80
|
+
break if socket.closed?
|
51
81
|
|
52
|
-
|
82
|
+
log('Writing response status and headers')
|
83
|
+
write_headers(client_response, socket)
|
84
|
+
break if socket.closed?
|
53
85
|
|
54
|
-
|
55
|
-
|
56
|
-
socket.write "HTTP/1.1 #{client_response.code} #{client_response.message}\r\n"
|
86
|
+
@callbacks[:on_body].call(client_response, socket)
|
87
|
+
break if socket.closed?
|
57
88
|
|
58
|
-
|
59
|
-
|
89
|
+
# There may have been some existing data in client_response's read buffer, flush it out
|
90
|
+
# before we manually connect the raw sockets
|
91
|
+
log('Flushing existing response buffer to client')
|
92
|
+
http.flush_existing_buffer_to(socket)
|
93
|
+
|
94
|
+
# Copy the rest of the client response to the socket
|
95
|
+
log('Copying response body to client')
|
96
|
+
http.copy_to(socket)
|
97
|
+
|
98
|
+
@callbacks[:on_complete].call(client_response)
|
60
99
|
end
|
100
|
+
rescue StandardError => e
|
101
|
+
@callbacks[:on_error].call(e) or raise
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
61
105
|
|
62
|
-
|
63
|
-
|
106
|
+
def write_headers(client_response, socket)
|
107
|
+
socket.write "HTTP/1.1 #{client_response.code} #{client_response.message}\r\n"
|
64
108
|
|
65
|
-
|
66
|
-
|
67
|
-
log("Flushing existing response buffer to client")
|
68
|
-
http.flush_existing_buffer_to(socket)
|
109
|
+
# We don't support reusing connections once we have disconnected them from rack
|
110
|
+
client_response['connection'] = 'close'
|
69
111
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
112
|
+
@callbacks[:on_headers].call(client_response, socket)
|
113
|
+
return if socket.closed?
|
114
|
+
|
115
|
+
client_response.each_header do |key, value|
|
116
|
+
socket.write "#{key}: #{value}\r\n" unless DISALLOWED_RESPONSE_HEADERS.include?(key.downcase)
|
74
117
|
end
|
118
|
+
|
119
|
+
# Done with headers
|
120
|
+
socket.write "\r\n"
|
75
121
|
end
|
76
122
|
|
77
123
|
def default_port(uri)
|
@@ -80,8 +126,6 @@ module ThreadedProxy
|
|
80
126
|
80
|
81
127
|
when 'https'
|
82
128
|
443
|
83
|
-
else
|
84
|
-
nil
|
85
129
|
end
|
86
130
|
end
|
87
131
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'client'
|
2
4
|
|
3
5
|
module ThreadedProxy
|
4
6
|
module Controller
|
5
|
-
def proxy_fetch(origin_url, options={})
|
7
|
+
def proxy_fetch(origin_url, options = {}, &block)
|
6
8
|
# hijack the response so we can take it outside of the rack request/response cycle
|
7
9
|
request.env['rack.hijack'].call
|
8
10
|
socket = request.env['rack.hijack_io']
|
@@ -17,15 +19,13 @@ module ThreadedProxy
|
|
17
19
|
elsif request.env['CONTENT_LENGTH']
|
18
20
|
options[:headers]['content-length'] = request.env['CONTENT_LENGTH'].to_s
|
19
21
|
else
|
20
|
-
raise
|
22
|
+
raise 'Cannot proxy a non-chunked POST request without content-length'
|
21
23
|
end
|
22
24
|
|
23
|
-
if request.env['CONTENT_TYPE']
|
24
|
-
options[:headers]['Content-Type'] = request.env['CONTENT_TYPE']
|
25
|
-
end
|
25
|
+
options[:headers]['Content-Type'] = request.env['CONTENT_TYPE'] if request.env['CONTENT_TYPE']
|
26
26
|
end
|
27
27
|
|
28
|
-
client = Client.new(origin_url, options)
|
28
|
+
client = Client.new(origin_url, options, &block)
|
29
29
|
client.start(socket)
|
30
30
|
rescue Errno::EPIPE
|
31
31
|
# client disconnected before request finished; not an error
|
data/lib/threaded_proxy/http.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'net/http'
|
2
4
|
|
3
5
|
module ThreadedProxy
|
@@ -5,6 +7,7 @@ module ThreadedProxy
|
|
5
7
|
def flush_existing_buffer_to(dest_socket)
|
6
8
|
while (data = @socket.send(:rbuf_consume))
|
7
9
|
break if data.empty?
|
10
|
+
|
8
11
|
dest_socket.write data
|
9
12
|
end
|
10
13
|
|
@@ -27,7 +30,7 @@ module ThreadedProxy
|
|
27
30
|
|
28
31
|
# We read the response ourselves; don't need net/http to try to read it again
|
29
32
|
def hijack_response(res)
|
30
|
-
res.instance_variable_set(
|
33
|
+
res.instance_variable_set('@read', true)
|
31
34
|
res
|
32
35
|
end
|
33
36
|
end
|
data/lib/threaded_proxy.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'threaded_proxy/client'
|
2
4
|
require 'threaded_proxy/controller'
|
3
5
|
|
4
6
|
module ThreadedProxy
|
5
7
|
def version
|
6
|
-
File.open(File.expand_path(
|
8
|
+
File.open(File.expand_path('../VERSION', __dir__)).read.strip
|
7
9
|
end
|
8
|
-
|
9
|
-
extend self
|
10
10
|
end
|
@@ -2,29 +2,32 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: rails-threaded-proxy 0.
|
5
|
+
# stub: rails-threaded-proxy 0.4.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "rails-threaded-proxy".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.4.0".freeze
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Michael Nutt".freeze]
|
14
|
-
s.date = "2024-10-
|
14
|
+
s.date = "2024-10-15"
|
15
15
|
s.description = "Threaded reverse proxy for Ruby on Rails".freeze
|
16
16
|
s.email = "michael@nuttnet.net".freeze
|
17
|
-
s.executables = ["bundle".freeze, "htmldiff".freeze, "jeweler".freeze, "ldiff".freeze, "nokogiri".freeze, "racc".freeze, "rackup".freeze, "rake".freeze, "rdoc".freeze, "ri".freeze, "rspec".freeze, "semver".freeze]
|
17
|
+
s.executables = ["bundle".freeze, "htmldiff".freeze, "jeweler".freeze, "ldiff".freeze, "nokogiri".freeze, "racc".freeze, "rackup".freeze, "rake".freeze, "rdoc".freeze, "ri".freeze, "rspec".freeze, "rubocop".freeze, "semver".freeze]
|
18
18
|
s.extra_rdoc_files = [
|
19
|
-
"LICENSE"
|
19
|
+
"LICENSE",
|
20
|
+
"README.md"
|
20
21
|
]
|
21
22
|
s.files = [
|
22
23
|
".bundle/config",
|
23
24
|
".rspec",
|
25
|
+
".rubocop.yml",
|
24
26
|
".ruby-version",
|
25
27
|
"Gemfile",
|
26
28
|
"Gemfile.lock",
|
27
29
|
"LICENSE",
|
30
|
+
"README.md",
|
28
31
|
"Rakefile",
|
29
32
|
"VERSION",
|
30
33
|
"bin/bundle",
|
@@ -38,6 +41,7 @@ Gem::Specification.new do |s|
|
|
38
41
|
"bin/rdoc",
|
39
42
|
"bin/ri",
|
40
43
|
"bin/rspec",
|
44
|
+
"bin/rubocop",
|
41
45
|
"bin/semver",
|
42
46
|
"lib/rails-threaded-proxy.rb",
|
43
47
|
"lib/threaded-proxy.rb",
|
@@ -63,6 +67,7 @@ Gem::Specification.new do |s|
|
|
63
67
|
s.add_development_dependency(%q<nokogiri>.freeze, [">= 1.16.7".freeze])
|
64
68
|
s.add_development_dependency(%q<rdoc>.freeze, ["~> 6.7.0".freeze])
|
65
69
|
s.add_development_dependency(%q<rspec>.freeze, [">= 0".freeze])
|
70
|
+
s.add_development_dependency(%q<rubocop>.freeze, [">= 0".freeze])
|
66
71
|
s.add_development_dependency(%q<webrick>.freeze, [">= 0".freeze])
|
67
72
|
end
|
68
73
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,20 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rails-threaded-proxy'
|
2
4
|
require 'json'
|
3
5
|
|
4
|
-
|
5
|
-
BACKEND_PORT = 38293
|
6
|
+
BACKEND_STUB_PORT = 38_293
|
6
7
|
|
8
|
+
RSpec.describe ThreadedProxy::Client do
|
7
9
|
before(:all) do
|
8
|
-
@backend_server = WEBrick::HTTPServer.new(Port:
|
9
|
-
Logger: WEBrick::Log.new(
|
10
|
+
@backend_server = WEBrick::HTTPServer.new(Port: BACKEND_STUB_PORT,
|
11
|
+
Logger: WEBrick::Log.new('/dev/null'),
|
10
12
|
AccessLog: [])
|
11
13
|
@backend_server.mount_proc '/get' do |req, res|
|
12
14
|
raise unless req.request_method == 'GET'
|
15
|
+
|
13
16
|
res.body = "Received request: #{req.path}"
|
14
17
|
end
|
15
18
|
|
16
19
|
@backend_server.mount_proc '/post' do |req, res|
|
17
20
|
raise unless req.request_method == 'POST'
|
21
|
+
|
18
22
|
res.content_type = 'application/json'
|
19
23
|
res.body = JSON.generate(path: req.path,
|
20
24
|
headers: req.header,
|
@@ -29,19 +33,19 @@ RSpec.describe ThreadedProxy::Client do
|
|
29
33
|
@server_thread.kill
|
30
34
|
end
|
31
35
|
|
32
|
-
it
|
36
|
+
it 'proxies a GET request' do
|
33
37
|
socket = StringIO.new
|
34
38
|
|
35
|
-
client = ThreadedProxy::Client.new("http://localhost:#{
|
39
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get")
|
36
40
|
client.start(socket)
|
37
41
|
|
38
|
-
expect(socket.string).to include(
|
42
|
+
expect(socket.string).to include('Received request: /get')
|
39
43
|
end
|
40
44
|
|
41
|
-
it
|
45
|
+
it 'proxies a POST request with content-length' do
|
42
46
|
socket = StringIO.new
|
43
47
|
|
44
|
-
client = ThreadedProxy::Client.new("http://localhost:#{
|
48
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/post",
|
45
49
|
method: 'post',
|
46
50
|
body: 'hello world')
|
47
51
|
client.start(socket)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-threaded-proxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Nutt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
126
|
name: webrick
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -136,17 +150,21 @@ executables:
|
|
136
150
|
- rdoc
|
137
151
|
- ri
|
138
152
|
- rspec
|
153
|
+
- rubocop
|
139
154
|
- semver
|
140
155
|
extensions: []
|
141
156
|
extra_rdoc_files:
|
142
157
|
- LICENSE
|
158
|
+
- README.md
|
143
159
|
files:
|
144
160
|
- ".bundle/config"
|
145
161
|
- ".rspec"
|
162
|
+
- ".rubocop.yml"
|
146
163
|
- ".ruby-version"
|
147
164
|
- Gemfile
|
148
165
|
- Gemfile.lock
|
149
166
|
- LICENSE
|
167
|
+
- README.md
|
150
168
|
- Rakefile
|
151
169
|
- VERSION
|
152
170
|
- bin/bundle
|
@@ -160,6 +178,7 @@ files:
|
|
160
178
|
- bin/rdoc
|
161
179
|
- bin/ri
|
162
180
|
- bin/rspec
|
181
|
+
- bin/rubocop
|
163
182
|
- bin/semver
|
164
183
|
- lib/rails-threaded-proxy.rb
|
165
184
|
- lib/threaded-proxy.rb
|