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.
@@ -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