#!/usr/bin/env -S ruby -w # frozen_string_literal: true
require 'csv' require 'eventmachine' require 'optparse' require 'pathname' require 'socket' require 'time'
op = OptionParser.new do |opts|
opts.banner = "Usage: proxxxy [options]\n" \ 'https proxy' opts.summary_width = 20 opts.separator '' opts.separator 'Options' opts.on('-h', '--host HOST', String, 'Bind to host (default: 0.0.0.0)') opts.on('-p', '--port PORT', OptionParser::DecimalInteger, 'Use port (default: 3128)') do |port| raise OptionParser::InvalidArgument, port unless (0..65535).cover?(port) port end opts.on('-s', '--socket FILE', String, 'Bind to unix domain socket') opts.on('-q', '--quiet', 'Disable logging') opts.on('-v', '--version', 'Show version and exit') do version = Pathname(__dir__).join('VERSION').read puts(version) exit end opts.on_tail('--help', 'Print this help') do puts opts exit end
end
begin
op.parse!(into: opts = {})
rescue OptionParser::ParseError => e
op.abort(e)
else
if opts[:socket] if opts[:host] || opts[:port] op.abort('host/port and socket are mutually exclusive') end else opts[:host] ||= '0.0.0.0' opts[:port] ||= 3128 end
end
class Client < EventMachine::Connection
attr_accessor :quiet def post_init @client_addr = client_addr @buf = nil @host = nil @port = nil @server = nil end def receive_data(data) return if @server @buf ? @buf << data : @buf = data return unless @buf.match?(/\r?\n\r?\n/) match = @buf.match( /\ACONNECT ([^:]+):([1-9][0-9]*) (HTTP\/[0-1]\.\d+).*\r?\n\r?\n/m ) unless match log('failure', "request: #{@buf[0, 32].inspect[1...-1]}") close_connection return end @buf = nil @host, @port, @httpv = match.captures begin @server = EventMachine.connect(@host, @port, Server, self) rescue EventMachine::ConnectionError => e log('failure', e) close_connection rescue RangeError log('failure', 'invalid port') close_connection else proxy_incoming_to(@server) @server.proxy_incoming_to(self) @server.send_data(match.post_match) end end def send_ok send_data("#{@httpv} 200 Connection established\r\n\r\n") end def unbind return unless @server if @server.error log('failure', 'server connection error') else log('success', get_proxied_bytes) end @server.close_connection_after_writing @server = nil end private def client_addr addrinfo = Addrinfo.new(get_peername) rescue RuntimeError nil else addrinfo.unix? ? '-' : addrinfo.inspect_sockaddr end def log(status, comment) return if quiet time = Time.now.iso8601(3) server = @host && @port ? "#{@host}:#{@port}" : '-' row = [time, @client_addr, server, status, comment.to_s] puts CSV.generate_line(row, col_sep: ' ') end
end
class Server < EventMachine::Connection
attr_reader :error def initialize(client) super @client = client @connected = false @error = false end def connection_completed @connected = true @client.send_ok end def unbind @error = true unless @connected @client.close_connection_after_writing end
end
EventMachine.epoll EventMachine.run do
trap('INT') { EventMachine.stop } host = opts[:host] || opts[:socket] port = opts[:port] begin EventMachine.start_server(host, port, Client) do |client| client.quiet = opts[:quiet] end rescue RuntimeError => e EventMachine.stop op.abort(e) end
end
# vim: ft=ruby