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