binproxy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ module BinProxy
2
+ class ProxyBaseItem
3
+ include BinProxy::Logger
4
+ attr_accessor :session, :disposition, :id
5
+ attr_reader :src, :dest, :time
6
+ def src=(s)
7
+ @src = s
8
+ @dest = opposite_peer(s)
9
+ end
10
+ def dest=(d)
11
+ @dest = d
12
+ @src = opposite_peer(d)
13
+ end
14
+
15
+ def initialize
16
+ @time = Time.now
17
+ end
18
+
19
+ def headers
20
+ {
21
+ message_id: @id,
22
+ session_id: @session && @session.id,
23
+ src: @src,
24
+ dest: @dest,
25
+ time: @time.to_i,
26
+ disposition: @disposition,
27
+ }
28
+ end
29
+
30
+ def to_hash
31
+ { head: headers }
32
+ end
33
+ private
34
+
35
+ def opposite_peer(p)
36
+ case p
37
+ when :client; :server
38
+ when :server; :client
39
+ else raise "invalid peer: #{p}"
40
+ end
41
+ end
42
+
43
+ end
44
+ class ProxyEvent < ProxyBaseItem
45
+ def initialize(summary)
46
+ super()
47
+ @summary = summary
48
+ @disposition = 'Info'
49
+ end
50
+
51
+ def headers
52
+ super.merge({
53
+ summary: @summary
54
+ })
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,191 @@
1
+ require 'rbkb'
2
+ require 'bindata'
3
+ require_relative 'bindata'
4
+ require_relative 'logger'
5
+ require_relative 'proxy_event'
6
+ require 'base64'
7
+
8
+ ######################################################################
9
+ # monkey patch bindata classes to add annotated_snapshot
10
+ #
11
+ # Annotated snapshots should be hashes which convert cleanly to JSON and
12
+ # have the following properties: (Note that this is somewhat different
13
+ # from the format of builtin BinData snapshots.)
14
+ #
15
+ # name: string or null
16
+ # objclass: string
17
+ # display: string or null
18
+ #
19
+ # [either]
20
+ # contents: array of annotated_snapshots
21
+ # [or]
22
+ # value: primitive type
23
+ # repr: 'raw' or 'base64'
24
+ #
25
+ ######################################################################
26
+
27
+ class BinData::Base
28
+ # Not called directly, but return value is merged by subclasses
29
+ def annotated_snapshot
30
+ {
31
+ name: nil, #things generally don't know their own name, so compound types will overwrite this.
32
+ objclass: self.class.to_s,
33
+ display: eval_parameter(:display_as)
34
+ }
35
+ end
36
+ def annotate_value(v)
37
+ if v.respond_to? :annotated_snapshot
38
+ return v.annotated_snapshot
39
+ end
40
+ v = Base64.encode64(v) if v.is_a? String
41
+ return { value: v }
42
+ end
43
+ end
44
+
45
+ class BinData::BasePrimitive
46
+ def annotated_snapshot
47
+ super.merge annotate_value(snapshot)
48
+ end
49
+ end
50
+
51
+ #XXX should we pass along which choice, include what the options were?
52
+ class BinData::Choice
53
+ def annotated_snapshot
54
+ super.merge contents: [ current_choice.annotated_snapshot ], contents_type: :hash
55
+ end
56
+ end
57
+
58
+ class BinData::Array
59
+ def annotated_snapshot
60
+ super.merge contents: elements.map { |el| el.annotated_snapshot }, contents_type: :array
61
+ end
62
+ end
63
+
64
+ class BinData::Struct
65
+ def annotated_snapshot
66
+ super.merge( contents: field_names.map do |name|
67
+ o = find_obj_for_name(name)
68
+ o.annotated_snapshot.merge name: name if include_obj?(o)
69
+ end.find_all { |x| x }, contents_type: :hash )
70
+ end
71
+ end
72
+
73
+ class BinData::Primitive
74
+ def annotated_snapshot
75
+ super.merge annotate_value(snapshot)
76
+ end
77
+ end
78
+
79
+ module BinProxy
80
+ # This class represents a message being proxied, including the
81
+ # raw bits, the BinData parsed representation, and metadata such
82
+ # as the asssociated session, which direction it's going, and whether
83
+ # it's been forwarded. [some of the above still TODO!]
84
+ class ProxyMessage < BinProxy::ProxyBaseItem
85
+ attr_accessor :message, :message_class, :force_reserialize
86
+ attr_reader :modified
87
+
88
+
89
+ def initialize(raw_bytes, parsed_message)
90
+ super()
91
+ @raw = raw_bytes
92
+ @message = parsed_message
93
+ @message_class = @message.class
94
+ @modified = false
95
+ @force_reserialize # XXX ???
96
+
97
+ if @raw != @message.to_binary_s
98
+ log.warn "WARNING, inconsistent binary representation:\n[[ORIGINAL]]\n#{@raw.hexdump}\n[[RESERIALIZED]]\n#{@message.to_binary_s.hexdump}"
99
+ log.warn "... @raw encoding is #{@raw.encoding}; to_binary_s is #{@message.to_binary_s.encoding}"
100
+ end
101
+ end
102
+
103
+ # The next two methods are the last stop before JSON encoding, so all strings
104
+ # in the returned hash must be UTF-8 compatible.
105
+
106
+ def headers
107
+ super.merge({
108
+ size: @raw.length,
109
+ # HACK - this will prevent errors, but will mangle anything that isn't
110
+ # actually utf8. We should try to handle this upstream where we might
111
+ # know what the actual encoding is.
112
+ summary: @message.summary.force_encoding('UTF-8').scrub,
113
+ message_class: @message_class.to_s,
114
+ })
115
+ end
116
+
117
+ def to_hash
118
+ {
119
+ head: headers,
120
+ body: {
121
+ snapshot: @message.annotated_snapshot,
122
+ raw: Base64.encode64(@raw)
123
+ }
124
+ }
125
+ end
126
+
127
+ def to_binary_s
128
+ if @modified or @force_reserialize
129
+ @message.to_binary_s
130
+ else
131
+ @raw
132
+ end
133
+ end
134
+
135
+ def update!(snapshot)
136
+ @modified = true
137
+ @message.assign( deannotate_snapshot(snapshot) )
138
+ end
139
+
140
+ def forward!(reason)
141
+ @session.send_message(self)
142
+ self.disposition = "Sent #{reason}"
143
+ end
144
+
145
+ def drop!(reason)
146
+ self.disposition = "Dropped #{reason}"
147
+ end
148
+
149
+ def inspect #standard inspect pulls in junk from sesssion
150
+ "#<#{self.class.to_s} #{self.to_hash}>"
151
+ end
152
+
153
+ private
154
+ #turns the output of #annotated_snapshot into the format used by
155
+ # #snapshot and #assign XXX not fully tested w/ compound elements, esp arrays
156
+ def deannotate_snapshot(s, out=nil)
157
+ val = if s.has_key? :value
158
+ if s[:value].is_a? String
159
+ Base64.decode64(s[:value])
160
+ else
161
+ s[:value]
162
+ end
163
+ elsif s.has_key? :contents
164
+ if s[:contents_type].to_s == 'hash'
165
+ s[:contents].reduce({}) {|h, c| deannotate_snapshot(c, h) }
166
+ elsif s[:contents_type].to_s == 'array'
167
+ s[:contents].reduce([]) {|a, c| deannotate_snapshot(c, a) }
168
+ else
169
+ raise "Expected hash or array for contents_type, got #{s[:contents_type]}"
170
+ end
171
+ else
172
+ raise "Snapshot has neither :value nor :contents key"
173
+ end
174
+
175
+ if out.nil? # top level item
176
+ val
177
+ elsif out.is_a? Hash
178
+ raise "Expected :name for snapshot item within a hash" unless s.has_key? :name
179
+ out[s[:name].to_sym] = val
180
+ out
181
+ elsif out.is_a? Array
182
+ out << val
183
+ else
184
+ raise "Ooops! out param s/b hash or array, but it was #{out.class}"
185
+ end
186
+ end
187
+
188
+
189
+
190
+ end
191
+ end
@@ -0,0 +1,47 @@
1
+ module BinProxy
2
+ # This class represents a pair of TCP connections (client <-> proxy and proxy
3
+ # <-> server), through which a number of messages may be sent.
4
+ class Session
5
+ include Observable
6
+ attr_reader :id, :endpoints, :open_time, :close_time
7
+
8
+ def initialize(id, client, server, parser_class)
9
+ @open_time = Time.now
10
+ @id = id
11
+ @endpoints = { client: client, server: server }
12
+ p = parser_class.new
13
+ @endpoints.each_pair do |peer, conn|
14
+ conn.parser = p
15
+ conn.add_observer(self, :send)
16
+ end
17
+ end
18
+
19
+ # should receive a ProxyMessage from Connection
20
+ def message_received(pm)
21
+ pm.session = self
22
+ changed
23
+ notify_observers(:message_received, pm)
24
+ end
25
+
26
+ def send_message(message)
27
+ endpoints[message.dest].send_message(message)
28
+ end
29
+
30
+ def connection_completed(conn)
31
+ # this is only called for the upstream connection, as the downstream connection is already completed
32
+ # by the time that the session is created
33
+ log.warn 'unexpected connection_completed on downstream' if conn.peer != :server
34
+ endpoints[:client].upstream_connected(conn)
35
+ end
36
+
37
+ def connection_lost(peer, reason)
38
+ @close_time = Time.now
39
+ # XXX Shutdown the endpoints
40
+ # - but not until we've finished passing through* any existing messages
41
+ # (this needs to be handled up a level at the proxy)
42
+ # * or dropping??
43
+ changed
44
+ notify_observers(:session_closed, self, peer, reason)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,194 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra-websocket'
3
+ require 'clipboard'
4
+
5
+ # Currently, the caller passes a block to .new which configures the Sinatra
6
+ # app. this config applies to the whole class, so technically we should
7
+ # probably use some sort of get_instance class method which builds a subclass
8
+ # for each invocation, but YAGNI.
9
+
10
+ class BinProxy::WebConsole < Sinatra::Base
11
+ include BinProxy::Logger
12
+
13
+ def self.new_instance(&blk)
14
+ c = Class.new(self)
15
+ c.configure &blk
16
+ c.new
17
+ end
18
+
19
+ #def initialize
20
+ # raise RuntimeError.new "Use WebConsole.build(&blk) instead of .new" if self.class == BinProxy::WebConsole
21
+ # super
22
+ #end
23
+
24
+ set sockets: []
25
+ set haml: { escape_html: true }
26
+
27
+ get '/' do
28
+ if request.websocket?
29
+ request.websocket {|s| WebSocketHandler.new(settings.proxy, s) }
30
+ elsif settings.proxy.status != 'running'
31
+ redirect '/config'
32
+ else
33
+ haml :index, locals: {need_config: settings.proxy.nil?}
34
+ end
35
+ end
36
+
37
+ get '/config' do
38
+ haml :config, locals: { opt_vals: settings.opts }
39
+ end
40
+
41
+ post '/config' do
42
+ params.symbolize_keys!
43
+ new_opts = settings.opts.merge params
44
+ begin
45
+ settings.opts = new_opts
46
+ settings.proxy.configure(new_opts)
47
+ redirect '/'
48
+ rescue Exception => e
49
+ log.err_trace(e, 'updating configuration')
50
+ haml :config, locals: { opt_vals: new_opts, err_msg: e.message }
51
+ end
52
+ end
53
+
54
+ post '/reload' do
55
+ content_type :json
56
+ begin
57
+ settings.proxy.configure(settings.opts) #XXX this relies on configure to always trigger reload
58
+ { success: true }.to_json
59
+ rescue Exception => e
60
+ { success: false, message: e.message, detail: e.backtrace.join("\n") }.to_json
61
+ end
62
+ end
63
+
64
+ put '/clipboard' do
65
+ content_type :json
66
+ begin
67
+ if Clipboard.implementation == Clipboard::File
68
+ return { success: false, message: "Clipboard not available. Install xclip?" }.to_json
69
+ end
70
+ log.info "Copying #{request.content_length} bytes to clipboard"
71
+ text = request.body.read
72
+ log.debug "CB Data: #{text}"
73
+ Clipboard.copy text
74
+ { success: true }.to_json
75
+ rescue Exception => e
76
+ log.error e.message + ": " + e.backtrace.join("\n")
77
+ { success: false, message: e.message, detail: e.backtrace.join("\n") }.to_json
78
+ end
79
+ end
80
+
81
+ get '/:name.css' do
82
+ begin
83
+ scss params[:name].to_sym, style: :expanded
84
+ rescue Sass::SyntaxError => e
85
+ log.err_trace(e, 'processing SCSS stylesheet')
86
+ content_type :css
87
+ "body::before { color: red; content: 'SASS Error: #{e.message} : #{e.backtrace[0]}' }"
88
+ end
89
+ end
90
+
91
+ get '/m/:id' do
92
+ content_type :json
93
+ settings.proxy.buffer[params[:id].to_i].to_hash.to_json rescue 404
94
+ end
95
+
96
+ class WebSocketHandler
97
+ include BinProxy::Logger
98
+
99
+ def initialize(proxy, socket)
100
+ @proxy = proxy
101
+ @socket = socket
102
+ @pending_messages = []
103
+
104
+ socket.onopen { self.onopen }
105
+ socket.onmessage {|m| self.onmessage(m) }
106
+ socket.onclose { self.onclose }
107
+ end
108
+
109
+ def socket_send(type, data)
110
+ @socket.send( JSON.generate({
111
+ type: type,
112
+ data: data
113
+ }, max_nesting: 99)) #XXX
114
+ end
115
+
116
+ def onopen
117
+ @proxy.add_observer(self, :send)
118
+ socket_send :message_count, @proxy.history_size
119
+ end
120
+
121
+ def onmessage(raw_ws_message)
122
+ log.debug "websocket message received: #{raw_ws_message}"
123
+ ws_message = JSON.parse(raw_ws_message, symbolize_names: true)
124
+ case ws_message[:action]
125
+ when 'ping'
126
+ socket_send :pong, status: @proxy.status
127
+ when 'forward'
128
+ message = @proxy.update_message_from_hash(ws_message[:message])
129
+ @proxy.send_message(message, :manual)
130
+ socket_send :update, message.to_hash #XXX send all of this, or just update?
131
+ when 'drop'
132
+ #XXX just doing this to get the message object
133
+ message = @proxy.update_message_from_hash(ws_message[:message])
134
+ @proxy.drop_message(message, :manual)
135
+ socket_send :update, message.to_hash #XXX same as above
136
+ when 'setIntercept'
137
+ log.debug "setIntercept: #{ws_message[:value]}"
138
+ @proxy.hold = ws_message[:value]
139
+ when 'load'
140
+ log.debug "load: #{ws_message[:value]}"
141
+ socket_send_message @proxy.buffer[ws_message[:value]]
142
+ when 'getHistory'
143
+ log.error 'unexpected getHistory'
144
+ # log.debug "getHistory"
145
+ # @proxy.buffer.each do |message|
146
+ # socket_send_message message
147
+ # end
148
+ when 'reloadParser'
149
+ log.debug 'reloadParser'
150
+ begin
151
+ @proxy.configure #XXX this relies on configure to always trigger reload, which is considered a bug
152
+ socket_send :info, message: 'Parser Reloaded'
153
+ rescue Exception => e
154
+ socket_send :error, message: "Parser Reload Failed: #{e.message}", detail: e.backtrace
155
+ log.err_trace(e, 'Reloading Parser')
156
+ end
157
+ else
158
+ log.error 'Unexpected WS message: ' + ws_message.inspect
159
+ end
160
+ log.debug 'Finished processing WS message'
161
+ #rescue Exception => e
162
+ # puts "caught #{e}", e.backtrace
163
+ end
164
+
165
+ def onclose
166
+ @proxy.delete_observer(self)
167
+ #rescue Exception => e
168
+ # puts "caught #{e}", e.backtrace
169
+ end
170
+
171
+ def message_received(message)
172
+ log.debug "sending WS message to front-end for message #{message.id}"
173
+ socket_send_message message
174
+ end
175
+
176
+ def session_event(event)
177
+ log.debug "session event #{event}"
178
+ socket_send :event, event.to_hash
179
+ #TODO delete observer if event is connection close
180
+ end
181
+
182
+ def bindata_error(operation, err)
183
+ socket_send :error, {
184
+ message: "Internal Error in #{operation}: #{err.class}: #{err.message}",
185
+ detail: err.backtrace.join("\n")
186
+ }
187
+ end
188
+
189
+ private
190
+ def socket_send_message(message)
191
+ socket_send :message, message.to_hash
192
+ end
193
+ end
194
+ end