forward-proxy 0.1.5 → 0.5.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: 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.