forward-proxy 0.1.3 → 0.4.0

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