forward-proxy 0.1.1 → 0.2.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: 6af22f5d41a5a3b702233f8a13b67533451a411b
4
- data.tar.gz: 850669c9081a84975689e17d0e92b6b9adb5f29a
2
+ SHA256:
3
+ metadata.gz: 628359d25536c4190014c9e082fff41b2adef209f04a62d8a6b217c078f3b58b
4
+ data.tar.gz: 7834932b7242fd66c120d270a922e385a6a58ab63c65e8ce2b7957da43ba5a11
5
5
  SHA512:
6
- metadata.gz: 0eba287e90bd3369421aa16401b2958203058047900f1b496e5c690bb15b5b58c018340118a76d18a39c3dcb054f5b034b2a5c3c6a94bc1a95a71b1f5ace78fd
7
- data.tar.gz: c71b4cb6d19cdf9e9bad3605b03d4a8d2cc73933bb3b575ddf3be75d4408d266627dc97421b50c71578ede837a24417d00f243e5ad05cabebbd1c21c2d3a2d4f
6
+ metadata.gz: 8c47ab1d691807a23ba779f1ae8d4feecd32300f8d840044f66a5cc2f40ccd33c095db60ca9e3f99fb3c8fd60c02ae46d5d8d07b6a4de6298bef24731fb57cb4
7
+ data.tar.gz: 609a35651496d98e4977f3691c7834efd58fe736615b3b4594e0bc61d11dad0088f11f535f6b287db85f95c1cc8ab55ea2a536447028b690d92ffd0aa3e60669
@@ -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 ADDED
@@ -0,0 +1,26 @@
1
+ # CHANGELOG
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
+
20
+ ## 0.1.3
21
+
22
+ - Refine CLI help text.
23
+
24
+ ## 0.1.2
25
+
26
+ - Wrap ThreadPool in ForwardProxy module.
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
@@ -1,6 +1,8 @@
1
1
  # ForwardProxy
2
2
 
3
- Ruby Forward Proxy implemented with only standard libraries.
3
+ ![Gem Version][3] ![Gem][1] ![Build Status][2]
4
+
5
+ Minimal forward proxy using 150LOC and only standard libraries. Useful for development, testing, and learning.
4
6
 
5
7
  ```
6
8
  $ forward-proxy --binding 0.0.0.0 --port 3182 --threads 2
@@ -35,9 +37,9 @@ forward-proxy
35
37
  ```
36
38
  Usage: forward-proxy [options]
37
39
  -p, --port=PORT Bind to specified port. Default: 9292
38
- -b, --binding=BINDING Bind to the specified IP. Default: 127.0.0.1
40
+ -b, --binding=BINDING Bind to the specified ip. Default: 127.0.0.1
39
41
  -t, --threads=THREADS Specify the number of client threads. Default: 32
40
- -h, --help Prints this help
42
+ -h, --help Prints this help.
41
43
  ```
42
44
 
43
45
  ### Library
@@ -70,3 +72,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
70
72
  ## Code of Conduct
71
73
 
72
74
  Everyone interacting in the ForwardProxy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jamesmoriarty/forward-proxy/blob/master/CODE_OF_CONDUCT.md).
75
+
76
+ [1]: https://img.shields.io/gem/dt/forward-proxy
77
+ [2]: https://github.com/jamesmoriarty/forward-proxy/workflows/Continuous%20Integration/badge.svg?branch=main
78
+ [3]: https://img.shields.io/gem/v/forward-proxy
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,11 +7,11 @@ 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
 
14
- parser.on("-bBINDING", "--binding=BINDING", String, "Bind to the specified IP. Default: 127.0.0.1") do |bind_address|
14
+ parser.on("-bBINDING", "--binding=BINDING", String, "Bind to the specified ip. Default: 127.0.0.1") do |bind_address|
15
15
  options[:bind_address] = bind_address
16
16
  end
17
17
 
@@ -19,13 +19,12 @@ OptionParser.new do |parser|
19
19
  options[:threads] = threads
20
20
  end
21
21
 
22
- parser.on("-h", "--help", "Prints this help") do
22
+ parser.on("-h", "--help", "Prints this help.") do
23
23
  puts parser
24
24
  exit
25
25
  end
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
@@ -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,11 +1,11 @@
1
- require 'forward_proxy/thread_pool'
2
1
  require 'socket'
3
2
  require 'webrick'
4
3
  require 'net/http'
4
+ require 'forward_proxy/errors/http_method_not_implemented'
5
+ require 'forward_proxy/errors/http_parse_error'
6
+ require 'forward_proxy/thread_pool'
5
7
 
6
8
  module ForwardProxy
7
- class HTTPMethodNotImplemented < StandardError; end
8
-
9
9
  class Server
10
10
  attr_reader :bind_address, :bind_port
11
11
 
@@ -16,16 +16,14 @@ module ForwardProxy
16
16
  end
17
17
 
18
18
  def start
19
- @server = TCPServer.new(bind_address, bind_port)
20
-
21
19
  thread_pool.start
22
20
 
21
+ @socket = TCPServer.new(bind_address, bind_port)
22
+
23
23
  log("Listening #{bind_address}:#{bind_port}")
24
24
 
25
25
  loop do
26
- client = server.accept
27
-
28
- thread_pool.schedule(client) do |client_conn|
26
+ thread_pool.schedule(socket.accept) do |client_conn|
29
27
  begin
30
28
  req = parse_req(client_conn)
31
29
 
@@ -35,11 +33,12 @@ module ForwardProxy
35
33
  when METHOD_CONNECT then handle_tunnel(client_conn, req)
36
34
  when METHOD_GET, METHOD_POST then handle(client_conn, req)
37
35
  else
38
- raise HTTPMethodNotImplemented
36
+ raise Errors::HTTPMethodNotImplemented
39
37
  end
40
38
  rescue => e
41
39
  client_conn.puts <<~eos.chomp
42
40
  HTTP/1.1 502
41
+ Via: #{HEADER_VIA}
43
42
  eos
44
43
 
45
44
  puts e.message
@@ -50,24 +49,38 @@ module ForwardProxy
50
49
  end
51
50
  end
52
51
  rescue Interrupt
52
+ shutdown
53
+ rescue IOError, Errno::EBADF => e
54
+ log(e.message, "ERROR")
53
55
  end
54
56
 
55
57
  def shutdown
56
- log("Closing client connections...")
57
- thread_pool.shutdown
58
+ if socket
59
+ log("Shutting down")
58
60
 
59
- log("Stoping server...")
60
- server.close if server
61
+ socket.close
62
+ end
61
63
  end
62
64
 
63
65
  private
64
66
 
65
- attr_reader :server, :thread_pool
67
+ attr_reader :socket, :thread_pool
66
68
 
67
69
  METHOD_CONNECT = "CONNECT"
68
70
  METHOD_GET = "GET"
69
71
  METHOD_POST = "POST"
70
72
 
73
+ # The following comments are from the IETF document
74
+ # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
75
+ # https://tools.ietf.org/html/rfc7231#section-4.3.6
76
+
77
+ # A proxy MUST send an appropriate Via header field, as described
78
+ # below, in each message that it forwards. An HTTP-to-HTTP gateway
79
+ # MUST send an appropriate Via header field in each inbound request
80
+ # message and MAY send a Via header field in forwarded response
81
+ # messages.
82
+ HEADER_VIA = "HTTP/1.1 ForwardProxy"
83
+
71
84
  def handle_tunnel(client_conn, req)
72
85
  # The following comments are from the IETF document
73
86
  # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
@@ -104,22 +117,25 @@ module ForwardProxy
104
117
  end
105
118
 
106
119
  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)
120
+ Net::HTTP.start(req.host, req.port) do |http|
121
+ http.request(map_webrick_to_net_http_req(req)) do |resp|
122
+ client_conn.puts <<~eos.chomp
123
+ HTTP/1.1 #{resp.code}
124
+ Via: #{[HEADER_VIA, resp['Via']].compact.join(', ')}
125
+ #{resp.each.map { |header, value| "#{header}: #{value}" }.join("\n")}\n\n
126
+ eos
127
+
128
+ # The following comments are taken from:
129
+ # https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Streaming+Response+Bodies
130
+
131
+ # By default Net::HTTP reads an entire response into memory. If you are
132
+ # handling large files or wish to implement a progress bar you can
133
+ # instead stream the body directly to an IO.
134
+ resp.read_body do |chunk|
135
+ client_conn << chunk
136
+ end
137
+ end
109
138
  end
110
-
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.
116
- 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}
122
- eos
123
139
  end
124
140
 
125
141
  def map_webrick_to_net_http_req(req)
@@ -129,13 +145,13 @@ module ForwardProxy
129
145
  when METHOD_GET then Net::HTTP::Get
130
146
  when METHOD_POST then Net::HTTP::Post
131
147
  else
132
- raise HTTPMethodNotImplemented
148
+ raise Errors::HTTPMethodNotImplemented
133
149
  end
134
150
 
135
151
  klass.new(req.path, req_headers)
136
152
  end
137
153
 
138
- def transfer(dest_conn, src_conn)
154
+ def transfer(src_conn, dest_conn)
139
155
  IO.copy_stream(src_conn, dest_conn)
140
156
  rescue => e
141
157
  log(e.message, "WARNING")
@@ -145,6 +161,8 @@ module ForwardProxy
145
161
  WEBrick::HTTPRequest.new(WEBrick::Config::HTTP).tap do |req|
146
162
  req.parse(client_conn)
147
163
  end
164
+ rescue => e
165
+ throw Errors::HTTPParseError.new(e.message)
148
166
  end
149
167
 
150
168
  def log(str, level = 'INFO')
@@ -1,34 +1,36 @@
1
- class ThreadPool
2
- attr_reader :queue, :threads, :size
1
+ module ForwardProxy
2
+ class ThreadPool
3
+ attr_reader :queue, :threads, :size
3
4
 
4
- def initialize(size)
5
- @size = size
6
- @queue = Queue.new
7
- @threads = []
8
- end
5
+ def initialize(size)
6
+ @size = size
7
+ @queue = Queue.new
8
+ @threads = []
9
+ end
9
10
 
10
- def start
11
- size.times do
12
- threads << Thread.new do
13
- catch(:exit) do
14
- loop do
15
- job, args = queue.pop
16
- job.call(*args)
11
+ def start
12
+ 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
17
19
  end
18
20
  end
19
21
  end
20
22
  end
21
- end
22
-
23
- def schedule(*args, &block)
24
- queue.push([block, args])
25
- end
26
23
 
27
- def shutdown
28
- threads.each do
29
- schedule { throw :exit }
24
+ def schedule(*args, &block)
25
+ queue.push([block, args])
30
26
  end
31
27
 
32
- threads.each(&:join)
28
+ def shutdown
29
+ threads.each do
30
+ schedule { throw :exit }
31
+ end
32
+
33
+ threads.each(&:join)
34
+ end
33
35
  end
34
36
  end
@@ -1,3 +1,3 @@
1
1
  module ForwardProxy
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.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.1
4
+ version: 0.2.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-05-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Forward proxy using just Ruby standard libraries.
14
14
  email:
@@ -19,7 +19,9 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - ".github/workflows/ci.yaml"
22
+ - ".github/workflows/cli.yaml"
22
23
  - ".gitignore"
24
+ - CHANGELOG.md
23
25
  - CODE_OF_CONDUCT.md
24
26
  - Gemfile
25
27
  - LICENSE.txt
@@ -30,6 +32,8 @@ files:
30
32
  - exe/forward-proxy
31
33
  - forward-proxy.gemspec
32
34
  - lib/forward_proxy.rb
35
+ - lib/forward_proxy/errors/http_method_not_implemented.rb
36
+ - lib/forward_proxy/errors/http_parse_error.rb
33
37
  - lib/forward_proxy/server.rb
34
38
  - lib/forward_proxy/thread_pool.rb
35
39
  - lib/forward_proxy/version.rb
@@ -55,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
59
  - !ruby/object:Gem::Version
56
60
  version: '0'
57
61
  requirements: []
58
- rubyforge_project:
59
- rubygems_version: 2.5.2.3
62
+ rubygems_version: 3.0.3
60
63
  signing_key:
61
64
  specification_version: 4
62
65
  summary: Forward proxy.