jscmd 0.0.2
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.
- data/COPYING +56 -0
- data/GPL +340 -0
- data/History.txt +7 -0
- data/Manifest.txt +16 -0
- data/README.txt +6 -0
- data/Rakefile +89 -0
- data/bin/jscmd +44 -0
- data/lib/jscmd.rb +1 -0
- data/lib/jscmd/agent.js +253 -0
- data/lib/jscmd/asynchttpproxy.rb +140 -0
- data/lib/jscmd/jscommander.rb +339 -0
- data/lib/jscmd/version.rb +9 -0
- data/scripts/txt2html +67 -0
- data/setup.rb +1585 -0
- data/test/test_helper.rb +2 -0
- data/test/test_jscmd.rb +94 -0
- metadata +62 -0
data/bin/jscmd
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#! /opt/local/bin/ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
rescue LoadError
|
6
|
+
# no rubygems to load, so we fail silently
|
7
|
+
end
|
8
|
+
|
9
|
+
if File.exist?(File.dirname(__FILE__) + "/../lib/jscmd.rb")
|
10
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'jscmd'
|
14
|
+
require 'optparse'
|
15
|
+
|
16
|
+
OPTIONS = {
|
17
|
+
:port => 9000
|
18
|
+
}
|
19
|
+
MANDATORY_OPTIONS = %w()
|
20
|
+
|
21
|
+
parser = OptionParser.new do |opts|
|
22
|
+
opts.banner = <<BANNER
|
23
|
+
JS Commander: Remote JavaScript Console
|
24
|
+
|
25
|
+
Usage: #{File.basename($0)} [options]
|
26
|
+
|
27
|
+
Options are:
|
28
|
+
BANNER
|
29
|
+
opts.separator ""
|
30
|
+
opts.on("-p", "--port=PORT", Integer,
|
31
|
+
"Start proxy server on the specified port.",
|
32
|
+
"Default: 9000") { |OPTIONS[:port]| }
|
33
|
+
opts.on("-h", "--help",
|
34
|
+
"Show this help message.") { puts opts; exit }
|
35
|
+
opts.parse!(ARGV)
|
36
|
+
|
37
|
+
if MANDATORY_OPTIONS && MANDATORY_OPTIONS.find { |option| OPTIONS[option.to_sym].nil? }
|
38
|
+
puts opts; exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# do stuff
|
43
|
+
server = JSCommander::Broker.new(:Port => OPTIONS[:port], :AccessLog => [], :ProxyVia => nil)
|
44
|
+
server.start
|
data/lib/jscmd.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__), 'jscmd/**/*.rb')].sort.each { |lib| require lib }
|
data/lib/jscmd/agent.js
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
({
|
2
|
+
createXHR: function() {
|
3
|
+
if (window.ActiveXObject) {
|
4
|
+
try {
|
5
|
+
return new ActiveXObject("Msxml2.XMLHTTP");
|
6
|
+
} catch (e) {
|
7
|
+
try {
|
8
|
+
return new ActiveXObject("Microsoft.XMLHTTP");
|
9
|
+
} catch (e2) {
|
10
|
+
return null;
|
11
|
+
}
|
12
|
+
}
|
13
|
+
} else if (window.XMLHttpRequest) {
|
14
|
+
return new XMLHttpRequest();
|
15
|
+
} else {
|
16
|
+
return null;
|
17
|
+
}
|
18
|
+
},
|
19
|
+
|
20
|
+
queue: [],
|
21
|
+
|
22
|
+
evaluate: function(script, recordValue) {
|
23
|
+
function dir(o) {
|
24
|
+
var list = [];
|
25
|
+
var maxNameLen = 0;
|
26
|
+
var valueLenLimit = 200;
|
27
|
+
for (name in o) {
|
28
|
+
var value = null;
|
29
|
+
try {
|
30
|
+
value = "" + (o[name] != null ? o[name] : null);
|
31
|
+
} catch (e) {
|
32
|
+
value = "[Error: " + e + "]";
|
33
|
+
// value = "[Error]";
|
34
|
+
}
|
35
|
+
value = value.replace(/\\/g, "\\\\");
|
36
|
+
value = value.replace(/\r/g, "");
|
37
|
+
value = value.replace(/\n/g, "\\n");
|
38
|
+
if (value.length > valueLenLimit) value = value.substring(0, valueLenLimit) + "...";
|
39
|
+
list.push([name, value]);
|
40
|
+
if (name.length > maxNameLen) maxNameLen = name.length;
|
41
|
+
}
|
42
|
+
for (var i = 0; i < list.length; i++) {
|
43
|
+
var name = list[i][0], value = list[i][1];
|
44
|
+
list[i] = name + (new Array(maxNameLen + 3 - name.length).join(" ")) + value;
|
45
|
+
}
|
46
|
+
return list.sort().join("\n");
|
47
|
+
}
|
48
|
+
var $_ = this.lastValue;
|
49
|
+
var value = eval(script);
|
50
|
+
if (recordValue) {
|
51
|
+
this.lastValue = value;
|
52
|
+
}
|
53
|
+
return value;
|
54
|
+
},
|
55
|
+
|
56
|
+
poll: function() {
|
57
|
+
var content = this.queue.length > 0 ? this.queue.shift() : "";
|
58
|
+
var xhr = this.createXHR();
|
59
|
+
xhr.open("POST", "/_remote_js_proxy/poll", true);
|
60
|
+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
61
|
+
var self = this;
|
62
|
+
xhr.onreadystatechange = function() {
|
63
|
+
if (xhr.readyState == 4) {
|
64
|
+
delete self.currentXHR;
|
65
|
+
try {
|
66
|
+
if (xhr.status == 200) {
|
67
|
+
var text = xhr.responseText;
|
68
|
+
if (text) {
|
69
|
+
var value = null;
|
70
|
+
try {
|
71
|
+
var option = text.substring(0, 1);
|
72
|
+
var script = text.substring(1, text.length);
|
73
|
+
if (option == 'P') {
|
74
|
+
// get properties for code completion
|
75
|
+
var obj = self.evaluate.apply(window, [script, false]);
|
76
|
+
value = "V" + self.getAllProperties(obj).join(",");
|
77
|
+
} else if (option == 'E') {
|
78
|
+
// just evaluate
|
79
|
+
value = "V" + self.evaluate.apply(window, [script, true]);
|
80
|
+
} else {
|
81
|
+
value = "Ebad request: " + text;
|
82
|
+
}
|
83
|
+
} catch (e) {
|
84
|
+
value = "E" + e.toString();
|
85
|
+
}
|
86
|
+
self.queue.push(value);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
} catch (e) {
|
90
|
+
// failed to poll; ignore
|
91
|
+
}
|
92
|
+
if (!self.aborted) {
|
93
|
+
if (self.queue.length > 0) {
|
94
|
+
self.poll();
|
95
|
+
} else {
|
96
|
+
// if polling failed, try again later
|
97
|
+
setTimeout(function() { self.poll() }, 5000);
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
this.currentXHR = xhr;
|
103
|
+
if (this.queue.length > 0) {
|
104
|
+
// mark that there are more messages pending
|
105
|
+
content = "+" + content;
|
106
|
+
}
|
107
|
+
xhr.send(encodeURI(content));
|
108
|
+
},
|
109
|
+
|
110
|
+
startPoll: function() {
|
111
|
+
this.startPoll = function() {}
|
112
|
+
if (window.top.parent != window) return;
|
113
|
+
this.poll();
|
114
|
+
},
|
115
|
+
|
116
|
+
abortPoll: function(retry) {
|
117
|
+
if (!retry) {
|
118
|
+
// don't retry any more
|
119
|
+
this.aborted = true;
|
120
|
+
}
|
121
|
+
if (this.currentXHR) {
|
122
|
+
this.currentXHR.abort();
|
123
|
+
}
|
124
|
+
},
|
125
|
+
|
126
|
+
pushValue: function(value) {
|
127
|
+
this.queue.push(value);
|
128
|
+
this.abortPoll(true);
|
129
|
+
this.startPoll(); // in case startPoll has not been called yet
|
130
|
+
},
|
131
|
+
|
132
|
+
attachEvent: function(event, handler) {
|
133
|
+
if (window.addEventListener) {
|
134
|
+
window.addEventListener(event, handler, false);
|
135
|
+
} else {
|
136
|
+
window.attachEvent("on" + event, handler);
|
137
|
+
}
|
138
|
+
},
|
139
|
+
|
140
|
+
getAllProperties: function(obj) {
|
141
|
+
var result = [];
|
142
|
+
for (var i in obj) {
|
143
|
+
result.push(i);
|
144
|
+
}
|
145
|
+
for (var i = 0; i < this.dontEnums.length; i++) {
|
146
|
+
var e = this.dontEnums[i];
|
147
|
+
if (Object.prototype.toString.apply(obj).match(/object (\w+)/)) {
|
148
|
+
if (e.check(obj, RegExp.$1)) {
|
149
|
+
for (var k in e.properties) {
|
150
|
+
var names = e.properties[k].split(/\s+/);
|
151
|
+
for (var j = 0; j < names.length; j++) {
|
152
|
+
if (obj[names[j]]) {
|
153
|
+
result.push(names[j]);
|
154
|
+
}
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
return result;
|
161
|
+
},
|
162
|
+
|
163
|
+
// dontEnums is copied from jsh. thanks!
|
164
|
+
// http://d.hatena.ne.jp/brazil/20061022
|
165
|
+
dontEnums: [
|
166
|
+
{check: function(obj, objType){return objType=='String'}, properties: {
|
167
|
+
F:'charAt charCodeAt concat indexOf lastIndexOf match replace search slice split substr substring toLowerCase toUpperCase valueOf '+
|
168
|
+
'anchor big blink bold fixed fontcolor fontsize italics link small strike sub sup',
|
169
|
+
N:'length'
|
170
|
+
}},
|
171
|
+
{check: function(obj, objType){return objType=='Array'}, properties: {
|
172
|
+
F:'concat join pop push unshift shift sort reverse slice splice toString valueOf '+
|
173
|
+
'every filter forEach map some',
|
174
|
+
N:'length'
|
175
|
+
}},
|
176
|
+
{check: function(obj, objType){return objType=='Boolean'}, properties: {
|
177
|
+
F:'toString valueOf'
|
178
|
+
}},
|
179
|
+
{check: function(obj, objType){return objType=='Date'}, properties: {
|
180
|
+
F:'getDate getYear getFullYear getHours getMilliseconds getMinutes getMonth getSeconds getTime getDay '+
|
181
|
+
'setDate setYear setFullYear setHours setMilliseconds setMinutes setMonth setSeconds setTime '+
|
182
|
+
'getUTCDate getUTCFullYear getUTCHours getUTCMilliseconds getUTCMinutes getUTCMonth getUTCSeconds getUTCDay '+
|
183
|
+
'setUTCDate setUTCFullYear setUTCHours setUTCMilliseconds setUTCMinutes setUTCMonth setUTCSeconds '+
|
184
|
+
'toGMTString toLocaleString toUTCString UTC getTimezoneOffset parse toString valueOf toSource'
|
185
|
+
}},
|
186
|
+
{check: function(obj, objType){return objType=='Number'}, properties: {
|
187
|
+
F:'toExponential toFixed toPrecision toSource toString valueOf'
|
188
|
+
}},
|
189
|
+
{check: function(obj, objType){return objType=='RegExp'}, properties: {
|
190
|
+
F:'exec test toSource toString',
|
191
|
+
B:'global ignoreCase multiline',
|
192
|
+
N:'lastIndex',
|
193
|
+
S:'source'
|
194
|
+
}},
|
195
|
+
{check: function(obj, objType){return obj.navigator}, properties: {
|
196
|
+
F:'Array String Number Date RegExp Boolean '+
|
197
|
+
'escape unescape decodeURI decodeURIComponent encodeURI encodeURIComponent '+
|
198
|
+
'eval isFinite isNaN parseFloat parseInt',
|
199
|
+
O:'Math undefined',
|
200
|
+
N:'Infinity NaN'
|
201
|
+
}},
|
202
|
+
{check: function(obj, objType){return objType=='Math'}, properties: {
|
203
|
+
F:'abs acos asin atan atan2 ceil cos exp floor log max min pow random round sin sqrt tan',
|
204
|
+
N:'E LN2 LN10 LOG2E LOG10E PI SQRT1_2 SQRT2'
|
205
|
+
}},
|
206
|
+
{check: function(obj, objType){return obj.MAX_VALUE}, properties: {
|
207
|
+
N:'MAX_VALUE MIN_VALUE NaN NEGATIVE_INFINITY POSITIVE_INFINITY'
|
208
|
+
}},
|
209
|
+
{check: function(obj, objType){return obj.fromCharCode}, properties: {
|
210
|
+
F:'fromCharCode'
|
211
|
+
}},
|
212
|
+
{check: function(obj, objType){return objType=='Function'}, properties: {
|
213
|
+
F:'apply call toSource toString'
|
214
|
+
}}
|
215
|
+
],
|
216
|
+
|
217
|
+
init: function() {
|
218
|
+
var self = this;
|
219
|
+
|
220
|
+
this.attachEvent("abort", function() { self.abortPoll() });
|
221
|
+
this.attachEvent("unload", function() { self.abortPoll() });
|
222
|
+
this.attachEvent("pagehide", function() { self.abortPoll() });
|
223
|
+
|
224
|
+
this.attachEvent("load", function() { self.startPoll() });
|
225
|
+
this.attachEvent("pageshow", function() { self.startPoll() });
|
226
|
+
|
227
|
+
window.onerror = function(message, file, line) {
|
228
|
+
self.pushValue("E" +
|
229
|
+
"Message: " + message + "\n" +
|
230
|
+
"File: " + file + "\n" +
|
231
|
+
"Line: " + line + "\n");
|
232
|
+
}
|
233
|
+
setTimeout(function() { self.startPoll() }, 3000);
|
234
|
+
|
235
|
+
if (!window.console) {
|
236
|
+
window.console = {};
|
237
|
+
var levels = ["debug", "info", "warn", "error"];
|
238
|
+
for (var i = 0; i < levels.length; i++) {
|
239
|
+
(function() {
|
240
|
+
var level = levels[i];
|
241
|
+
window.console[level] = function() {
|
242
|
+
var message = [];
|
243
|
+
for (var i = 0; i < arguments.length; i++) {
|
244
|
+
message.push(arguments[i]);
|
245
|
+
}
|
246
|
+
self.pushValue("V[" + level + "] " + message.join(", "));
|
247
|
+
}
|
248
|
+
})();
|
249
|
+
}
|
250
|
+
window.console.log = window.console.info;
|
251
|
+
}
|
252
|
+
}
|
253
|
+
}).init();
|
@@ -0,0 +1,140 @@
|
|
1
|
+
#
|
2
|
+
# asynchttpproxy.rb: Asynchronous HTTP Proxy Server
|
3
|
+
#
|
4
|
+
# Copyright 2007 Shinya Kasatani
|
5
|
+
#
|
6
|
+
# The original WEBrick::HTTPProxyServer sends response to the client
|
7
|
+
# after the whole content has been downloaded to the proxy,
|
8
|
+
# while this AsyncHTTPProxyServer sends the response concurrently while
|
9
|
+
# downloading the content.
|
10
|
+
#
|
11
|
+
# Based on webrick/httpproxy.rb. The original copyright notice follows:
|
12
|
+
#
|
13
|
+
# httpproxy.rb -- HTTPProxy Class
|
14
|
+
#
|
15
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
16
|
+
# Copyright (c) 2002 GOTO Kentaro
|
17
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
18
|
+
# reserved.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'stringio'
|
22
|
+
require 'webrick/httpproxy'
|
23
|
+
|
24
|
+
module WEBrick
|
25
|
+
class AsyncHTTPProxyServer < HTTPProxyServer
|
26
|
+
def proxy_service(req, res)
|
27
|
+
# Proxy Authentication
|
28
|
+
proxy_auth(req, res)
|
29
|
+
|
30
|
+
# Create Request-URI to send to the origin server
|
31
|
+
uri = req.request_uri
|
32
|
+
path = uri.path.dup
|
33
|
+
path << "?" << uri.query if uri.query
|
34
|
+
|
35
|
+
# Choose header fields to transfer
|
36
|
+
header = Hash.new
|
37
|
+
choose_header(req, header)
|
38
|
+
set_via(header)
|
39
|
+
|
40
|
+
# select upstream proxy server
|
41
|
+
if proxy = proxy_uri(req, res)
|
42
|
+
proxy_host = proxy.host
|
43
|
+
proxy_port = proxy.port
|
44
|
+
if proxy.userinfo
|
45
|
+
credentials = "Basic " + [proxy.userinfo].pack("m*")
|
46
|
+
credentials.chomp!
|
47
|
+
header['proxy-authorization'] = credentials
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
response = nil
|
53
|
+
q = Queue.new
|
54
|
+
p_reader, p_writer = IO.pipe
|
55
|
+
thread = Thread.start do
|
56
|
+
begin
|
57
|
+
@logger.debug "downloading #{uri}"
|
58
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
|
59
|
+
http.start{
|
60
|
+
if @config[:ProxyTimeout]
|
61
|
+
################################## these issues are
|
62
|
+
http.open_timeout = 30 # secs # necessary (maybe bacause
|
63
|
+
http.read_timeout = 60 # secs # Ruby's bug, but why?)
|
64
|
+
##################################
|
65
|
+
end
|
66
|
+
http_req = nil
|
67
|
+
http_req_body = nil
|
68
|
+
case req.request_method
|
69
|
+
when "GET"
|
70
|
+
http_req = Net::HTTP::Get.new(path, header)
|
71
|
+
when "POST"
|
72
|
+
http_req = Net::HTTP::Post.new(path, header)
|
73
|
+
http_req_body = req.body || ""
|
74
|
+
when "HEAD"
|
75
|
+
http_req = Net::HTTP::Head.new(path, header)
|
76
|
+
else
|
77
|
+
raise HTTPStatus::MethodNotAllowed,
|
78
|
+
"unsupported method `#{req.request_method}'."
|
79
|
+
end
|
80
|
+
http.request(http_req, http_req_body) do |response|
|
81
|
+
q.push response
|
82
|
+
size = 0
|
83
|
+
last_size = 0
|
84
|
+
response.read_body do |str|
|
85
|
+
last_size = size
|
86
|
+
size += str.size
|
87
|
+
if last_size / 500000 != size / 500000
|
88
|
+
@logger.debug "downloading #{uri}: size=#{size}"
|
89
|
+
end
|
90
|
+
p_writer.write str
|
91
|
+
end
|
92
|
+
@logger.debug "finished downloading #{uri}: size=#{size}"
|
93
|
+
p_writer.close
|
94
|
+
end
|
95
|
+
}
|
96
|
+
rescue Exception => err
|
97
|
+
logger.debug("#{err.class}: #{err.message}")
|
98
|
+
q.push err
|
99
|
+
# raise HTTPStatus::ServiceUnavailable, err.message
|
100
|
+
end
|
101
|
+
end
|
102
|
+
response = q.pop
|
103
|
+
if response.is_a?(Exception)
|
104
|
+
@logger.debug "failed to download #{uri}"
|
105
|
+
raise HTTPStatus::ServiceUnavailable, response.message
|
106
|
+
end
|
107
|
+
|
108
|
+
# Persistent connction requirements are mysterious for me.
|
109
|
+
# So I will close the connection in every response.
|
110
|
+
res['proxy-connection'] = "close"
|
111
|
+
res['connection'] = "close"
|
112
|
+
|
113
|
+
# Convert Net::HTTP::HTTPResponse to WEBrick::HTTPProxy
|
114
|
+
res.status = response.code.to_i
|
115
|
+
choose_header(response, res)
|
116
|
+
set_cookie(response, res)
|
117
|
+
set_via(res)
|
118
|
+
res.body = p_reader
|
119
|
+
def res.flush_body
|
120
|
+
if @body.is_a?(IO)
|
121
|
+
begin
|
122
|
+
str_body = @body.read
|
123
|
+
ensure
|
124
|
+
@body.close
|
125
|
+
end
|
126
|
+
@body = str_body
|
127
|
+
end
|
128
|
+
@body
|
129
|
+
end
|
130
|
+
|
131
|
+
@logger.debug "downloading #{uri}: content-length=#{res.header['content-length']}"
|
132
|
+
|
133
|
+
# Process contents
|
134
|
+
if handler = @config[:ProxyContentHandler]
|
135
|
+
handler.call(req, res)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
@@ -0,0 +1,339 @@
|
|
1
|
+
#
|
2
|
+
# jscommander.rb: Remote JavaScript Console
|
3
|
+
#
|
4
|
+
# Copyright 2007 Shinya Kasatani
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'uri'
|
8
|
+
require 'thread'
|
9
|
+
require 'webrick'
|
10
|
+
require 'zlib'
|
11
|
+
|
12
|
+
module JSCommander
|
13
|
+
SCRIPT_DIR = "/_remote_js_proxy/"
|
14
|
+
|
15
|
+
class PipeQueue
|
16
|
+
attr_reader :reader, :writer
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@reader, @writer = IO.pipe
|
20
|
+
end
|
21
|
+
|
22
|
+
def push(obj)
|
23
|
+
data = Marshal.dump(obj)
|
24
|
+
@writer.write([data.size].pack("N") + data)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pop
|
28
|
+
size = @reader.read(4).unpack("N")[0]
|
29
|
+
Marshal.load(@reader.read(size))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# types are: V: value, E: error, A: active message from proxy
|
34
|
+
Message = Struct.new(:value, :type, :clients)
|
35
|
+
|
36
|
+
class Broker
|
37
|
+
attr_reader :cmd_queue, :msg_queue
|
38
|
+
|
39
|
+
def initialize(opts = {})
|
40
|
+
@cmd_queue = PipeQueue.new
|
41
|
+
@msg_queue = PipeQueue.new
|
42
|
+
@proxy = ProxyServer.new(self, opts)
|
43
|
+
@shell = Shell.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def start
|
47
|
+
@proxy_pid = fork do
|
48
|
+
begin
|
49
|
+
Signal.trap('INT') {}
|
50
|
+
Signal.trap('TERM') { @proxy.shutdown }
|
51
|
+
IO.for_fd(0).close
|
52
|
+
@proxy.start
|
53
|
+
rescue Exception => e
|
54
|
+
p e
|
55
|
+
end
|
56
|
+
end
|
57
|
+
Signal.trap("TERM") { shutdown }
|
58
|
+
@shell.run # @shell.run should block
|
59
|
+
end
|
60
|
+
|
61
|
+
def shutdown
|
62
|
+
$stderr.puts "shutdown"
|
63
|
+
Process.kill "TERM", @proxy_pid
|
64
|
+
Process.waitall
|
65
|
+
exit(0)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Shell
|
70
|
+
class SimpleConsole
|
71
|
+
def show_banner
|
72
|
+
end
|
73
|
+
|
74
|
+
def readline
|
75
|
+
line = $stdin.readline
|
76
|
+
line.chomp! if line
|
77
|
+
line
|
78
|
+
end
|
79
|
+
|
80
|
+
def close
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class ReadlineConsole
|
85
|
+
HISTORY_PATH = "~/.jscmd_history"
|
86
|
+
|
87
|
+
def history_path
|
88
|
+
File.expand_path(HISTORY_PATH)
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize(shell)
|
92
|
+
@shell = shell
|
93
|
+
require 'readline'
|
94
|
+
if File.exist?(history_path)
|
95
|
+
hist = File.readlines(history_path).map{|line| line.chomp}
|
96
|
+
Readline::HISTORY.push(*hist)
|
97
|
+
end
|
98
|
+
Readline.basic_word_break_characters = " \t\n\\`@><=;|&{([+-*/%"
|
99
|
+
Readline.completion_append_character = nil
|
100
|
+
Readline.completion_proc = proc do |word|
|
101
|
+
if word =~ /\.$/
|
102
|
+
@shell.object_props($`).map{|name| word + name}
|
103
|
+
elsif word =~ /\.([^.]+)$/
|
104
|
+
prefix = $1
|
105
|
+
parent = $`
|
106
|
+
props = @shell.object_props(parent)
|
107
|
+
props.select{|name|name[0...(prefix.size)] == prefix}.map{|name| "#{parent}.#{name}"}
|
108
|
+
else
|
109
|
+
props = @shell.object_props("this")
|
110
|
+
prefix = word
|
111
|
+
props.select{|name|name[0...(prefix.size)] == prefix}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def show_banner
|
117
|
+
puts "Press Ctrl+D to exit."
|
118
|
+
end
|
119
|
+
|
120
|
+
def close
|
121
|
+
open(history_path, "ab") do |f|
|
122
|
+
Readline::HISTORY.each{|line| f.puts(line)}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def readline
|
127
|
+
line = Readline.readline("#{@shell.clients}> ", true)
|
128
|
+
Readline::HISTORY.pop if /^\s*$/ =~ line
|
129
|
+
line
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def console
|
134
|
+
@console ||= $stdin.tty? ? ReadlineConsole.new(self) : SimpleConsole.new
|
135
|
+
end
|
136
|
+
|
137
|
+
attr_reader :clients
|
138
|
+
|
139
|
+
def initialize(broker)
|
140
|
+
@broker = broker
|
141
|
+
@clients = nil
|
142
|
+
@msg_lock = Mutex.new
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_script(line, &handler)
|
146
|
+
@msg_lock.synchronize do
|
147
|
+
@broker.cmd_queue.push(line)
|
148
|
+
yield read_msg
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def read_msg
|
153
|
+
msg = @broker.msg_queue.pop
|
154
|
+
# p msg
|
155
|
+
@clients = msg.clients
|
156
|
+
msg
|
157
|
+
end
|
158
|
+
|
159
|
+
def object_props(object)
|
160
|
+
send_script("P" + object) do |r|
|
161
|
+
if r.type == "V" && r.value
|
162
|
+
r.value.split(/,/)
|
163
|
+
else
|
164
|
+
[]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def run
|
170
|
+
# read and print messages in background
|
171
|
+
Thread.start do
|
172
|
+
while IO.select([@broker.msg_queue.reader], nil, nil, nil)
|
173
|
+
@msg_lock.synchronize do
|
174
|
+
if IO.select([@broker.msg_queue.reader], nil, nil, 0)
|
175
|
+
msg = read_msg
|
176
|
+
puts msg.value || ""
|
177
|
+
Process.kill "INT", $$ # interrupt readline
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
console.show_banner
|
184
|
+
|
185
|
+
begin
|
186
|
+
loop do
|
187
|
+
break unless line = console.readline
|
188
|
+
send_script("E" + line) do |msg|
|
189
|
+
puts msg.value if msg.value
|
190
|
+
end
|
191
|
+
end
|
192
|
+
rescue Interrupt
|
193
|
+
retry
|
194
|
+
rescue SystemExit
|
195
|
+
return
|
196
|
+
rescue Exception => e
|
197
|
+
$stderr.puts "#{e.inspect} at:\n#{e.backtrace.join("\n")}"
|
198
|
+
ensure
|
199
|
+
begin
|
200
|
+
console.close
|
201
|
+
rescue Exception => e
|
202
|
+
$stderr.puts "failed to close console: #{e.inspect}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
@broker.shutdown
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class ProxyServer < WEBrick::AsyncHTTPProxyServer
|
210
|
+
def initialize(broker, args = {})
|
211
|
+
@broker = broker
|
212
|
+
@cmd_queue = PipeQueue.new
|
213
|
+
@clients = []
|
214
|
+
super({:ProxyContentHandler => method(:handle_content).to_proc}.merge(args))
|
215
|
+
end
|
216
|
+
|
217
|
+
def start
|
218
|
+
Thread.start do
|
219
|
+
sleep 1
|
220
|
+
begin
|
221
|
+
poll_cmd_queue
|
222
|
+
rescue Exception => e
|
223
|
+
p e
|
224
|
+
end
|
225
|
+
end
|
226
|
+
super
|
227
|
+
end
|
228
|
+
|
229
|
+
def service(req, res)
|
230
|
+
if req.path == "#{SCRIPT_DIR}agent.js"
|
231
|
+
res.content_type = "application/x-javascript"
|
232
|
+
res.body = File.read(File.join(File.dirname(__FILE__), "agent.js"))
|
233
|
+
elsif req.path == "#{SCRIPT_DIR}poll"
|
234
|
+
serve_script(req, res)
|
235
|
+
else
|
236
|
+
if req.header["accept-encoding"] && !req.header["accept-encoding"].empty?
|
237
|
+
if req.header["accept-encoding"].first.split(/,/).include?("gzip")
|
238
|
+
req.header["accept-encoding"] = ["gzip"]
|
239
|
+
else
|
240
|
+
req.header.delete "accept-encoding"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
super
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def format_clients
|
248
|
+
@clients.map{|c|"[#{URI.parse(c.header["referer"].to_s).host}]"}.join(",")
|
249
|
+
end
|
250
|
+
|
251
|
+
def poll_cmd_queue
|
252
|
+
while @status == :Running
|
253
|
+
cmd = @broker.cmd_queue.pop
|
254
|
+
cmd_line = cmd[1...(cmd.size)]
|
255
|
+
if cmd_line.strip.empty?
|
256
|
+
@broker.msg_queue.push(Message.new(nil, "V", format_clients))
|
257
|
+
else
|
258
|
+
@cmd_queue.push(cmd)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def serve_script(req, res)
|
264
|
+
# puts "serve:#{req.body}"
|
265
|
+
@clients << req
|
266
|
+
if req.body && req.body =~ /^(\+?)([VE])/
|
267
|
+
more = $1 == '+'
|
268
|
+
type = $2
|
269
|
+
# V: value
|
270
|
+
# E: error
|
271
|
+
@broker.msg_queue.push Message.new(URI.decode($'), type, format_clients)
|
272
|
+
if more
|
273
|
+
res.content_type = "text/plain"
|
274
|
+
res.body = ''
|
275
|
+
return
|
276
|
+
end
|
277
|
+
else
|
278
|
+
# empty message - maybe a new client connected?
|
279
|
+
@broker.msg_queue.push Message.new(nil, "A", format_clients)
|
280
|
+
end
|
281
|
+
while @status == :Running
|
282
|
+
req_socket = req.instance_eval{@socket}
|
283
|
+
sockets = [req_socket, @cmd_queue.reader]
|
284
|
+
if @clients.first != req
|
285
|
+
# don't poll cmd_queue if this is request is not the first one
|
286
|
+
sockets.pop
|
287
|
+
end
|
288
|
+
r = IO.select(sockets, nil, nil, 1)
|
289
|
+
if r
|
290
|
+
if r[0].include?(@cmd_queue.reader)
|
291
|
+
line = @cmd_queue.pop
|
292
|
+
res.content_type = "text/plain"
|
293
|
+
res.body = line
|
294
|
+
return
|
295
|
+
elsif r[0].include?(req_socket)
|
296
|
+
@clients.delete(req); req = nil
|
297
|
+
@broker.msg_queue.push(Message.new("aborted", "A", format_clients))
|
298
|
+
raise WEBrick::HTTPStatus::EOFError
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
# server is shutting down
|
303
|
+
raise WEBrick::HTTPStatus::EOFError
|
304
|
+
ensure
|
305
|
+
@clients.delete(req) if req
|
306
|
+
end
|
307
|
+
|
308
|
+
def handle_content(req, res)
|
309
|
+
# $stderr.puts "handle_content:type=#{res.content_type}, status=#{res.status}, encoding=#{res.header["content-encoding"]}"
|
310
|
+
if res.content_type =~ %r{^text/html} && res.status == 200
|
311
|
+
res.flush_body
|
312
|
+
# we cannot always trust content_type, so check if the content looks like html
|
313
|
+
body = res.body
|
314
|
+
if res.header["content-encoding"] == "gzip"
|
315
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
316
|
+
end
|
317
|
+
if body =~ /^\s*</
|
318
|
+
body = body.dup
|
319
|
+
# puts "injecting javascript"
|
320
|
+
script_tag = '<script type="text/javascript" src="/_remote_js_proxy/agent.js"></script>'
|
321
|
+
unless body.sub!(%r{<head( .*?)?>}i){|s|s+script_tag}
|
322
|
+
body = script_tag + body
|
323
|
+
end
|
324
|
+
if res.header["content-encoding"] == "gzip"
|
325
|
+
io = StringIO.new
|
326
|
+
writer = Zlib::GzipWriter.new(io)
|
327
|
+
writer.write(body)
|
328
|
+
writer.close
|
329
|
+
res.body = io.string
|
330
|
+
else
|
331
|
+
res.body = body
|
332
|
+
end
|
333
|
+
res.content_length = res.body.size if res.content_length
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|