forward-proxy 0.2.0 → 0.6.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 +4 -4
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/cli.yaml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +2 -2
- data/exe/forward-proxy +7 -1
- data/lib/forward_proxy.rb +1 -1
- data/lib/forward_proxy/errors/connection_timeout_error.rb +5 -0
- data/lib/forward_proxy/errors/http_method_not_implemented.rb +1 -1
- data/lib/forward_proxy/errors/http_parse_error.rb +1 -1
- data/lib/forward_proxy/server.rb +82 -33
- data/lib/forward_proxy/thread_pool.rb +7 -18
- data/lib/forward_proxy/version.rb +1 -1
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 7ef1fa7e497462fc27f6b33ace589450994664c35c0e2744fae3a46c6caa0372
         | 
| 4 | 
            +
              data.tar.gz: a396b7121e8a512a518a033aaa14261d7b86fd0377cf6e29d183a34ba02ff010
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: af543772c2f4159b0697f3ad02d4a330f776b73f1ccbaab6e8eb0dff9a8b5a6855b515fea99165c884132e743fd65aff7ac5973916256bcd6f476102d9dad743
         | 
| 7 | 
            +
              data.tar.gz: e625cc351e9ce99744ff6ad00dac9c4e2e22c75cc0f8e4899c17736dcd06067398e1228bbebc6fb5ba07b44322f9806e62259c712f7d9bfddc89db5620045ded
         | 
    
        data/.github/workflows/ci.yaml
    CHANGED
    
    
    
        data/.github/workflows/cli.yaml
    CHANGED
    
    
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,15 @@ | |
| 1 1 | 
             
            # CHANGELOG
         | 
| 2 2 |  | 
| 3 | 
            +
            ## 0.6.0
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            - add connection timeout to stop tracking connection from saturating client threads.
         | 
| 6 | 
            +
            - add cli flats for connection timeout `-t` and `--timeout`.
         | 
| 7 | 
            +
            - change cli short flag `-t` to `-c` for `--threads`.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## 0.5.0
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            - increase default threads from `32` to `128`.
         | 
| 12 | 
            +
             | 
| 3 13 | 
             
            ## 0.2.0
         | 
| 4 14 |  | 
| 5 15 | 
             
            - Extract errors into module.
         | 
    
        data/README.md
    CHANGED
    
    | @@ -6,8 +6,8 @@ Minimal forward proxy using 150LOC and only standard libraries. Useful for devel | |
| 6 6 |  | 
| 7 7 | 
             
            ```
         | 
| 8 8 | 
             
            $ forward-proxy --binding 0.0.0.0 --port 3182 --threads 2
         | 
| 9 | 
            -
            [2021- | 
| 10 | 
            -
            [2021- | 
| 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
    
    | @@ -15,7 +15,13 @@ OptionParser.new do |parser| | |
| 15 15 | 
             
                options[:bind_address] = bind_address
         | 
| 16 16 | 
             
              end
         | 
| 17 17 |  | 
| 18 | 
            -
              parser.on("- | 
| 18 | 
            +
              parser.on("-tTIMEOUT", "--timeout=TIMEOUT", Integer,
         | 
| 19 | 
            +
                        "Specify the connection timout in seconds. Default: 300") do |threads|
         | 
| 20 | 
            +
                options[:timeout] = threads
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              parser.on("-cTHREADS", "--threads=THREADS", Integer,
         | 
| 24 | 
            +
                        "Specify the number of client threads. Default: 128") do |threads|
         | 
| 19 25 | 
             
                options[:threads] = threads
         | 
| 20 26 | 
             
              end
         | 
| 21 27 |  | 
    
        data/lib/forward_proxy.rb
    CHANGED
    
    
    
        data/lib/forward_proxy/server.rb
    CHANGED
    
    | @@ -1,18 +1,23 @@ | |
| 1 | 
            +
            require 'logger'
         | 
| 1 2 | 
             
            require 'socket'
         | 
| 2 | 
            -
            require ' | 
| 3 | 
            +
            require 'timeout'
         | 
| 3 4 | 
             
            require 'net/http'
         | 
| 5 | 
            +
            require 'webrick'
         | 
| 6 | 
            +
            require 'forward_proxy/errors/connection_timeout_error'
         | 
| 4 7 | 
             
            require 'forward_proxy/errors/http_method_not_implemented'
         | 
| 5 8 | 
             
            require 'forward_proxy/errors/http_parse_error'
         | 
| 6 9 | 
             
            require 'forward_proxy/thread_pool'
         | 
| 7 10 |  | 
| 8 11 | 
             
            module ForwardProxy
         | 
| 9 12 | 
             
              class Server
         | 
| 10 | 
            -
                attr_reader :bind_address, :bind_port
         | 
| 13 | 
            +
                attr_reader :bind_address, :bind_port, :logger, :timeout
         | 
| 11 14 |  | 
| 12 | 
            -
                def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads:  | 
| 13 | 
            -
                  @thread_pool = ThreadPool.new(threads)
         | 
| 15 | 
            +
                def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 4, timeout: 1, logger: default_logger)
         | 
| 14 16 | 
             
                  @bind_address = bind_address
         | 
| 15 17 | 
             
                  @bind_port = bind_port
         | 
| 18 | 
            +
                  @logger = logger
         | 
| 19 | 
            +
                  @thread_pool = ThreadPool.new(threads)
         | 
| 20 | 
            +
                  @timeout = timeout
         | 
| 16 21 | 
             
                end
         | 
| 17 22 |  | 
| 18 23 | 
             
                def start
         | 
| @@ -20,29 +25,25 @@ module ForwardProxy | |
| 20 25 |  | 
| 21 26 | 
             
                  @socket = TCPServer.new(bind_address, bind_port)
         | 
| 22 27 |  | 
| 23 | 
            -
                   | 
| 28 | 
            +
                  logger.info("Listening #{bind_address}:#{bind_port}")
         | 
| 24 29 |  | 
| 25 30 | 
             
                  loop do
         | 
| 26 31 | 
             
                    thread_pool.schedule(socket.accept) do |client_conn|
         | 
| 27 32 | 
             
                      begin
         | 
| 28 | 
            -
                         | 
| 33 | 
            +
                        Timeout::timeout(timeout, Errors::ConnectionTimeoutError, "connection exceeded #{timeout} seconds") do
         | 
| 34 | 
            +
                          req = parse_req(client_conn)
         | 
| 29 35 |  | 
| 30 | 
            -
             | 
| 36 | 
            +
                          logger.info(req.request_line.strip)
         | 
| 31 37 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 38 | 
            +
                          case req.request_method
         | 
| 39 | 
            +
                          when METHOD_CONNECT then handle_tunnel(client_conn, req)
         | 
| 40 | 
            +
                          when METHOD_GET, METHOD_POST then handle(client_conn, req)
         | 
| 41 | 
            +
                          else
         | 
| 42 | 
            +
                            raise Errors::HTTPMethodNotImplemented
         | 
| 43 | 
            +
                          end
         | 
| 37 44 | 
             
                        end
         | 
| 38 45 | 
             
                      rescue => e
         | 
| 39 | 
            -
                        client_conn | 
| 40 | 
            -
                          HTTP/1.1 502
         | 
| 41 | 
            -
                          Via: #{HEADER_VIA}
         | 
| 42 | 
            -
                        eos
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                        puts e.message
         | 
| 45 | 
            -
                        puts e.backtrace.map { |line| "  #{line}" }
         | 
| 46 | 
            +
                        handle_error(client_conn, e)
         | 
| 46 47 | 
             
                      ensure
         | 
| 47 48 | 
             
                        client_conn.close
         | 
| 48 49 | 
             
                      end
         | 
| @@ -51,14 +52,14 @@ module ForwardProxy | |
| 51 52 | 
             
                rescue Interrupt
         | 
| 52 53 | 
             
                  shutdown
         | 
| 53 54 | 
             
                rescue IOError, Errno::EBADF => e
         | 
| 54 | 
            -
                   | 
| 55 | 
            +
                  logger.error(e.message)
         | 
| 55 56 | 
             
                end
         | 
| 56 57 |  | 
| 57 58 | 
             
                def shutdown
         | 
| 58 59 | 
             
                  if socket
         | 
| 59 | 
            -
                     | 
| 60 | 
            +
                    logger.info("Shutting down")
         | 
| 60 61 |  | 
| 61 | 
            -
                    socket.close | 
| 62 | 
            +
                    socket.close
         | 
| 62 63 | 
             
                  end
         | 
| 63 64 | 
             
                end
         | 
| 64 65 |  | 
| @@ -66,10 +67,18 @@ module ForwardProxy | |
| 66 67 |  | 
| 67 68 | 
             
                attr_reader :socket, :thread_pool
         | 
| 68 69 |  | 
| 70 | 
            +
                # The following comments are from the IETF document
         | 
| 71 | 
            +
                # "Hypertext Transfer Protocol -- HTTP/1.1: Basic Rules"
         | 
| 72 | 
            +
                # https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
         | 
| 73 | 
            +
             | 
| 69 74 | 
             
                METHOD_CONNECT = "CONNECT"
         | 
| 70 75 | 
             
                METHOD_GET = "GET"
         | 
| 71 76 | 
             
                METHOD_POST = "POST"
         | 
| 72 77 |  | 
| 78 | 
            +
                # HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all
         | 
| 79 | 
            +
                # protocol elements except the entity-body.
         | 
| 80 | 
            +
                HEADER_EOP = "\r\n"
         | 
| 81 | 
            +
             | 
| 73 82 | 
             
                # The following comments are from the IETF document
         | 
| 74 83 | 
             
                # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
         | 
| 75 84 | 
             
                # https://tools.ietf.org/html/rfc7231#section-4.3.6
         | 
| @@ -102,7 +111,10 @@ module ForwardProxy | |
| 102 111 | 
             
                  # blank line that concludes the successful response's header section;
         | 
| 103 112 | 
             
                  # data received after that blank line is from the server identified by
         | 
| 104 113 | 
             
                  # the request-target.
         | 
| 105 | 
            -
                  client_conn.write  | 
| 114 | 
            +
                  client_conn.write <<~eos.chomp
         | 
| 115 | 
            +
                    HTTP/1.1 200 OK
         | 
| 116 | 
            +
                    #{HEADER_EOP}
         | 
| 117 | 
            +
                  eos
         | 
| 106 118 |  | 
| 107 119 | 
             
                  # The CONNECT method requests that the recipient establish a tunnel to
         | 
| 108 120 | 
             
                  # the destination origin server identified by the request-target and,
         | 
| @@ -119,17 +131,37 @@ module ForwardProxy | |
| 119 131 | 
             
                def handle(client_conn, req)
         | 
| 120 132 | 
             
                  Net::HTTP.start(req.host, req.port) do |http|
         | 
| 121 133 | 
             
                    http.request(map_webrick_to_net_http_req(req)) do |resp|
         | 
| 134 | 
            +
                      # The following comments are from the IETF document
         | 
| 135 | 
            +
                      # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
         | 
| 136 | 
            +
                      # https://tools.ietf.org/html/rfc7231#section-4.3.6
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                      # An intermediary MAY combine an ordered subsequence of Via header
         | 
| 139 | 
            +
                      # field entries into a single such entry if the entries have identical
         | 
| 140 | 
            +
                      # received-protocol values.  For example,
         | 
| 141 | 
            +
                      #
         | 
| 142 | 
            +
                      #   Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy
         | 
| 143 | 
            +
                      #
         | 
| 144 | 
            +
                      # could be collapsed to
         | 
| 145 | 
            +
                      #
         | 
| 146 | 
            +
                      #   Via: 1.0 ricky, 1.1 mertz, 1.0 lucy
         | 
| 147 | 
            +
                      #
         | 
| 148 | 
            +
                      # A sender SHOULD NOT combine multiple entries unless they are all
         | 
| 149 | 
            +
                      # under the same organizational control and the hosts have already been
         | 
| 150 | 
            +
                      # replaced by pseudonyms.  A sender MUST NOT combine entries that have
         | 
| 151 | 
            +
                      # different received-protocol values.
         | 
| 152 | 
            +
                      headers = resp.to_hash.merge(Via: [HEADER_VIA, resp['Via']].compact.join(', '))
         | 
| 153 | 
            +
             | 
| 122 154 | 
             
                      client_conn.puts <<~eos.chomp
         | 
| 123 155 | 
             
                        HTTP/1.1 #{resp.code}
         | 
| 124 | 
            -
                         | 
| 125 | 
            -
                        #{ | 
| 156 | 
            +
                        #{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
         | 
| 157 | 
            +
                        #{HEADER_EOP}
         | 
| 126 158 | 
             
                      eos
         | 
| 127 159 |  | 
| 128 | 
            -
                      # The following comments are taken from: | 
| 160 | 
            +
                      # The following comments are taken from:
         | 
| 129 161 | 
             
                      # https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Streaming+Response+Bodies
         | 
| 130 | 
            -
             | 
| 162 | 
            +
             | 
| 131 163 | 
             
                      # 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 | 
| 164 | 
            +
                      # handling large files or wish to implement a progress bar you can
         | 
| 133 165 | 
             
                      # instead stream the body directly to an IO.
         | 
| 134 166 | 
             
                      resp.read_body do |chunk|
         | 
| 135 167 | 
             
                        client_conn << chunk
         | 
| @@ -138,6 +170,27 @@ module ForwardProxy | |
| 138 170 | 
             
                  end
         | 
| 139 171 | 
             
                end
         | 
| 140 172 |  | 
| 173 | 
            +
                def handle_error(client_conn, err)
         | 
| 174 | 
            +
                  status_code = case err
         | 
| 175 | 
            +
                                when Errors::ConnectionTimeoutError then 504
         | 
| 176 | 
            +
                                else
         | 
| 177 | 
            +
                                  502
         | 
| 178 | 
            +
                                end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  client_conn.puts <<~eos.chomp
         | 
| 181 | 
            +
                    HTTP/1.1 #{status_code}
         | 
| 182 | 
            +
                    Via: #{HEADER_VIA}
         | 
| 183 | 
            +
                    #{HEADER_EOP}
         | 
| 184 | 
            +
                  eos
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                  logger.error(err.message)
         | 
| 187 | 
            +
                  logger.debug(err.backtrace.join("\n"))
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                def default_logger
         | 
| 191 | 
            +
                  Logger.new(STDOUT, level: :info)
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 141 194 | 
             
                def map_webrick_to_net_http_req(req)
         | 
| 142 195 | 
             
                  req_headers = Hash[req.header.map { |k, v| [k, v.first] }]
         | 
| 143 196 |  | 
| @@ -154,7 +207,7 @@ module ForwardProxy | |
| 154 207 | 
             
                def transfer(src_conn, dest_conn)
         | 
| 155 208 | 
             
                  IO.copy_stream(src_conn, dest_conn)
         | 
| 156 209 | 
             
                rescue => e
         | 
| 157 | 
            -
                   | 
| 210 | 
            +
                  logger.warn(e.message)
         | 
| 158 211 | 
             
                end
         | 
| 159 212 |  | 
| 160 213 | 
             
                def parse_req(client_conn)
         | 
| @@ -164,9 +217,5 @@ module ForwardProxy | |
| 164 217 | 
             
                rescue => e
         | 
| 165 218 | 
             
                  throw Errors::HTTPParseError.new(e.message)
         | 
| 166 219 | 
             
                end
         | 
| 167 | 
            -
             | 
| 168 | 
            -
                def log(str, level = 'INFO')
         | 
| 169 | 
            -
                  puts "[#{Time.now}] #{level} #{str}"
         | 
| 170 | 
            -
                end
         | 
| 171 220 | 
             
              end
         | 
| 172 221 | 
             
            end
         | 
| @@ -1,21 +1,18 @@ | |
| 1 1 | 
             
            module ForwardProxy
         | 
| 2 2 | 
             
              class ThreadPool
         | 
| 3 | 
            -
                attr_reader :queue, : | 
| 3 | 
            +
                attr_reader :queue, :size
         | 
| 4 4 |  | 
| 5 5 | 
             
                def initialize(size)
         | 
| 6 | 
            -
                  @size | 
| 7 | 
            -
                  @queue | 
| 8 | 
            -
                  @threads = []
         | 
| 6 | 
            +
                  @size  = size
         | 
| 7 | 
            +
                  @queue = Queue.new
         | 
| 9 8 | 
             
                end
         | 
| 10 9 |  | 
| 11 10 | 
             
                def start
         | 
| 12 11 | 
             
                  size.times do
         | 
| 13 | 
            -
                     | 
| 14 | 
            -
                       | 
| 15 | 
            -
                         | 
| 16 | 
            -
             | 
| 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
         | 
    
        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. | 
| 4 | 
            +
              version: 0.6.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- | 
| 11 | 
            +
            date: 2021-07-17 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: Forward proxy using just Ruby standard libraries.
         | 
| 14 14 | 
             
            email:
         | 
| @@ -32,6 +32,7 @@ files: | |
| 32 32 | 
             
            - exe/forward-proxy
         | 
| 33 33 | 
             
            - forward-proxy.gemspec
         | 
| 34 34 | 
             
            - lib/forward_proxy.rb
         | 
| 35 | 
            +
            - lib/forward_proxy/errors/connection_timeout_error.rb
         | 
| 35 36 | 
             
            - lib/forward_proxy/errors/http_method_not_implemented.rb
         | 
| 36 37 | 
             
            - lib/forward_proxy/errors/http_parse_error.rb
         | 
| 37 38 | 
             
            - lib/forward_proxy/server.rb
         |