jscmd 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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 }
@@ -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
+