forward-proxy 0.1.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3f624058897845408afede390c49b3048ee7a74f
4
- data.tar.gz: d2a90df621baefdecc61bba48a44bfdee2180766
2
+ SHA256:
3
+ metadata.gz: 1d2b9dbd7abcfa81bcdbd29b6dd2045e6d2210e92b303f7cfcdc4ab35ab0b868
4
+ data.tar.gz: 729d4b7d964c8a5a3bec860f6bceb669a12225900a01b4737de8fdf6ba02a049
5
5
  SHA512:
6
- metadata.gz: 76d5bffc4db996cdc9578baa97cef6e13eb519cc065a0856cf83724470b27beaf6819b66aaddaf10bc21b1053b269635623ed5b4e9e0f13dff561bc044c7313d
7
- data.tar.gz: e085eba75e664a72575920750045ef62334cc0e3c64c72daddc5db5bf571656fb395790c6cb5d0a32bd3b3320fdeb2e0ce50705d59e3a2da0844628ae9b37148
6
+ metadata.gz: 70c0b51f0313e4ffedd1146ae93035db45bc8fe3a7cee647ea11cb0397d6862e99047befbc39aee6a08b17fd1e3d16394eb59ad9ad092140ea9354da10feadf6
7
+ data.tar.gz: b0f3587f3c08b255be222f8f59530e19b5fcc6e589f93eafb1eaa21fac832a0f01847dfcf309ed443e51fdafd96a516241143c1a75795b3d09f0ce7088eedac0
@@ -0,0 +1,25 @@
1
+ name: Command Line Interface
2
+
3
+ on: [pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ matrix:
11
+ ruby: [ '2.3', '2.7' ]
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - run: gem install forward-proxy
20
+ - run: |
21
+ exe/forward-proxy --h
22
+ exe/forward-proxy --help
23
+ exe/forward-proxy --binding 127.0.0.1 --port 3001 --threads 2 &
24
+ sleep 1
25
+ curl --fail -x http://127.0.0.1:3001 https://google.com/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Extract errors into module.
6
+
7
+ ## 0.1.5
8
+
9
+ - stream http proxy requests.
10
+ - call shutdown on `Interrupt`
11
+ - remove waiting for the thread-pool to finish on shutdown and just close the server conn.
12
+
13
+ ## 0.1.4
14
+
15
+ - `accept` connections via thread pool.
16
+ - catch and throw custom http parse error.
17
+ - `Server:` header added for default error 502 response.
18
+ - rescue and logs socket error `Errno::EBADF`.
19
+
3
20
  ## 0.1.3
4
21
 
5
22
  - Refine CLI help text.
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
+ gem "yard", "~> 0.9"
7
8
  gem "minitest", "~> 5.0"
8
9
  gem "minitest-reporters", "~> 1.4"
9
10
  gem "simplecov", "~> 0.17"
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Gem Version][3] ![Gem][1] ![Build Status][2]
4
4
 
5
- Ruby Forward Proxy implemented with only standard libraries.
5
+ Minimal forward proxy using 150LOC and only standard libraries. Useful for development, testing, and learning.
6
6
 
7
7
  ```
8
8
  $ forward-proxy --binding 0.0.0.0 --port 3182 --threads 2
data/Rakefile CHANGED
@@ -1,5 +1,12 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
+ require 'yard'
4
+
5
+ YARD::Rake::YardocTask.new do |t|
6
+ t.files = ['lib/**/*.rb', 'README', 'CHANGELOG', 'CODE_OF_CONDUCT']
7
+ t.options = []
8
+ t.stats_options = ['--list-undoc']
9
+ end
3
10
 
4
11
  Rake::TestTask.new(:test) do |t|
5
12
  t.libs << "test"
@@ -7,4 +14,16 @@ Rake::TestTask.new(:test) do |t|
7
14
  t.test_files = FileList["test/**/*_test.rb"]
8
15
  end
9
16
 
17
+ namespace :gh do
18
+ desc "Deploy yard docs to github pages"
19
+ task :pages => :yard do
20
+ `git add -f doc`
21
+ `git commit -am "update: $(date)"`
22
+ `git subtree split --prefix doc -b gh-pages`
23
+ `git push -f origin gh-pages:gh-pages`
24
+ `git branch -D gh-pages`
25
+ `git reset head~1`
26
+ end
27
+ end
28
+
10
29
  task :default => :test
data/exe/forward-proxy CHANGED
@@ -7,7 +7,7 @@ options = {}
7
7
  OptionParser.new do |parser|
8
8
  parser.banner = "Usage: forward-proxy [options]"
9
9
 
10
- parser.on("-pPORT", "--port=PORT", "Bind to specified port. Default: 9292", String) do |bind_port|
10
+ parser.on("-pPORT", "--port=PORT", String, "Bind to specified port. Default: 9292") do |bind_port|
11
11
  options[:bind_port] = bind_port
12
12
  end
13
13
 
@@ -26,6 +26,5 @@ OptionParser.new do |parser|
26
26
  end.parse!
27
27
 
28
28
  require_relative '../lib/forward_proxy'
29
-
30
29
  server = ForwardProxy::Server.new(options)
31
30
  server.start
data/lib/forward_proxy.rb CHANGED
@@ -1,4 +1,4 @@
1
- $:.unshift File.dirname(__FILE__)
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'forward_proxy/version'
4
4
  require 'forward_proxy/server'
@@ -0,0 +1,5 @@
1
+ module ForwardProxy
2
+ module Errors
3
+ class HTTPMethodNotImplemented < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module ForwardProxy
2
+ module Errors
3
+ class HTTPParseError < StandardError; end
4
+ end
5
+ end
@@ -1,73 +1,90 @@
1
- require 'forward_proxy/thread_pool'
1
+ require 'logger'
2
2
  require 'socket'
3
3
  require 'webrick'
4
4
  require 'net/http'
5
+ require 'forward_proxy/errors/http_method_not_implemented'
6
+ require 'forward_proxy/errors/http_parse_error'
7
+ require 'forward_proxy/thread_pool'
5
8
 
6
9
  module ForwardProxy
7
- class HTTPMethodNotImplemented < StandardError; end
8
-
9
10
  class Server
10
- attr_reader :bind_address, :bind_port
11
+ attr_reader :bind_address, :bind_port, :logger
11
12
 
12
- def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32)
13
+ def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32, logger: Logger.new(STDOUT, level: :info))
14
+ @logger = logger
13
15
  @thread_pool = ThreadPool.new(threads)
14
16
  @bind_address = bind_address
15
17
  @bind_port = bind_port
16
18
  end
17
19
 
18
20
  def start
19
- @server = TCPServer.new(bind_address, bind_port)
20
-
21
21
  thread_pool.start
22
22
 
23
- log("Listening #{bind_address}:#{bind_port}")
23
+ @socket = TCPServer.new(bind_address, bind_port)
24
24
 
25
- loop do
26
- client = server.accept
25
+ logger.info("Listening #{bind_address}:#{bind_port}")
27
26
 
28
- thread_pool.schedule(client) do |client_conn|
27
+ loop do
28
+ thread_pool.schedule(socket.accept) do |client_conn|
29
29
  begin
30
30
  req = parse_req(client_conn)
31
31
 
32
- log(req.request_line)
32
+ logger.info(req.request_line)
33
33
 
34
34
  case req.request_method
35
35
  when METHOD_CONNECT then handle_tunnel(client_conn, req)
36
36
  when METHOD_GET, METHOD_POST then handle(client_conn, req)
37
37
  else
38
- raise HTTPMethodNotImplemented
38
+ raise Errors::HTTPMethodNotImplemented
39
39
  end
40
- rescue => e
41
- client_conn.puts <<~eos.chomp
42
- HTTP/1.1 502
43
- eos
44
-
45
- puts e.message
46
- puts e.backtrace.map { |line| " #{line}" }
40
+ rescue => err
41
+ handle_error(err, client_conn)
47
42
  ensure
48
43
  client_conn.close
49
44
  end
50
45
  end
51
46
  end
52
47
  rescue Interrupt
48
+ shutdown
49
+ rescue IOError, Errno::EBADF => e
50
+ logger.error(e.message)
53
51
  end
54
52
 
55
53
  def shutdown
56
- log("Closing client connections...")
57
- thread_pool.shutdown
54
+ if socket
55
+ logger.info("Shutting down")
58
56
 
59
- log("Stoping server...")
60
- server.close if server
57
+ socket.close
58
+ end
61
59
  end
62
60
 
63
61
  private
64
62
 
65
- attr_reader :server, :thread_pool
63
+ attr_reader :socket, :thread_pool
64
+
65
+ # The following comments are from the IETF document
66
+ # "Hypertext Transfer Protocol -- HTTP/1.1: Basic Rules"
67
+ # https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
66
68
 
67
69
  METHOD_CONNECT = "CONNECT"
68
70
  METHOD_GET = "GET"
69
71
  METHOD_POST = "POST"
70
72
 
73
+ # HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all
74
+ # protocol elements except the entity-body.
75
+ HEADER_EOP = "\r\n"
76
+
77
+ # The following comments are from the IETF document
78
+ # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
79
+ # https://tools.ietf.org/html/rfc7231#section-4.3.6
80
+
81
+ # A proxy MUST send an appropriate Via header field, as described
82
+ # below, in each message that it forwards. An HTTP-to-HTTP gateway
83
+ # MUST send an appropriate Via header field in each inbound request
84
+ # message and MAY send a Via header field in forwarded response
85
+ # messages.
86
+ HEADER_VIA = "HTTP/1.1 ForwardProxy"
87
+
71
88
  def handle_tunnel(client_conn, req)
72
89
  # The following comments are from the IETF document
73
90
  # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
@@ -89,7 +106,10 @@ module ForwardProxy
89
106
  # blank line that concludes the successful response's header section;
90
107
  # data received after that blank line is from the server identified by
91
108
  # the request-target.
92
- client_conn.write "HTTP/1.1 200 OK\n\n"
109
+ client_conn.write <<~eos.chomp
110
+ HTTP/1.1 200 OK
111
+ #{HEADER_EOP}
112
+ eos
93
113
 
94
114
  # The CONNECT method requests that the recipient establish a tunnel to
95
115
  # the destination origin server identified by the request-target and,
@@ -104,22 +124,57 @@ module ForwardProxy
104
124
  end
105
125
 
106
126
  def handle(client_conn, req)
107
- resp = Net::HTTP.start(req.host, req.port) do |http|
108
- http.request map_webrick_to_net_http_req(req)
127
+ Net::HTTP.start(req.host, req.port) do |http|
128
+ http.request(map_webrick_to_net_http_req(req)) do |resp|
129
+ # The following comments are from the IETF document
130
+ # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
131
+ # https://tools.ietf.org/html/rfc7231#section-4.3.6
132
+
133
+ # An intermediary MAY combine an ordered subsequence of Via header
134
+ # field entries into a single such entry if the entries have identical
135
+ # received-protocol values. For example,
136
+ #
137
+ # Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy
138
+ #
139
+ # could be collapsed to
140
+ #
141
+ # Via: 1.0 ricky, 1.1 mertz, 1.0 lucy
142
+ #
143
+ # A sender SHOULD NOT combine multiple entries unless they are all
144
+ # under the same organizational control and the hosts have already been
145
+ # replaced by pseudonyms. A sender MUST NOT combine entries that have
146
+ # different received-protocol values.
147
+ headers = resp.to_hash.merge(Via: [HEADER_VIA, resp['Via']].compact.join(', '))
148
+
149
+ client_conn.puts <<~eos.chomp
150
+ HTTP/1.1 #{resp.code}
151
+ #{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
152
+ #{HEADER_EOP}
153
+ eos
154
+
155
+ # The following comments are taken from:
156
+ # https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Streaming+Response+Bodies
157
+
158
+ # By default Net::HTTP reads an entire response into memory. If you are
159
+ # handling large files or wish to implement a progress bar you can
160
+ # instead stream the body directly to an IO.
161
+ resp.read_body do |chunk|
162
+ client_conn << chunk
163
+ end
164
+ end
109
165
  end
166
+ end
110
167
 
111
- # A proxy MUST send an appropriate Via header field, as described
112
- # below, in each message that it forwards. An HTTP-to-HTTP gateway
113
- # MUST send an appropriate Via header field in each inbound request
114
- # message and MAY send a Via header field in forwarded response
115
- # messages.
168
+ def handle_error(err, client_conn)
116
169
  client_conn.puts <<~eos.chomp
117
- HTTP/1.1 #{resp.code}
118
- Via: #{["1.1 ForwardProxy", resp['Via']].compact.join(', ')}
119
- #{resp.each.map { |header, value| "#{header}: #{value}" }.join("\n")}
120
-
121
- #{resp.body}
170
+ HTTP/1.1 502
171
+ Via: #{HEADER_VIA}
172
+ #{HEADER_EOP}
122
173
  eos
174
+
175
+ logger.error(err.message)
176
+
177
+ logger.debug(err.backtrace.join("\n"))
123
178
  end
124
179
 
125
180
  def map_webrick_to_net_http_req(req)
@@ -129,26 +184,24 @@ module ForwardProxy
129
184
  when METHOD_GET then Net::HTTP::Get
130
185
  when METHOD_POST then Net::HTTP::Post
131
186
  else
132
- raise HTTPMethodNotImplemented
187
+ raise Errors::HTTPMethodNotImplemented
133
188
  end
134
189
 
135
190
  klass.new(req.path, req_headers)
136
191
  end
137
192
 
138
- def transfer(dest_conn, src_conn)
193
+ def transfer(src_conn, dest_conn)
139
194
  IO.copy_stream(src_conn, dest_conn)
140
195
  rescue => e
141
- log(e.message, "WARNING")
196
+ logger.warn(e.message)
142
197
  end
143
198
 
144
199
  def parse_req(client_conn)
145
200
  WEBrick::HTTPRequest.new(WEBrick::Config::HTTP).tap do |req|
146
201
  req.parse(client_conn)
147
202
  end
148
- end
149
-
150
- def log(str, level = 'INFO')
151
- puts "[#{Time.now}] #{level} #{str}"
203
+ rescue => e
204
+ throw Errors::HTTPParseError.new(e.message)
152
205
  end
153
206
  end
154
207
  end
@@ -1,3 +1,3 @@
1
1
  module ForwardProxy
2
- VERSION = "0.1.3"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forward-proxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Moriarty
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-14 00:00:00.000000000 Z
11
+ date: 2021-07-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Forward proxy using just Ruby standard libraries.
14
14
  email:
@@ -19,6 +19,7 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - ".github/workflows/ci.yaml"
22
+ - ".github/workflows/cli.yaml"
22
23
  - ".gitignore"
23
24
  - CHANGELOG.md
24
25
  - CODE_OF_CONDUCT.md
@@ -31,6 +32,8 @@ files:
31
32
  - exe/forward-proxy
32
33
  - forward-proxy.gemspec
33
34
  - lib/forward_proxy.rb
35
+ - lib/forward_proxy/errors/http_method_not_implemented.rb
36
+ - lib/forward_proxy/errors/http_parse_error.rb
34
37
  - lib/forward_proxy/server.rb
35
38
  - lib/forward_proxy/thread_pool.rb
36
39
  - lib/forward_proxy/version.rb
@@ -56,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
59
  - !ruby/object:Gem::Version
57
60
  version: '0'
58
61
  requirements: []
59
- rubyforge_project:
60
- rubygems_version: 2.5.2.3
62
+ rubygems_version: 3.0.3
61
63
  signing_key:
62
64
  specification_version: 4
63
65
  summary: Forward proxy.