binproxy 1.0.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 +7 -0
- data/bin/binproxy +135 -0
- data/lib/binproxy.rb +2 -0
- data/lib/binproxy/bd_util.rb +267 -0
- data/lib/binproxy/bindata.rb +44 -0
- data/lib/binproxy/class_loader.rb +64 -0
- data/lib/binproxy/connection.rb +105 -0
- data/lib/binproxy/connection/filters.rb +190 -0
- data/lib/binproxy/logger.rb +17 -0
- data/lib/binproxy/parser.rb +76 -0
- data/lib/binproxy/parsers/chat_demo.rb +14 -0
- data/lib/binproxy/parsers/dns.rb +70 -0
- data/lib/binproxy/parsers/dumb_http.rb +28 -0
- data/lib/binproxy/parsers/msgpack.rb +56 -0
- data/lib/binproxy/parsers/plain_text.rb +4 -0
- data/lib/binproxy/parsers/raw_message.rb +4 -0
- data/lib/binproxy/parsers/x11_proto.rb +134 -0
- data/lib/binproxy/parsers/zmq.rb +62 -0
- data/lib/binproxy/proxy.rb +242 -0
- data/lib/binproxy/proxy_event.rb +57 -0
- data/lib/binproxy/proxy_message.rb +191 -0
- data/lib/binproxy/session.rb +47 -0
- data/lib/binproxy/web_console.rb +194 -0
- data/public/bright_squares.png +0 -0
- data/public/ui/app.js +54910 -0
- data/public/ui/fixed-data-table.css +509 -0
- data/views/application.scss +335 -0
- data/views/common.scss +90 -0
- data/views/config.haml +54 -0
- data/views/config.scss +15 -0
- data/views/index.haml +15 -0
- metadata +325 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
module BinProxy
|
2
|
+
class ClassLoader
|
3
|
+
include BinProxy::Logger
|
4
|
+
|
5
|
+
def initialize(root_path)
|
6
|
+
@root_path = root_path
|
7
|
+
end
|
8
|
+
|
9
|
+
def load_class(class_name, explicit_file_path = nil)
|
10
|
+
#unload top-level module for old class
|
11
|
+
#XXX This is a bit aggressive, maybe need a manual param to tune it?
|
12
|
+
top_level_name = class_name.split('::')[0]
|
13
|
+
old_const = Object.send(:remove_const, top_level_name) if Object.const_defined?(top_level_name)
|
14
|
+
try_load_class(class_name, explicit_file_path)
|
15
|
+
rescue StandardError
|
16
|
+
Object.const_set(top_level_name, old_const) if old_const
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def try_load_class(class_name, explicit_file_path)
|
22
|
+
file_path = explicit_file_path.presence || find_file_for_class(class_name)
|
23
|
+
log.info "Loading class file: #{File.absolute_path(file_path)}"
|
24
|
+
load File.absolute_path(file_path)
|
25
|
+
return class_name.constantize
|
26
|
+
rescue LoadError => e
|
27
|
+
log.error "Unexpected LoadError: #{e}" # This shouldn't happen except in weird cases like bad permissions or races
|
28
|
+
raise StandardError.new "Couldn't load class file '#{file_path}'"
|
29
|
+
rescue NameError => e
|
30
|
+
raise StandardError.new "Loaded file '#{file_path}' successfully, but class '#{class_name}' not found."
|
31
|
+
end
|
32
|
+
|
33
|
+
def unstack_path(p,arr)
|
34
|
+
arr << p
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_file_for_class(class_name)
|
38
|
+
un = class_name.underscore + ".rb"
|
39
|
+
names = [un.dup]
|
40
|
+
loop do
|
41
|
+
m = un.match %r|^(.+)/[^/]+\.rb$|
|
42
|
+
break unless m
|
43
|
+
un = m[1] + ".rb"
|
44
|
+
names << un
|
45
|
+
end
|
46
|
+
# this does some extra work
|
47
|
+
fn = names.map {|n| find_file(n) }.find {|f| f }
|
48
|
+
unless fn
|
49
|
+
raise StandardError.new "Could not find any of #{names.inspect} for #{class_name}"
|
50
|
+
end
|
51
|
+
return fn
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_file(fn)
|
55
|
+
[
|
56
|
+
"./#{fn}",
|
57
|
+
"./lib/#{fn}",
|
58
|
+
"#{@root_path}/lib/binproxy/parsers/#{fn}",
|
59
|
+
"#{@root_path}/test/#{fn}"
|
60
|
+
].find {|f| File.exists? f}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'observer'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'ipaddr'
|
5
|
+
|
6
|
+
require_relative 'proxy_message'
|
7
|
+
require_relative 'logger'
|
8
|
+
require_relative 'connection/filters'
|
9
|
+
|
10
|
+
module BinProxy
|
11
|
+
# This module is included in an anonymous subclass of EM::Connection; each
|
12
|
+
# instance represents a TCP connection between the proxy and the client or
|
13
|
+
# server, so each Session has two Connections.
|
14
|
+
module Connection
|
15
|
+
include BinProxy::Logger
|
16
|
+
include Observable
|
17
|
+
|
18
|
+
attr_accessor :parser
|
19
|
+
attr_reader :opts, :peer, :filters
|
20
|
+
|
21
|
+
def initialize(opts)
|
22
|
+
@opts = opts
|
23
|
+
@peer = opts[:peer] # :client or :server
|
24
|
+
@buffer = StringIO.new
|
25
|
+
@filters = opts[:filter_classes].map do |c| c.new(self) end
|
26
|
+
end
|
27
|
+
|
28
|
+
def post_init
|
29
|
+
@filters.each do |f|
|
30
|
+
log.debug "initializing filter #{f}"
|
31
|
+
f.init
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Used by filters to initiate upstream connection in response to
|
36
|
+
# inbound connection
|
37
|
+
def connect(host=nil, port=nil, &cb)
|
38
|
+
host ||= opts[:upstream_host] || raise('no upstream host')
|
39
|
+
port ||= opts[:upstream_port] || raise('no upstream port')
|
40
|
+
cb ||= lambda { |conn| opts[:session_callback].call(self, conn) }
|
41
|
+
log.debug "Making upstream connection to #{host}:#{port}"
|
42
|
+
EM.connect(host, port, Connection, opts[:upstream_args], &cb)
|
43
|
+
end
|
44
|
+
|
45
|
+
# EM callback
|
46
|
+
def connection_completed
|
47
|
+
log.debug "connection_completed callback"
|
48
|
+
changed
|
49
|
+
notify_observers(:connection_completed, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
# called by session
|
53
|
+
def upstream_connected(upstream_conn)
|
54
|
+
@filters.each do |f|
|
55
|
+
f.upstream_connected(upstream_conn)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def receive_data(data)
|
60
|
+
@filters.each do |f|
|
61
|
+
data = f.read data
|
62
|
+
return if data.nil? or data == ''
|
63
|
+
end
|
64
|
+
|
65
|
+
@buffer.string << data #does not update @buffer's pos
|
66
|
+
|
67
|
+
parser.parse @buffer, peer do |pm|
|
68
|
+
log.debug "parsed proxy message: #{pm.inspect}"
|
69
|
+
changed
|
70
|
+
notify_observers(:message_received, pm)
|
71
|
+
end
|
72
|
+
|
73
|
+
if (pos = @buffer.pos) > 0
|
74
|
+
@buffer.string = @buffer.string[pos .. -1] #resets pos to 0
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
rescue Exception => e
|
79
|
+
puts e, e.backtrace
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
|
83
|
+
# called with a ProxyMessage
|
84
|
+
def send_message(pm)
|
85
|
+
log.error "OOPS! message going the wrong way (to #{peer})" if pm.dest != peer
|
86
|
+
|
87
|
+
data = pm.to_binary_s
|
88
|
+
@filters.each do |f|
|
89
|
+
data = f.write data
|
90
|
+
return if data.nil? or data == ''
|
91
|
+
end
|
92
|
+
send_data(data)
|
93
|
+
end
|
94
|
+
|
95
|
+
def unbind(reason)
|
96
|
+
log.debug "unbind called"
|
97
|
+
changed
|
98
|
+
notify_observers(:connection_lost, peer, reason)
|
99
|
+
rescue Exception => e
|
100
|
+
puts e, e.backtrace
|
101
|
+
raise e
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module BinProxy::Connection; end
|
2
|
+
module BinProxy::Connection::Filters
|
3
|
+
class Base
|
4
|
+
attr_reader :conn
|
5
|
+
def initialize(connection)
|
6
|
+
@conn = connection
|
7
|
+
end
|
8
|
+
|
9
|
+
def init; end
|
10
|
+
def upstream_connected(upstream_conn); end
|
11
|
+
def session_closing(reason); end
|
12
|
+
|
13
|
+
def read(data); data; end
|
14
|
+
def write(data); data; end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Fortunately, we don't have to implement TLS ourself, just tell EM to
|
18
|
+
# use it on opening the connection.
|
19
|
+
#
|
20
|
+
# TODO: The "magical" nature of the start_tls connection upgrade doesn't play
|
21
|
+
# well with the filter concept, data might be buffered into filters before
|
22
|
+
# start_tls happens. There's also no way to do STARTTLS-like protocols that
|
23
|
+
# pass plaintext data all the way through.
|
24
|
+
class InboundTLS < Base
|
25
|
+
include BinProxy::Logger
|
26
|
+
def init
|
27
|
+
@state = :new
|
28
|
+
end
|
29
|
+
def upstream_connected(upstream_conn)
|
30
|
+
#TODO no way to set tls_args for upstream connection currently
|
31
|
+
conn.start_tls(conn.opts[:tls_args]||{})
|
32
|
+
@state = :tls
|
33
|
+
end
|
34
|
+
def read(data)
|
35
|
+
if @state != :tls
|
36
|
+
#XXX we might want this in the case of STARTTLS?
|
37
|
+
log.fatal "DATA RECEIVED BY FILTER BEFORE START_TLS #{conn}"
|
38
|
+
end
|
39
|
+
data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
class UpstreamTLS < Base
|
43
|
+
def init
|
44
|
+
#TODO no way to set tls_args for upstream connection currently
|
45
|
+
conn.start_tls(conn.opts[:tls_args]||{})
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Logger < Base
|
50
|
+
include BinProxy::Logger
|
51
|
+
def init
|
52
|
+
log.debug "CONNECTION CREATED #{conn}"
|
53
|
+
end
|
54
|
+
def read(data)
|
55
|
+
log.debug "READ #{conn}\n#{data.hexdump}"
|
56
|
+
data
|
57
|
+
end
|
58
|
+
def write(data)
|
59
|
+
log.debug "WRITE #{conn}\n#{data.hexdump}"
|
60
|
+
data
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class StaticUpstream < Base
|
65
|
+
def init
|
66
|
+
conn.connect
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Socks < Base
|
71
|
+
SOCKS_OK = "\x00\x5a" + "\x00" * 6
|
72
|
+
SOCKS_ERR = "\x00\x5b" + "\x00" * 6
|
73
|
+
include BinProxy::Logger
|
74
|
+
attr_reader :socks_state, :header
|
75
|
+
class ClientHeader < BinData::Record #TODO just handles v4/v4a for now
|
76
|
+
uint8 :version
|
77
|
+
uint8 :command_code
|
78
|
+
uint16be :port
|
79
|
+
uint32be :ip
|
80
|
+
stringz :user
|
81
|
+
stringz :host, onlyif: :bogus_ip?
|
82
|
+
|
83
|
+
def host_or_ip
|
84
|
+
if host? then host else IPAddr.new(ip, Socket::AF_INET).to_s end
|
85
|
+
end
|
86
|
+
|
87
|
+
def bogus_ip?
|
88
|
+
# an IP value of 0.0.0.x (x > 0) is a SOCKSv4a flag for server-side DNS.
|
89
|
+
ip > 0 && ip <= 255
|
90
|
+
end
|
91
|
+
end
|
92
|
+
def init
|
93
|
+
@buf = StringIO.new
|
94
|
+
@state = :new
|
95
|
+
end
|
96
|
+
def read(data)
|
97
|
+
return data unless @state == :new
|
98
|
+
|
99
|
+
@buf.string << data
|
100
|
+
@header = ClientHeader.read(@buf)
|
101
|
+
|
102
|
+
# no exception means we've read a full header...
|
103
|
+
log.debug "Read SOCKS header #{@header}"
|
104
|
+
@state = :connecting
|
105
|
+
|
106
|
+
conn.connect @header.host_or_ip, @header.port
|
107
|
+
|
108
|
+
# return any extra data
|
109
|
+
@buf.read
|
110
|
+
rescue EOFError, IOError
|
111
|
+
#partial read of header, reset to try again on next packet
|
112
|
+
@buf.pos = 0
|
113
|
+
nil
|
114
|
+
rescue EM::ConnectionError => e
|
115
|
+
#synchronous error when connecting upstream, e.g. bogus hostname
|
116
|
+
log.warn "Can't connect to '#{@header.host_or_ip}': #{e.message}"
|
117
|
+
#TODO -close the connection
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
def upstream_connected(upstream_conn)
|
121
|
+
log.error "unexpected upstream_connected in state #{@state}" unless @state == :connecting
|
122
|
+
@state = :connected
|
123
|
+
conn.send_data SOCKS_OK
|
124
|
+
end
|
125
|
+
def session_closing(reason)
|
126
|
+
conn.send_data SOCKS_ERR if @state == :connecting
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#TODO - lots of copy-paste from SOCKS, could stand to refactor
|
131
|
+
class HTTPConnect < Base
|
132
|
+
include BinProxy::Logger
|
133
|
+
def init
|
134
|
+
@buf = StringIO.new
|
135
|
+
@state = :new
|
136
|
+
end
|
137
|
+
def read(data)
|
138
|
+
log.debug "HTTPConnect read data #{data.inspect} in state #{@state}"
|
139
|
+
return data if @state == :connected
|
140
|
+
raise "unexpected data while connecting" if @state == :connecting
|
141
|
+
|
142
|
+
#append, but keep current position
|
143
|
+
p = @buf.pos
|
144
|
+
@buf << data
|
145
|
+
@buf.pos = p
|
146
|
+
|
147
|
+
while line = @buf.gets #XXX assumes we get whole lines
|
148
|
+
log.debug "processing line #{line}, state=#{@state}"
|
149
|
+
case @state
|
150
|
+
when :new
|
151
|
+
if m = line.match( %r<\ACONNECT ([\w.-]+):(\d+) HTTP/1.1\r\n\z> )
|
152
|
+
@host, @port = m[1], m[2]
|
153
|
+
@state = :headers
|
154
|
+
log.debug "Got CONNECT message to #{@host}:#{@port}"
|
155
|
+
else
|
156
|
+
log.warn "expected a CONNECT request, got #{line.inspect}"
|
157
|
+
end
|
158
|
+
when :headers
|
159
|
+
if line == "\r\n"
|
160
|
+
log.debug "End of CONNECT headers"
|
161
|
+
@state = :connecting
|
162
|
+
conn.connect @host, @port
|
163
|
+
return nil #XXX TODO confirm that @buf is empty
|
164
|
+
else
|
165
|
+
log.debug "Extra header on CONNECT: #{line.inspect}"
|
166
|
+
end
|
167
|
+
else
|
168
|
+
log.fatal "HTTPConnect filter in bad state: #{@state}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
log.debug "loop terminated with line #{line.inspect}"
|
172
|
+
|
173
|
+
#not done with CONNECT yet
|
174
|
+
nil
|
175
|
+
rescue EM::ConnectionError => e
|
176
|
+
#synchronous error when connecting upstream, e.g. bogus hostname
|
177
|
+
log.warn "Can't connect to '#{m[1]}:#{m[2]}': #{e.message}"
|
178
|
+
#TODO -close the connection
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
def upstream_connected(upstream_conn)
|
182
|
+
log.error "unexpected upstream_connected in state #{@state}, conn=#{conn}" unless @state == :connecting
|
183
|
+
@state = :connected
|
184
|
+
conn.send_data "HTTP/1.1 200 BINPROXY OK\r\n\r\n"
|
185
|
+
end
|
186
|
+
def session_closing(reason)
|
187
|
+
conn.send_data "HTTP/1.1 502 BINPROXY FAIL\r\n\r\n" if @state == :connecting
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'logger'
|
2
|
+
module BinProxy; end
|
3
|
+
module BinProxy::Logger
|
4
|
+
class BPLogger < Logger
|
5
|
+
def err_trace(e, context = nil, level = Logger::ERROR)
|
6
|
+
add level, "Error while #{context}:" if context
|
7
|
+
add level, "#{e.class}: #{e.message}\n#{(e.backtrace - caller).join "\n"}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def log
|
12
|
+
@@logger ||= BPLogger.new(STDOUT).tap do |log|
|
13
|
+
log.level = Logger::WARN
|
14
|
+
end
|
15
|
+
end
|
16
|
+
module_function :log
|
17
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative 'logger'
|
2
|
+
|
3
|
+
module BinProxy
|
4
|
+
class Parser
|
5
|
+
include BinProxy::Logger
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :proxy, :message_class, :validate
|
9
|
+
end
|
10
|
+
def message_class; self.class.message_class; end
|
11
|
+
def validate; self.class.validate; end
|
12
|
+
|
13
|
+
def self.subclass(proxy, mc)
|
14
|
+
unless mc.class == Class
|
15
|
+
BinProxy::Logger::log.fatal "#{mc} is a #{mc.class}, not a Class."
|
16
|
+
exit!
|
17
|
+
end
|
18
|
+
c = Class.new(self) do
|
19
|
+
@proxy = proxy #XXX I don't love the tight coupling here; need a better way to pass messages up the chain
|
20
|
+
@message_class = mc
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@protocol_state = message_class.initial_state
|
26
|
+
rescue => e
|
27
|
+
log.warn "Exception while getting initial state for #{message_class}: e"
|
28
|
+
if e.message.match /undefined method `initial_state'/ then
|
29
|
+
log.warn "This is possibly not a subclass of BinData::Base"
|
30
|
+
end
|
31
|
+
# try to proceed with a default value
|
32
|
+
BinData::Base.initial_state
|
33
|
+
end
|
34
|
+
|
35
|
+
# Try to parse one or more messages from the buffer, and yield them
|
36
|
+
def parse(raw_buffer, peer)
|
37
|
+
start_pos = nil
|
38
|
+
loop do
|
39
|
+
break if raw_buffer.eof?
|
40
|
+
|
41
|
+
start_pos = raw_buffer.pos
|
42
|
+
|
43
|
+
log.debug "at #{start_pos} of #{raw_buffer.length} in buffer"
|
44
|
+
|
45
|
+
read_fn = lambda { message_class.new(src: peer.to_s, protocol_state: @protocol_state).read(raw_buffer) }
|
46
|
+
|
47
|
+
message = if log.debug?
|
48
|
+
BinData::trace_reading &read_fn
|
49
|
+
else
|
50
|
+
read_fn.call
|
51
|
+
end
|
52
|
+
|
53
|
+
bytes_read = raw_buffer.pos - start_pos
|
54
|
+
log.debug "read #{bytes_read} bytes"
|
55
|
+
|
56
|
+
# Go back and grab raw bytes for validation of serialization
|
57
|
+
raw_buffer.pos = start_pos
|
58
|
+
raw_m = raw_buffer.read bytes_read
|
59
|
+
|
60
|
+
@protocol_state = message.update_state
|
61
|
+
log.debug "protocol state is now #{@protocol_state.inspect}"
|
62
|
+
|
63
|
+
pm = ProxyMessage.new(raw_m, message)
|
64
|
+
pm.src = peer
|
65
|
+
yield pm
|
66
|
+
end
|
67
|
+
rescue EOFError, IOError
|
68
|
+
log.info "Hit end of buffer while parsing. Consumed #{raw_buffer.pos - start_pos} bytes."
|
69
|
+
raw_buffer.pos = start_pos #rewind partial read
|
70
|
+
#todo, warn to client if validate flag set?
|
71
|
+
rescue Exception => e
|
72
|
+
log.err_trace(e, 'parsing message (probably an issue with user BinData class)', ::Logger::WARN)
|
73
|
+
self.class.proxy.on_bindata_error('parsing', e)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|