rails-threaded-proxy 0.4.0 → 0.5.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/VERSION +1 -1
- data/lib/threaded_proxy/client.rb +19 -7
- data/lib/threaded_proxy/controller.rb +39 -17
- data/lib/threaded_proxy/http.rb +20 -2
- data/lib/threaded_proxy/socket_responder.rb +65 -0
- data/rails-threaded-proxy.gemspec +6 -18
- data/spec/threaded_proxy/client_spec.rb +197 -4
- data/spec/threaded_proxy/controller_spec.rb +50 -0
- metadata +5 -29
- data/bin/bundle +0 -109
- data/bin/htmldiff +0 -27
- data/bin/jeweler +0 -27
- data/bin/ldiff +0 -27
- data/bin/nokogiri +0 -27
- data/bin/racc +0 -27
- data/bin/rackup +0 -27
- data/bin/rake +0 -27
- data/bin/rdoc +0 -27
- data/bin/ri +0 -27
- data/bin/rspec +0 -27
- data/bin/rubocop +0 -27
- data/bin/semver +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d77c14092a49dc2e231f233da21a934600a5159aabbc6bbb40294404c8e49d7
|
4
|
+
data.tar.gz: 8590d4359939c617098a036435c340623de538fcb5a65bbd15fe1b72726be86b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7704567ef587afce79dbf21ce20f67add2e04c87cbc1efb7fbb3491cc7a95e01e05a8b7881917a64073caca7f4769c07814d22038f06fce045aa80b07f00b501
|
7
|
+
data.tar.gz: 025e8d6eda736b0ffa5c8f8e97d47941f64b06081b4a3a2164c6288a337b50d7325d64971c89fab35622f9c383dad0941584f1e1e4bf80d2a9c99bc4be20501b
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
@@ -2,10 +2,15 @@
|
|
2
2
|
|
3
3
|
require 'addressable/uri'
|
4
4
|
require 'active_support/notifications'
|
5
|
+
require 'action_dispatch'
|
5
6
|
require 'net/http'
|
7
|
+
|
6
8
|
require_relative 'http'
|
9
|
+
require_relative 'socket_responder'
|
7
10
|
|
8
11
|
module ThreadedProxy
|
12
|
+
class ResponseBodyAlreadyConsumedError < StandardError; end
|
13
|
+
|
9
14
|
class Client
|
10
15
|
DISALLOWED_RESPONSE_HEADERS = %w[keep-alive].freeze
|
11
16
|
|
@@ -22,7 +27,6 @@ module ThreadedProxy
|
|
22
27
|
CALLBACK_METHODS = %i[
|
23
28
|
on_response
|
24
29
|
on_headers
|
25
|
-
on_body
|
26
30
|
on_complete
|
27
31
|
on_error
|
28
32
|
].freeze
|
@@ -42,11 +46,13 @@ module ThreadedProxy
|
|
42
46
|
def initialize(origin_url, options = {})
|
43
47
|
@origin_url = Addressable::URI.parse(origin_url)
|
44
48
|
@options = DEFAULT_OPTIONS.merge(options)
|
49
|
+
@wrote_headers = false
|
45
50
|
|
46
51
|
@callbacks = {}
|
47
|
-
CALLBACK_METHODS.each do |method_name|
|
52
|
+
(CALLBACK_METHODS - [:on_error]).each do |method_name|
|
48
53
|
@callbacks[method_name] = proc {}
|
49
54
|
end
|
55
|
+
@callbacks[:on_error] = proc { |e| raise e }
|
50
56
|
|
51
57
|
yield(self) if block_given?
|
52
58
|
end
|
@@ -67,6 +73,8 @@ module ThreadedProxy
|
|
67
73
|
http_request.body = @options[:body]
|
68
74
|
end
|
69
75
|
|
76
|
+
socket_responder = SocketResponder.new(socket)
|
77
|
+
|
70
78
|
ActiveSupport::Notifications.instrument('threaded_proxy.fetch', method: request_method, url: @origin_url.to_s,
|
71
79
|
headers: request_headers) do
|
72
80
|
http = HTTP.new(@origin_url.host, @origin_url.port || default_port(@origin_url))
|
@@ -76,15 +84,14 @@ module ThreadedProxy
|
|
76
84
|
|
77
85
|
http.start do
|
78
86
|
http.request(http_request) do |client_response|
|
79
|
-
@callbacks[:on_response].call(client_response,
|
87
|
+
@callbacks[:on_response].call(client_response, socket_responder)
|
80
88
|
break if socket.closed?
|
81
89
|
|
82
90
|
log('Writing response status and headers')
|
83
91
|
write_headers(client_response, socket)
|
84
92
|
break if socket.closed?
|
85
93
|
|
86
|
-
|
87
|
-
break if socket.closed?
|
94
|
+
raise ResponseBodyAlreadyConsumedError if client_response.read?
|
88
95
|
|
89
96
|
# There may have been some existing data in client_response's read buffer, flush it out
|
90
97
|
# before we manually connect the raw sockets
|
@@ -97,9 +104,13 @@ module ThreadedProxy
|
|
97
104
|
|
98
105
|
@callbacks[:on_complete].call(client_response)
|
99
106
|
end
|
100
|
-
rescue StandardError => e
|
101
|
-
@callbacks[:on_error].call(e) or raise
|
102
107
|
end
|
108
|
+
rescue StandardError => e
|
109
|
+
@callbacks[:on_error].call(e, socket_responder)
|
110
|
+
# Default to 500 if the error callback didn't write a response
|
111
|
+
socket_responder.render(status: 500, text: 'Internal Server Error') unless socket.closed? || @wrote_headers
|
112
|
+
|
113
|
+
socket.close unless socket.closed?
|
103
114
|
end
|
104
115
|
end
|
105
116
|
|
@@ -118,6 +129,7 @@ module ThreadedProxy
|
|
118
129
|
|
119
130
|
# Done with headers
|
120
131
|
socket.write "\r\n"
|
132
|
+
@wrote_headers = true
|
121
133
|
end
|
122
134
|
|
123
135
|
def default_port(uri)
|
@@ -4,36 +4,58 @@ require_relative 'client'
|
|
4
4
|
|
5
5
|
module ThreadedProxy
|
6
6
|
module Controller
|
7
|
+
# Proxies a fetch request to the specified origin URL, allowing for hijacking
|
8
|
+
# the controller response outside of the Rack request/response cycle.
|
9
|
+
#
|
10
|
+
# @param origin_url [String] The URL to which the request will be proxied.
|
11
|
+
# @param options [Hash] Optional parameters for the request.
|
12
|
+
# @option options [Symbol] :body The body of the request. If set to :rack, the request body stream will be used.
|
13
|
+
# @option options [Hash] :headers Additional headers to include in the request.
|
14
|
+
# @yield [Client] Optional block to configure the client.
|
15
|
+
#
|
16
|
+
# @raise [RuntimeError] If a non-chunked POST request is made without a content-length header.
|
17
|
+
#
|
18
|
+
# @return [void]
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# proxy_fetch('http://example.com', body: :rack, headers: { 'Custom-Header' => 'value' }) do |client|
|
22
|
+
# client.on_headers { |client_response| client_response['x-foo'] = 'bar' }
|
23
|
+
# client.on_error { |e| Rails.logger.error(e) }
|
24
|
+
# end
|
7
25
|
def proxy_fetch(origin_url, options = {}, &block)
|
8
26
|
# hijack the response so we can take it outside of the rack request/response cycle
|
9
27
|
request.env['rack.hijack'].call
|
10
28
|
socket = request.env['rack.hijack_io']
|
11
29
|
|
12
|
-
|
13
|
-
if options[:body] == :rack
|
14
|
-
options[:headers] ||= {}
|
15
|
-
options[:body] = request.body_stream
|
16
|
-
|
17
|
-
if request.env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
18
|
-
options[:headers]['Transfer-Encoding'] = 'chunked'
|
19
|
-
elsif request.env['CONTENT_LENGTH']
|
20
|
-
options[:headers]['content-length'] = request.env['CONTENT_LENGTH'].to_s
|
21
|
-
else
|
22
|
-
raise 'Cannot proxy a non-chunked POST request without content-length'
|
23
|
-
end
|
24
|
-
|
25
|
-
options[:headers]['Content-Type'] = request.env['CONTENT_TYPE'] if request.env['CONTENT_TYPE']
|
26
|
-
end
|
30
|
+
options.deep_merge!(proxy_options_from_request) if options[:body] == :rack
|
27
31
|
|
32
|
+
Thread.new do
|
28
33
|
client = Client.new(origin_url, options, &block)
|
29
34
|
client.start(socket)
|
30
|
-
rescue Errno::EPIPE
|
31
|
-
# client disconnected before request finished; not an error
|
32
35
|
ensure
|
33
36
|
socket.close unless socket.closed?
|
34
37
|
end
|
35
38
|
|
36
39
|
head :ok
|
37
40
|
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def proxy_options_from_request
|
45
|
+
options = {}
|
46
|
+
options[:headers] ||= {}
|
47
|
+
options[:body] = request.body_stream
|
48
|
+
|
49
|
+
if request.env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
50
|
+
options[:headers]['Transfer-Encoding'] = 'chunked'
|
51
|
+
elsif request.env['CONTENT_LENGTH']
|
52
|
+
options[:headers]['content-length'] = request.env['CONTENT_LENGTH'].to_s
|
53
|
+
else
|
54
|
+
raise 'Cannot proxy a non-chunked POST request without content-length'
|
55
|
+
end
|
56
|
+
|
57
|
+
options[:headers]['Content-Type'] = request.env['CONTENT_TYPE'] if request.env['CONTENT_TYPE']
|
58
|
+
options
|
59
|
+
end
|
38
60
|
end
|
39
61
|
end
|
data/lib/threaded_proxy/http.rb
CHANGED
@@ -20,7 +20,14 @@ module ThreadedProxy
|
|
20
20
|
|
21
21
|
def request(*args)
|
22
22
|
if block_given?
|
23
|
-
super
|
23
|
+
super do |res|
|
24
|
+
access_read(res)
|
25
|
+
yield(res).tap do
|
26
|
+
# In the block case, the response is hijacked _after_ the block is called
|
27
|
+
# to allow the block to read the response body if it wants
|
28
|
+
hijack_response(res)
|
29
|
+
end
|
30
|
+
end
|
24
31
|
else
|
25
32
|
hijack_response(super)
|
26
33
|
end
|
@@ -30,8 +37,19 @@ module ThreadedProxy
|
|
30
37
|
|
31
38
|
# We read the response ourselves; don't need net/http to try to read it again
|
32
39
|
def hijack_response(res)
|
33
|
-
res.
|
40
|
+
access_read(res) unless res.respond_to?(:read?)
|
41
|
+
res.read = true
|
34
42
|
res
|
35
43
|
end
|
44
|
+
|
45
|
+
def access_read(res)
|
46
|
+
res.singleton_class.class_eval do
|
47
|
+
attr_writer :read
|
48
|
+
|
49
|
+
def read?
|
50
|
+
@read
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
36
54
|
end
|
37
55
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ThreadedProxy
|
2
|
+
class SocketResponder
|
3
|
+
def initialize(socket)
|
4
|
+
@socket = socket
|
5
|
+
end
|
6
|
+
|
7
|
+
def render(options = {})
|
8
|
+
return false if @socket.closed?
|
9
|
+
|
10
|
+
status = options[:status] || 200
|
11
|
+
headers = options[:headers] || {}
|
12
|
+
body = options[:body]
|
13
|
+
json = options[:json]
|
14
|
+
text = options[:text]
|
15
|
+
|
16
|
+
if json
|
17
|
+
body = json.to_json
|
18
|
+
headers['Content-Type'] ||= 'application/json; charset=utf-8'
|
19
|
+
elsif text
|
20
|
+
body = text
|
21
|
+
headers['Content-Type'] ||= 'text/plain; charset=utf-8'
|
22
|
+
else
|
23
|
+
body ||= ''
|
24
|
+
end
|
25
|
+
|
26
|
+
response = ActionDispatch::Response.new(status, headers, [])
|
27
|
+
response.prepare!
|
28
|
+
|
29
|
+
# Build the HTTP response
|
30
|
+
response_str = "HTTP/1.1 #{response.status} #{response.message}\r\n"
|
31
|
+
response.headers.each do |key, value|
|
32
|
+
Array(value).each do |v|
|
33
|
+
response_str += "#{key}: #{v}\r\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
response_str += "\r\n"
|
37
|
+
|
38
|
+
write(response_str)
|
39
|
+
|
40
|
+
if body.respond_to?(:read)
|
41
|
+
IO.copy_stream(body, @socket)
|
42
|
+
else
|
43
|
+
write(body)
|
44
|
+
end
|
45
|
+
|
46
|
+
close
|
47
|
+
end
|
48
|
+
|
49
|
+
def redirect_to(url)
|
50
|
+
render(status: 302, headers: { 'Location' => url })
|
51
|
+
end
|
52
|
+
|
53
|
+
def write(data)
|
54
|
+
@socket.write(data) unless @socket.closed?
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
@socket.close unless @socket.closed?
|
59
|
+
end
|
60
|
+
|
61
|
+
def closed?
|
62
|
+
@socket.closed?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -2,19 +2,18 @@
|
|
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.5.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.5.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-18"
|
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, "rubocop".freeze, "semver".freeze]
|
18
17
|
s.extra_rdoc_files = [
|
19
18
|
"LICENSE",
|
20
19
|
"README.md"
|
@@ -30,28 +29,17 @@ Gem::Specification.new do |s|
|
|
30
29
|
"README.md",
|
31
30
|
"Rakefile",
|
32
31
|
"VERSION",
|
33
|
-
"bin/bundle",
|
34
|
-
"bin/htmldiff",
|
35
|
-
"bin/jeweler",
|
36
|
-
"bin/ldiff",
|
37
|
-
"bin/nokogiri",
|
38
|
-
"bin/racc",
|
39
|
-
"bin/rackup",
|
40
|
-
"bin/rake",
|
41
|
-
"bin/rdoc",
|
42
|
-
"bin/ri",
|
43
|
-
"bin/rspec",
|
44
|
-
"bin/rubocop",
|
45
|
-
"bin/semver",
|
46
32
|
"lib/rails-threaded-proxy.rb",
|
47
33
|
"lib/threaded-proxy.rb",
|
48
34
|
"lib/threaded_proxy.rb",
|
49
35
|
"lib/threaded_proxy/client.rb",
|
50
36
|
"lib/threaded_proxy/controller.rb",
|
51
37
|
"lib/threaded_proxy/http.rb",
|
38
|
+
"lib/threaded_proxy/socket_responder.rb",
|
52
39
|
"rails-threaded-proxy.gemspec",
|
53
40
|
"spec/spec_helper.rb",
|
54
|
-
"spec/threaded_proxy/client_spec.rb"
|
41
|
+
"spec/threaded_proxy/client_spec.rb",
|
42
|
+
"spec/threaded_proxy/controller_spec.rb"
|
55
43
|
]
|
56
44
|
s.homepage = "http://github.com/mnutt/rails-threaded-proxy".freeze
|
57
45
|
s.licenses = ["MIT".freeze]
|
@@ -5,6 +5,15 @@ require 'json'
|
|
5
5
|
|
6
6
|
BACKEND_STUB_PORT = 38_293
|
7
7
|
|
8
|
+
def parse_raw_response(raw_response)
|
9
|
+
status, rest = raw_response.split("\r\n", 2)
|
10
|
+
headers, body = rest.split("\r\n\r\n", 2)
|
11
|
+
|
12
|
+
parsed_headers = headers.split("\r\n").map { |h| h.split(': ', 2) }.to_h
|
13
|
+
|
14
|
+
[status, parsed_headers, body]
|
15
|
+
end
|
16
|
+
|
8
17
|
RSpec.describe ThreadedProxy::Client do
|
9
18
|
before(:all) do
|
10
19
|
@backend_server = WEBrick::HTTPServer.new(Port: BACKEND_STUB_PORT,
|
@@ -50,15 +59,199 @@ RSpec.describe ThreadedProxy::Client do
|
|
50
59
|
body: 'hello world')
|
51
60
|
client.start(socket)
|
52
61
|
|
53
|
-
status,
|
54
|
-
headers, body = rest.split("\r\n\r\n", 2)
|
62
|
+
status, headers, body = parse_raw_response(socket.string)
|
55
63
|
|
56
64
|
parsed_body = JSON.parse(body)
|
57
|
-
parsed_headers = headers.split("\r\n").map { |h| h.split(': ', 2) }.to_h
|
58
65
|
|
59
66
|
expect(status).to eq('HTTP/1.1 200 OK')
|
60
|
-
expect(
|
67
|
+
expect(headers['content-type']).to eq('application/json')
|
61
68
|
expect(parsed_body['path']).to eq('/post')
|
62
69
|
expect(parsed_body['headers']['content-length']).to eq(['11'])
|
63
70
|
end
|
71
|
+
|
72
|
+
describe 'callbacks' do
|
73
|
+
describe 'on_headers' do
|
74
|
+
it 'proxies a request and modifies the response headers' do
|
75
|
+
socket = StringIO.new
|
76
|
+
|
77
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
78
|
+
config.on_headers do |response|
|
79
|
+
response['X-Test'] = 'test'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
client.start(socket)
|
83
|
+
|
84
|
+
status, headers, body = parse_raw_response(socket.string)
|
85
|
+
|
86
|
+
expect(status).to eq('HTTP/1.1 200 OK')
|
87
|
+
expect(headers['x-test']).to eq('test')
|
88
|
+
expect(headers['connection']).to eq('close')
|
89
|
+
expect(body).to eq('Received request: /get')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'on_complete' do
|
94
|
+
it 'fires when the request is successful' do
|
95
|
+
socket = StringIO.new
|
96
|
+
received_client_response = nil
|
97
|
+
|
98
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
99
|
+
config.on_complete do |client_response|
|
100
|
+
received_client_response = client_response
|
101
|
+
end
|
102
|
+
end
|
103
|
+
client.start(socket)
|
104
|
+
|
105
|
+
expect(received_client_response.code).to eq('200')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'on_error' do
|
110
|
+
it 'fires when the request is unsuccessful' do
|
111
|
+
socket = StringIO.new
|
112
|
+
received_error = nil
|
113
|
+
|
114
|
+
client = ThreadedProxy::Client.new('http://localhost:9999') do |config|
|
115
|
+
config.on_error do |e|
|
116
|
+
received_error = e
|
117
|
+
end
|
118
|
+
end
|
119
|
+
client.start(socket)
|
120
|
+
|
121
|
+
expect(received_error).to be_a_kind_of(Errno::ECONNREFUSED)
|
122
|
+
|
123
|
+
status, headers, body = parse_raw_response(socket.string)
|
124
|
+
expect(status).to eq('HTTP/1.1 500 Internal Server Error')
|
125
|
+
expect(headers['Content-Type']).to eq('text/plain; charset=utf-8')
|
126
|
+
expect(body).to eq('Internal Server Error')
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'returns custom response on error' do
|
130
|
+
socket = StringIO.new
|
131
|
+
received_error = nil
|
132
|
+
|
133
|
+
client = ThreadedProxy::Client.new('http://localhost:9999') do |config|
|
134
|
+
config.on_error do |e, response|
|
135
|
+
response.render status: 404, text: 'Custom error'
|
136
|
+
received_error = e
|
137
|
+
end
|
138
|
+
end
|
139
|
+
client.start(socket)
|
140
|
+
|
141
|
+
status, headers, body = parse_raw_response(socket.string)
|
142
|
+
expect(status).to eq('HTTP/1.1 404 Not Found')
|
143
|
+
expect(headers['Content-Type']).to eq('text/plain; charset=utf-8')
|
144
|
+
expect(body).to eq('Custom error')
|
145
|
+
expect(received_error).to be_a_kind_of(Errno::ECONNREFUSED)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe 'on_response' do
|
150
|
+
it 'proxies a request and lets caller send response' do
|
151
|
+
socket = StringIO.new
|
152
|
+
|
153
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
154
|
+
config.on_response do |client_response, response|
|
155
|
+
response.render status: 200, json: { body: client_response.body }, headers: { 'x-passed': 'yes' }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
client.start(socket)
|
159
|
+
|
160
|
+
status, headers, body = parse_raw_response(socket.string)
|
161
|
+
|
162
|
+
parsed_body = JSON.parse(body)
|
163
|
+
|
164
|
+
expect(status).to eq('HTTP/1.1 200 OK')
|
165
|
+
expect(headers['Content-Type']).to eq('application/json; charset=utf-8')
|
166
|
+
expect(headers['x-passed']).to eq('yes')
|
167
|
+
expect(parsed_body['body']).to eq('Received request: /get')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'accepts IO objects as the body' do
|
171
|
+
socket = StringIO.new
|
172
|
+
|
173
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
174
|
+
config.on_response do |_client_response, response|
|
175
|
+
response.render status: 200, body: StringIO.new('this is IO')
|
176
|
+
end
|
177
|
+
end
|
178
|
+
client.start(socket)
|
179
|
+
|
180
|
+
status, _headers, body = parse_raw_response(socket.string)
|
181
|
+
expect(status).to eq('HTTP/1.1 200 OK')
|
182
|
+
expect(body).to eq('this is IO')
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'accepts json body' do
|
186
|
+
socket = StringIO.new
|
187
|
+
|
188
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
189
|
+
config.on_response do |_client_response, response|
|
190
|
+
response.render status: 200, json: { key: 'value' }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
client.start(socket)
|
194
|
+
|
195
|
+
status, headers, body = parse_raw_response(socket.string)
|
196
|
+
|
197
|
+
parsed_body = JSON.parse(body)
|
198
|
+
|
199
|
+
expect(status).to eq('HTTP/1.1 200 OK')
|
200
|
+
expect(headers['Content-Type']).to eq('application/json; charset=utf-8')
|
201
|
+
expect(parsed_body['key']).to eq('value')
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'redirects to a URL' do
|
205
|
+
socket = StringIO.new
|
206
|
+
|
207
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
208
|
+
config.on_response do |_client_response, response|
|
209
|
+
response.redirect_to('http://example.com')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
client.start(socket)
|
213
|
+
|
214
|
+
status, headers, _body = parse_raw_response(socket.string)
|
215
|
+
|
216
|
+
expect(status).to eq('HTTP/1.1 302 Found')
|
217
|
+
expect(headers['Location']).to eq('http://example.com')
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'handles errors in on_response' do
|
221
|
+
socket = StringIO.new
|
222
|
+
received_error = nil
|
223
|
+
|
224
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
225
|
+
config.on_response do |_client_response, _response|
|
226
|
+
raise 'error in on_response'
|
227
|
+
end
|
228
|
+
|
229
|
+
config.on_error do |e|
|
230
|
+
received_error = e
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
client.start(socket)
|
235
|
+
|
236
|
+
status, headers, body = parse_raw_response(socket.string)
|
237
|
+
|
238
|
+
expect(status).to eq('HTTP/1.1 500 Internal Server Error')
|
239
|
+
expect(headers['Content-Type']).to eq('text/plain; charset=utf-8')
|
240
|
+
expect(body).to eq('Internal Server Error')
|
241
|
+
expect(received_error.message).to eq('error in on_response')
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'errors if on_response reads the body but does not render a response' do
|
245
|
+
socket = StringIO.new
|
246
|
+
|
247
|
+
client = ThreadedProxy::Client.new("http://localhost:#{BACKEND_STUB_PORT}/get") do |config|
|
248
|
+
config.on_response do |client_response, _response|
|
249
|
+
client_response.body
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
expect { client.start(socket) }.to raise_error(ThreadedProxy::ResponseBodyAlreadyConsumedError)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
64
257
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails-threaded-proxy'
|
4
|
+
|
5
|
+
class TestController
|
6
|
+
include ThreadedProxy::Controller
|
7
|
+
|
8
|
+
attr_accessor :request
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec.describe ThreadedProxy::Controller do
|
12
|
+
let(:request) { double(env: {}) }
|
13
|
+
let(:controller) do
|
14
|
+
TestController.new.tap do |controller|
|
15
|
+
controller.request = request
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#proxy_options_from_request' do
|
20
|
+
subject { controller.send(:proxy_options_from_request) }
|
21
|
+
let(:body_stream) { StringIO.new('HELLO') }
|
22
|
+
|
23
|
+
describe 'when the request is chunked' do
|
24
|
+
let(:request) { double(body_stream:, env: { 'HTTP_TRANSFER_ENCODING' => 'chunked' }) }
|
25
|
+
|
26
|
+
it 'sets the Transfer-Encoding header' do
|
27
|
+
expect(subject).to include(headers: { 'Transfer-Encoding' => 'chunked' },
|
28
|
+
body: body_stream)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'when the request is not chunked' do
|
33
|
+
let(:request) { double(body_stream:, env: { 'CONTENT_LENGTH' => '5', 'CONTENT_TYPE' => 'application/json' }) }
|
34
|
+
|
35
|
+
it 'sets the Content-Length header' do
|
36
|
+
expect(subject).to include(headers: { 'content-length' => '5',
|
37
|
+
'Content-Type' => 'application/json' },
|
38
|
+
body: body_stream)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'when the request is not chunked and has no content-length' do
|
43
|
+
let(:request) { double(body_stream:, env: {}) }
|
44
|
+
|
45
|
+
it 'raises an error' do
|
46
|
+
expect { subject }.to raise_error('Cannot proxy a non-chunked POST request without content-length')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
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.5.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-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -138,20 +138,7 @@ dependencies:
|
|
138
138
|
version: '0'
|
139
139
|
description: Threaded reverse proxy for Ruby on Rails
|
140
140
|
email: michael@nuttnet.net
|
141
|
-
executables:
|
142
|
-
- bundle
|
143
|
-
- htmldiff
|
144
|
-
- jeweler
|
145
|
-
- ldiff
|
146
|
-
- nokogiri
|
147
|
-
- racc
|
148
|
-
- rackup
|
149
|
-
- rake
|
150
|
-
- rdoc
|
151
|
-
- ri
|
152
|
-
- rspec
|
153
|
-
- rubocop
|
154
|
-
- semver
|
141
|
+
executables: []
|
155
142
|
extensions: []
|
156
143
|
extra_rdoc_files:
|
157
144
|
- LICENSE
|
@@ -167,28 +154,17 @@ files:
|
|
167
154
|
- README.md
|
168
155
|
- Rakefile
|
169
156
|
- VERSION
|
170
|
-
- bin/bundle
|
171
|
-
- bin/htmldiff
|
172
|
-
- bin/jeweler
|
173
|
-
- bin/ldiff
|
174
|
-
- bin/nokogiri
|
175
|
-
- bin/racc
|
176
|
-
- bin/rackup
|
177
|
-
- bin/rake
|
178
|
-
- bin/rdoc
|
179
|
-
- bin/ri
|
180
|
-
- bin/rspec
|
181
|
-
- bin/rubocop
|
182
|
-
- bin/semver
|
183
157
|
- lib/rails-threaded-proxy.rb
|
184
158
|
- lib/threaded-proxy.rb
|
185
159
|
- lib/threaded_proxy.rb
|
186
160
|
- lib/threaded_proxy/client.rb
|
187
161
|
- lib/threaded_proxy/controller.rb
|
188
162
|
- lib/threaded_proxy/http.rb
|
163
|
+
- lib/threaded_proxy/socket_responder.rb
|
189
164
|
- rails-threaded-proxy.gemspec
|
190
165
|
- spec/spec_helper.rb
|
191
166
|
- spec/threaded_proxy/client_spec.rb
|
167
|
+
- spec/threaded_proxy/controller_spec.rb
|
192
168
|
homepage: http://github.com/mnutt/rails-threaded-proxy
|
193
169
|
licenses:
|
194
170
|
- MIT
|
data/bin/bundle
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'bundle' is installed as part of a gem, and
|
8
|
-
# this file is here to facilitate running it.
|
9
|
-
#
|
10
|
-
|
11
|
-
require "rubygems"
|
12
|
-
|
13
|
-
m = Module.new do
|
14
|
-
module_function
|
15
|
-
|
16
|
-
def invoked_as_script?
|
17
|
-
File.expand_path($0) == File.expand_path(__FILE__)
|
18
|
-
end
|
19
|
-
|
20
|
-
def env_var_version
|
21
|
-
ENV["BUNDLER_VERSION"]
|
22
|
-
end
|
23
|
-
|
24
|
-
def cli_arg_version
|
25
|
-
return unless invoked_as_script? # don't want to hijack other binstubs
|
26
|
-
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
|
27
|
-
bundler_version = nil
|
28
|
-
update_index = nil
|
29
|
-
ARGV.each_with_index do |a, i|
|
30
|
-
if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
|
31
|
-
bundler_version = a
|
32
|
-
end
|
33
|
-
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
|
34
|
-
bundler_version = $1
|
35
|
-
update_index = i
|
36
|
-
end
|
37
|
-
bundler_version
|
38
|
-
end
|
39
|
-
|
40
|
-
def gemfile
|
41
|
-
gemfile = ENV["BUNDLE_GEMFILE"]
|
42
|
-
return gemfile if gemfile && !gemfile.empty?
|
43
|
-
|
44
|
-
File.expand_path("../Gemfile", __dir__)
|
45
|
-
end
|
46
|
-
|
47
|
-
def lockfile
|
48
|
-
lockfile =
|
49
|
-
case File.basename(gemfile)
|
50
|
-
when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
|
51
|
-
else "#{gemfile}.lock"
|
52
|
-
end
|
53
|
-
File.expand_path(lockfile)
|
54
|
-
end
|
55
|
-
|
56
|
-
def lockfile_version
|
57
|
-
return unless File.file?(lockfile)
|
58
|
-
lockfile_contents = File.read(lockfile)
|
59
|
-
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
|
60
|
-
Regexp.last_match(1)
|
61
|
-
end
|
62
|
-
|
63
|
-
def bundler_requirement
|
64
|
-
@bundler_requirement ||=
|
65
|
-
env_var_version ||
|
66
|
-
cli_arg_version ||
|
67
|
-
bundler_requirement_for(lockfile_version)
|
68
|
-
end
|
69
|
-
|
70
|
-
def bundler_requirement_for(version)
|
71
|
-
return "#{Gem::Requirement.default}.a" unless version
|
72
|
-
|
73
|
-
bundler_gem_version = Gem::Version.new(version)
|
74
|
-
|
75
|
-
bundler_gem_version.approximate_recommendation
|
76
|
-
end
|
77
|
-
|
78
|
-
def load_bundler!
|
79
|
-
ENV["BUNDLE_GEMFILE"] ||= gemfile
|
80
|
-
|
81
|
-
activate_bundler
|
82
|
-
end
|
83
|
-
|
84
|
-
def activate_bundler
|
85
|
-
gem_error = activation_error_handling do
|
86
|
-
gem "bundler", bundler_requirement
|
87
|
-
end
|
88
|
-
return if gem_error.nil?
|
89
|
-
require_error = activation_error_handling do
|
90
|
-
require "bundler/version"
|
91
|
-
end
|
92
|
-
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
93
|
-
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
|
94
|
-
exit 42
|
95
|
-
end
|
96
|
-
|
97
|
-
def activation_error_handling
|
98
|
-
yield
|
99
|
-
nil
|
100
|
-
rescue StandardError, LoadError => e
|
101
|
-
e
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
m.load_bundler!
|
106
|
-
|
107
|
-
if m.invoked_as_script?
|
108
|
-
load Gem.bin_path("bundler", "bundle")
|
109
|
-
end
|
data/bin/htmldiff
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'htmldiff' 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("diff-lcs", "htmldiff")
|
data/bin/jeweler
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'jeweler' 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("jeweler", "jeweler")
|
data/bin/ldiff
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'ldiff' 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("diff-lcs", "ldiff")
|
data/bin/nokogiri
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'nokogiri' 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("nokogiri", "nokogiri")
|
data/bin/racc
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'racc' 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("racc", "racc")
|
data/bin/rackup
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rackup' 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("rack", "rackup")
|
data/bin/rake
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rake' 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("rake", "rake")
|
data/bin/rdoc
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rdoc' 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("rdoc", "rdoc")
|
data/bin/ri
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'ri' 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("rdoc", "ri")
|
data/bin/rspec
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rspec' 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("rspec-core", "rspec")
|
data/bin/rubocop
DELETED
@@ -1,27 +0,0 @@
|
|
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/bin/semver
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'semver' 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("semver2", "semver")
|