binproxy 1.0.0

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