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