forward-proxy 0.1.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3e94626ce91d870564fcbed5db9133ddee5a1167
4
- data.tar.gz: 44c9c4e4ca3a4036a8c1e9698dac6a203c3fe367
2
+ SHA256:
3
+ metadata.gz: ccf19df114eeec55eaef8765999561904d8f5b6cd91b7ed2016053ec9e717408
4
+ data.tar.gz: ce712f520c0ed32b93ea211ff658d330bef7eeb26c2ddfcea1246d955ce78175
5
5
  SHA512:
6
- metadata.gz: c13a9fa915fb77699702226408cbd160d61cd375789beb947299423c788594c7f5d7af3783624fcf5fb30e8521e8a5e98c4953e7f1b5c7e477a756bda153634b
7
- data.tar.gz: 609126058336e8cc597c0df21b3a3cb65c4d1216d7af11d77dec09eda104c6277811d59b942d38c46f8776c0cf1b8f137d09778793343a88c138872f65b06b54
6
+ metadata.gz: b71608455b079161950b09521d8fd99496767463f3e2aa0f0d293f21e3955b03d7b0ba12f06f3b3df1ff6674d598ed2e7eec7492ffb8fd9047ebb19771625251
7
+ data.tar.gz: '092035474f162e29d2c7d0735b43cd38e41580aadca7ae50e97e02dee8e6f3825f50393497c0a8db8a5e0b3622d3e44e25e69e03ae02b56eba1cad55abdc1388'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Extract errors into module.
6
+
3
7
  ## 0.1.5
4
8
 
5
9
  - stream http proxy requests.
data/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  ![Gem Version][3] ![Gem][1] ![Build Status][2]
4
4
 
5
- 100 LOC Ruby forward proxy using 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
9
- [2021-01-14 19:37:47 +1100] INFO Listening 0.0.0.0:3182
10
- [2021-01-14 19:38:24 +1100] INFO CONNECT raw.githubusercontent.com:443 HTTP/1.1
9
+ I, [2021-07-04T10:33:32.947653 #1790] INFO -- : Listening 0.0.0.0:3182
10
+ I, [2021-07-04T10:33:32.998298 #1790] INFO -- : CONNECT raw.githubusercontent.com:443 HTTP/1.1
11
11
  ```
12
12
 
13
13
  ## Installation
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
 
@@ -15,7 +15,8 @@ OptionParser.new do |parser|
15
15
  options[:bind_address] = bind_address
16
16
  end
17
17
 
18
- parser.on("-tTHREADS", "--threads=THREADS", Integer, "Specify the number of client threads. Default: 32") do |threads|
18
+ parser.on("-tTHREADS", "--threads=THREADS", Integer,
19
+ "Specify the number of client threads. Default: 128") do |threads|
19
20
  options[:threads] = threads
20
21
  end
21
22
 
@@ -26,6 +27,5 @@ OptionParser.new do |parser|
26
27
  end.parse!
27
28
 
28
29
  require_relative '../lib/forward_proxy'
29
-
30
30
  server = ForwardProxy::Server.new(options)
31
31
  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,64 +1,44 @@
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
- class HTTPParseError < StandardError; end
9
-
10
10
  class Server
11
- attr_reader :bind_address, :bind_port
11
+ attr_reader :bind_address, :bind_port, :logger
12
12
 
13
- def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32)
14
- @thread_pool = ThreadPool.new(threads)
13
+ def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 128, logger: default_logger)
15
14
  @bind_address = bind_address
16
15
  @bind_port = bind_port
16
+ @logger = logger
17
+ @thread_pool = ThreadPool.new(threads)
17
18
  end
18
19
 
19
20
  def start
20
21
  thread_pool.start
21
22
 
22
- @server = TCPServer.new(bind_address, bind_port)
23
+ @socket = TCPServer.new(bind_address, bind_port)
23
24
 
24
- log("Listening #{bind_address}:#{bind_port}")
25
+ logger.info("Listening #{bind_address}:#{bind_port}")
25
26
 
26
27
  loop do
27
- # The following comments are from https://stackoverflow.com/q/5124320/273101
28
- #
29
- # accept(): POSIX.1-2001, POSIX.1-2008, SVr4, 4.4BSD (accept() first appeared in 4.2BSD).
30
- #
31
- # We see that POSIX.1-2008 is a viable reference (check this for a description of relevant
32
- # standards for Linux systems). As already said in other answers, POSIX.1 standard specifies
33
- # accept function as (POSIX-)thread safe (as defined in Base Definitions, section 3.399 Thread Safe)
34
- # by not listing it on System Interfaces, section 2.9.1 Thread Safety.
35
- thread_pool.schedule(server.accept) do |client_conn|
28
+ thread_pool.schedule(socket.accept) do |client_conn|
36
29
  begin
37
30
  req = parse_req(client_conn)
38
31
 
39
- log(req.request_line)
32
+ logger.info(req.request_line.strip)
40
33
 
41
34
  case req.request_method
42
35
  when METHOD_CONNECT then handle_tunnel(client_conn, req)
43
36
  when METHOD_GET, METHOD_POST then handle(client_conn, req)
44
37
  else
45
- raise HTTPMethodNotImplemented
38
+ raise Errors::HTTPMethodNotImplemented
46
39
  end
47
40
  rescue => e
48
- # The following comments are from the IETF document
49
- # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
50
- # https://tools.ietf.org/html/rfc7231#section-6.6.3
51
-
52
- # The 502 (Bad Gateway) status code indicates that the server, while
53
- # acting as a gateway or proxy, received an invalid response from an
54
- # inbound server it accessed while attempting to fulfill the request.
55
- client_conn.puts <<~eos.chomp
56
- HTTP/1.1 502
57
- Via: #{HEADER_VIA}
58
- eos
59
-
60
- puts e.message
61
- puts e.backtrace.map { |line| " #{line}" }
41
+ handle_error(e, client_conn)
62
42
  ensure
63
43
  client_conn.close
64
44
  end
@@ -67,22 +47,33 @@ module ForwardProxy
67
47
  rescue Interrupt
68
48
  shutdown
69
49
  rescue IOError, Errno::EBADF => e
70
- log(e.message, "ERROR")
50
+ logger.error(e.message)
71
51
  end
72
52
 
73
53
  def shutdown
74
- log("Stoping server...")
75
- server.close if server
54
+ if socket
55
+ logger.info("Shutting down")
56
+
57
+ socket.close
58
+ end
76
59
  end
77
60
 
78
61
  private
79
62
 
80
- 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
81
68
 
82
69
  METHOD_CONNECT = "CONNECT"
83
70
  METHOD_GET = "GET"
84
71
  METHOD_POST = "POST"
85
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
+
86
77
  # The following comments are from the IETF document
87
78
  # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
88
79
  # https://tools.ietf.org/html/rfc7231#section-4.3.6
@@ -115,7 +106,10 @@ module ForwardProxy
115
106
  # blank line that concludes the successful response's header section;
116
107
  # data received after that blank line is from the server identified by
117
108
  # the request-target.
118
- 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
119
113
 
120
114
  # The CONNECT method requests that the recipient establish a tunnel to
121
115
  # the destination origin server identified by the request-target and,
@@ -132,12 +126,38 @@ module ForwardProxy
132
126
  def handle(client_conn, req)
133
127
  Net::HTTP.start(req.host, req.port) do |http|
134
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
+
135
149
  client_conn.puts <<~eos.chomp
136
150
  HTTP/1.1 #{resp.code}
137
- Via: #{[HEADER_VIA, resp['Via']].compact.join(', ')}
138
- #{resp.each.map { |header, value| "#{header}: #{value}" }.join("\n")}\n\n
151
+ #{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
152
+ #{HEADER_EOP}
139
153
  eos
140
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.
141
161
  resp.read_body do |chunk|
142
162
  client_conn << chunk
143
163
  end
@@ -145,6 +165,21 @@ module ForwardProxy
145
165
  end
146
166
  end
147
167
 
168
+ def handle_error(err, client_conn)
169
+ client_conn.puts <<~eos.chomp
170
+ HTTP/1.1 502
171
+ Via: #{HEADER_VIA}
172
+ #{HEADER_EOP}
173
+ eos
174
+
175
+ logger.error(err.message)
176
+ logger.debug(err.backtrace.join("\n"))
177
+ end
178
+
179
+ def default_logger
180
+ Logger.new(STDOUT, level: :info)
181
+ end
182
+
148
183
  def map_webrick_to_net_http_req(req)
149
184
  req_headers = Hash[req.header.map { |k, v| [k, v.first] }]
150
185
 
@@ -152,16 +187,16 @@ module ForwardProxy
152
187
  when METHOD_GET then Net::HTTP::Get
153
188
  when METHOD_POST then Net::HTTP::Post
154
189
  else
155
- raise HTTPMethodNotImplemented
190
+ raise Errors::HTTPMethodNotImplemented
156
191
  end
157
192
 
158
193
  klass.new(req.path, req_headers)
159
194
  end
160
195
 
161
- def transfer(dest_conn, src_conn)
196
+ def transfer(src_conn, dest_conn)
162
197
  IO.copy_stream(src_conn, dest_conn)
163
198
  rescue => e
164
- log(e.message, "WARNING")
199
+ logger.warn(e.message)
165
200
  end
166
201
 
167
202
  def parse_req(client_conn)
@@ -169,11 +204,7 @@ module ForwardProxy
169
204
  req.parse(client_conn)
170
205
  end
171
206
  rescue => e
172
- throw HTTPParseError.new(e.message)
173
- end
174
-
175
- def log(str, level = 'INFO')
176
- puts "[#{Time.now}] #{level} #{str}"
207
+ throw Errors::HTTPParseError.new(e.message)
177
208
  end
178
209
  end
179
210
  end
@@ -1,21 +1,18 @@
1
1
  module ForwardProxy
2
2
  class ThreadPool
3
- attr_reader :queue, :threads, :size
3
+ attr_reader :queue, :size
4
4
 
5
5
  def initialize(size)
6
- @size = size
7
- @queue = Queue.new
8
- @threads = []
6
+ @size = size
7
+ @queue = Queue.new
9
8
  end
10
9
 
11
10
  def start
12
11
  size.times do
13
- threads << Thread.new do
14
- catch(:exit) do
15
- loop do
16
- job, args = queue.pop
17
- job.call(*args)
18
- end
12
+ Thread.new do
13
+ loop do
14
+ job, args = queue.pop
15
+ job.call(*args)
19
16
  end
20
17
  end
21
18
  end
@@ -24,13 +21,5 @@ module ForwardProxy
24
21
  def schedule(*args, &block)
25
22
  queue.push([block, args])
26
23
  end
27
-
28
- def shutdown
29
- threads.each do
30
- schedule { throw :exit }
31
- end
32
-
33
- threads.each(&:join)
34
- end
35
24
  end
36
25
  end
@@ -1,3 +1,3 @@
1
1
  module ForwardProxy
2
- VERSION = "0.1.5"
2
+ VERSION = "0.5.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.5
4
+ version: 0.5.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-20 00:00:00.000000000 Z
11
+ date: 2021-07-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Forward proxy using just Ruby standard libraries.
14
14
  email:
@@ -32,6 +32,8 @@ files:
32
32
  - exe/forward-proxy
33
33
  - forward-proxy.gemspec
34
34
  - lib/forward_proxy.rb
35
+ - lib/forward_proxy/errors/http_method_not_implemented.rb
36
+ - lib/forward_proxy/errors/http_parse_error.rb
35
37
  - lib/forward_proxy/server.rb
36
38
  - lib/forward_proxy/thread_pool.rb
37
39
  - lib/forward_proxy/version.rb
@@ -57,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
59
  - !ruby/object:Gem::Version
58
60
  version: '0'
59
61
  requirements: []
60
- rubyforge_project:
61
- rubygems_version: 2.5.2.3
62
+ rubygems_version: 3.0.3
62
63
  signing_key:
63
64
  specification_version: 4
64
65
  summary: Forward proxy.