binproxy 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|