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,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