forward-proxy 0.1.1 → 0.2.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: 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.