narou 1.7.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of narou might be problematic. Click here for more details.

Files changed (100) hide show
  1. checksums.yaml +5 -13
  2. data/.gitignore +1 -0
  3. data/ChangeLog.md +35 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +100 -0
  6. data/README.md +28 -39
  7. data/lib/color.rb +0 -2
  8. data/lib/command.rb +1 -0
  9. data/lib/command/convert.rb +33 -4
  10. data/lib/command/diff.rb +5 -4
  11. data/lib/command/download.rb +9 -1
  12. data/lib/command/flag.rb +2 -2
  13. data/lib/command/list.rb +1 -1
  14. data/lib/command/mail.rb +3 -3
  15. data/lib/command/remove.rb +2 -1
  16. data/lib/command/send.rb +7 -6
  17. data/lib/command/setting.rb +39 -95
  18. data/lib/command/tag.rb +25 -13
  19. data/lib/command/update.rb +6 -1
  20. data/lib/command/version.rb +5 -1
  21. data/lib/command/web.rb +111 -0
  22. data/lib/commandbase.rb +5 -2
  23. data/lib/commandline.rb +16 -0
  24. data/lib/converterbase.rb +20 -14
  25. data/lib/device.rb +5 -4
  26. data/lib/downloader.rb +68 -39
  27. data/lib/eventable.rb +72 -0
  28. data/lib/helper.rb +105 -37
  29. data/lib/ini.rb +2 -1
  30. data/lib/input.rb +68 -0
  31. data/lib/inventory.rb +4 -0
  32. data/lib/kindlestrip.rb +2 -2
  33. data/lib/logger.rb +41 -19
  34. data/lib/narou.rb +10 -0
  35. data/lib/narou/api.rb +1 -1
  36. data/lib/novelconverter.rb +8 -21
  37. data/lib/novelsetting.rb +79 -4
  38. data/lib/version.rb +1 -1
  39. data/lib/web/all.rb +12 -0
  40. data/lib/web/appserver.rb +612 -0
  41. data/lib/web/helper4web.rb +15 -0
  42. data/lib/web/progressbar4web.rb +32 -0
  43. data/lib/web/public/favicon.ico +0 -0
  44. data/lib/web/public/resources/bootbox.min.js +6 -0
  45. data/lib/web/public/resources/common.ui.js +143 -0
  46. data/lib/web/public/resources/dataTables.colVis.js +1113 -0
  47. data/lib/web/public/resources/help/rect_select.png +0 -0
  48. data/lib/web/public/resources/help/ssmain.png +0 -0
  49. data/lib/web/public/resources/help/tag.png +0 -0
  50. data/lib/web/public/resources/jquery.moveto.js +44 -0
  51. data/lib/web/public/resources/jquery.outerclick.js +60 -0
  52. data/lib/web/public/resources/jquery.slidenavbar.js +89 -0
  53. data/lib/web/public/resources/narou.library.js +815 -0
  54. data/lib/web/public/resources/narou.ui.js +993 -0
  55. data/lib/web/public/resources/perfect-scrollbar.min.css +5 -0
  56. data/lib/web/public/resources/perfect-scrollbar.min.js +4 -0
  57. data/lib/web/public/resources/shortcut.js +223 -0
  58. data/lib/web/public/resources/sort_asc.png +0 -0
  59. data/lib/web/public/resources/sort_desc.png +0 -0
  60. data/lib/web/public/resources/toggle-switch.css +322 -0
  61. data/lib/web/public/robots.txt +3 -0
  62. data/lib/web/public/test/jquery.outerclick.html +72 -0
  63. data/lib/web/pushserver.rb +110 -0
  64. data/lib/web/settingmessages.rb +14 -0
  65. data/lib/web/streaminginput.rb +103 -0
  66. data/lib/web/streaminglogger.rb +52 -0
  67. data/lib/web/views/about.haml +11 -0
  68. data/lib/web/views/help.haml +105 -0
  69. data/lib/web/views/index.haml +245 -0
  70. data/lib/web/views/js/widget.erb +74 -0
  71. data/lib/web/views/layout.haml +49 -0
  72. data/lib/web/views/novels/setting.haml +177 -0
  73. data/lib/web/views/settings.haml +115 -0
  74. data/lib/web/views/style.scss +737 -0
  75. data/lib/web/views/widget.haml +39 -0
  76. data/lib/web/web-socket-ruby/.gitignore +1 -0
  77. data/lib/web/web-socket-ruby/README.txt +75 -0
  78. data/lib/web/web-socket-ruby/lib/web_socket.rb +601 -0
  79. data/lib/web/web-socket-ruby/samples/chat_server.rb +58 -0
  80. data/lib/web/web-socket-ruby/samples/echo_server.rb +33 -0
  81. data/lib/web/web-socket-ruby/samples/stdio_client.rb +25 -0
  82. data/lib/web/worker.rb +87 -0
  83. data/narou.gemspec +36 -3
  84. data/narou.rb +8 -6
  85. data/preset/ncode.syosetu.com/n8725k/converter.rb +2 -1
  86. data/spec/data/convert_test/replace/correct_test_replace.txt +1 -1
  87. data/spec/data/convert_test/replace/test_replace.txt +1 -1
  88. data/spec/data/convert_test/ruby/correct_test_ruby.txt +18 -1
  89. data/spec/data/convert_test/ruby/test_ruby.txt +18 -0
  90. data/spec/downloader_spec.rb +37 -0
  91. data/spec/eventable_spec.rb +172 -0
  92. data/spec/exit_code_spec.rb +67 -0
  93. data/spec/helper_spec.rb +72 -0
  94. data/spec/input_spec.rb +76 -0
  95. data/spec/logger_spec.rb +53 -0
  96. data/spec/novelsetting_spec.rb +35 -0
  97. data/spec/worker_spec.rb +56 -0
  98. data/template/ibunko_novel.txt.erb +2 -2
  99. data/template/novel.txt.erb +2 -2
  100. metadata +213 -29
@@ -0,0 +1,39 @@
1
+ !!! 5
2
+ %html{:lang => "ja"}
3
+ %head
4
+ %meta{:charset => "utf-8"}/
5
+ %meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}/
6
+ %meta{:content => "width=device-width, initial-scale=1", :name => "viewport"}/
7
+ %meta{:content => "whiteleaf", :name => "author"}/
8
+ %meta{:content => "none", :name => "robots"}/
9
+ %title Narou.rb WEB UI Widget
10
+ %link{:href => "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", :rel => "stylesheet"}/
11
+ %link{:href => "/resources/perfect-scrollbar.min.css", :rel => "stylesheet"}/
12
+ %link{:href => "/style.css", :rel => "stylesheet"}/
13
+ / HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries
14
+ /[if lt IE 9]
15
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
16
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
17
+ %body
18
+ :javascript
19
+ var local_initialize_function = function($) {
20
+ var notification = new Narou.Notification();
21
+ var stream_console = new Narou.Console(notification, {
22
+ restore: false, buttons: false
23
+ });
24
+ };
25
+ :css
26
+ html, body, .console.fullscreen {
27
+ height: 100%;
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+ #console.console.fullscreen
32
+
33
+ %script{:src => "//code.jquery.com/jquery-1.11.1.min.js"}
34
+ %script{:src => "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"}
35
+ %script{:src => "/resources/perfect-scrollbar.min.js"}
36
+ %script{:src => "/resources/narou.library.js"}
37
+ :javascript
38
+ if (typeof local_initialize_function !== "undefined") local_initialize_function($);
39
+
@@ -0,0 +1 @@
1
+ exp
@@ -0,0 +1,75 @@
1
+ HTML5 Web Socket server/client implementation in Ruby.
2
+
3
+ For server, em-websocket ( https://github.com/igrigorik/em-websocket ) may be a better choice, especially if you want to use EventMachine.
4
+
5
+
6
+ * How to run sample
7
+
8
+ - Run sample Web Socket server (echo server) with:
9
+ $ ruby samples/echo_server.rb localhost 10081
10
+
11
+ - Run sample Web Socket client and type something:
12
+ $ ruby samples/stdio_client.rb ws://localhost:10081
13
+ Ready
14
+ hoge
15
+ Sent: "hoge"
16
+ Received: "hoge"
17
+
18
+
19
+ * Usage example
20
+
21
+ Server:
22
+
23
+ # Runs the server at port 10081. It allows connections whose origin is example.com.
24
+ server = WebSocketServer.new(:port => 10081, :accepted_domains => ["example.com"])
25
+ server.run() do |ws|
26
+ # The block is called for each connection.
27
+ # Checks requested path.
28
+ if ws.path == "/"
29
+ # Call ws.handshake() without argument first.
30
+ ws.handshake()
31
+ # Receives one message from the client as String.
32
+ while data = ws.receive()
33
+ puts(data)
34
+ # Sends the message to the client.
35
+ ws.send(data)
36
+ end
37
+ else
38
+ # You can call ws.handshake() with argument to return error status.
39
+ ws.handshake("404 Not Found")
40
+ end
41
+ end
42
+
43
+ Client:
44
+
45
+ # Connects to Web Socket server at host example.com port 10081.
46
+ client = WebSocket.new("ws://example.com:10081/")
47
+ # Sends a message to the server.
48
+ client.send("Hello")
49
+ # Receives a message from the server.
50
+ data = client.receive()
51
+ puts(data)
52
+
53
+
54
+ * Supported WebSocket protocol versions
55
+
56
+ WebSocketServer class (server) accepts version hixie-75, hixie-76, hybi-07, hybi-10.
57
+ WebSocket class (client) speaks version hixie-76.
58
+
59
+
60
+ * Tips: JavaScript client implementation
61
+
62
+ Google Chrome Dev Channel natively supports Web Socket. For other browsers, you can use an implementation using Flash:
63
+ http://github.com/gimite/web-socket-js/tree/master
64
+
65
+
66
+ * WebSocket protocol versions
67
+
68
+ The server supports the protocol defined in RFC 6455, draft versions hixie-75 and hixie-76.
69
+
70
+ The client speaks draft version hixie-76.
71
+
72
+
73
+ * License
74
+
75
+ New BSD License.
@@ -0,0 +1,601 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
4
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
5
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
6
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7
+
8
+ require "base64"
9
+ require "socket"
10
+ require "uri"
11
+ require "digest/md5"
12
+ require "digest/sha1"
13
+ require "openssl"
14
+ require "stringio"
15
+
16
+
17
+ class WebSocket
18
+
19
+ class << self
20
+
21
+ attr_accessor(:debug)
22
+
23
+ end
24
+
25
+ class Error < RuntimeError
26
+
27
+ end
28
+
29
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
+ OPCODE_CONTINUATION = 0x00
31
+ OPCODE_TEXT = 0x01
32
+ OPCODE_BINARY = 0x02
33
+ OPCODE_CLOSE = 0x08
34
+ OPCODE_PING = 0x09
35
+ OPCODE_PONG = 0x0a
36
+
37
+ def initialize(arg, params = {})
38
+ if params[:server] # server
39
+
40
+ @server = params[:server]
41
+ @socket = arg
42
+ line = gets()
43
+ if !line
44
+ raise(WebSocket::Error, "Client disconnected without sending anything.")
45
+ end
46
+ line = line.chomp()
47
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
48
+ raise(WebSocket::Error, "Invalid request: #{line}")
49
+ end
50
+ @path = $1
51
+ read_header()
52
+ if @header["sec-websocket-version"]
53
+ @web_socket_version = @header["sec-websocket-version"]
54
+ @key3 = nil
55
+ elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
56
+ @web_socket_version = "hixie-76"
57
+ @key3 = read(8)
58
+ else
59
+ @web_socket_version = "hixie-75"
60
+ @key3 = nil
61
+ end
62
+ if !@server.accepted_origin?(self.origin)
63
+ raise(WebSocket::Error,
64
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
65
+ "To accept this origin, write e.g. \n" +
66
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
67
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
68
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
69
+ end
70
+ @handshaked = false
71
+
72
+ else # client
73
+
74
+ @web_socket_version = "hixie-76"
75
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
76
+
77
+ if uri.scheme == "ws"
78
+ default_port = 80
79
+ elsif uri.scheme = "wss"
80
+ default_port = 443
81
+ else
82
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
83
+ end
84
+
85
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
86
+ host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
87
+ origin = params[:origin] || "http://#{uri.host}"
88
+ key1 = generate_key()
89
+ key2 = generate_key()
90
+ key3 = generate_key3()
91
+
92
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
93
+
94
+ if uri.scheme == "ws"
95
+ @socket = socket
96
+ else
97
+ @socket = ssl_handshake(socket)
98
+ end
99
+
100
+ write(
101
+ "GET #{@path} HTTP/1.1\r\n" +
102
+ "Upgrade: WebSocket\r\n" +
103
+ "Connection: Upgrade\r\n" +
104
+ "Host: #{host}\r\n" +
105
+ "Origin: #{origin}\r\n" +
106
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
107
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
108
+ "\r\n" +
109
+ "#{key3}")
110
+ flush()
111
+
112
+ line = gets().chomp()
113
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
114
+ read_header()
115
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
116
+ raise(WebSocket::Error,
117
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
118
+ end
119
+ reply_digest = read(16)
120
+ expected_digest = hixie_76_security_digest(key1, key2, key3)
121
+ if reply_digest != expected_digest
122
+ raise(WebSocket::Error,
123
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
124
+ end
125
+ @handshaked = true
126
+
127
+ end
128
+ @received = []
129
+ @buffer = ""
130
+ @closing_started = false
131
+ end
132
+
133
+ attr_reader(:server, :header, :path)
134
+
135
+ def handshake(status = nil, header = {})
136
+ if @handshaked
137
+ raise(WebSocket::Error, "handshake has already been done")
138
+ end
139
+ status ||= "101 Switching Protocols"
140
+ def_header = {}
141
+ case @web_socket_version
142
+ when "hixie-75"
143
+ def_header["WebSocket-Origin"] = self.origin
144
+ def_header["WebSocket-Location"] = self.location
145
+ extra_bytes = ""
146
+ when "hixie-76"
147
+ def_header["Sec-WebSocket-Origin"] = self.origin
148
+ def_header["Sec-WebSocket-Location"] = self.location
149
+ extra_bytes = hixie_76_security_digest(
150
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
151
+ else
152
+ def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
153
+ extra_bytes = ""
154
+ end
155
+ header = def_header.merge(header)
156
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
157
+ # Note that Upgrade and Connection must appear in this order.
158
+ write(
159
+ "HTTP/1.1 #{status}\r\n" +
160
+ "Upgrade: websocket\r\n" +
161
+ "Connection: Upgrade\r\n" +
162
+ "#{header_str}\r\n#{extra_bytes}")
163
+ flush()
164
+ @handshaked = true
165
+ end
166
+
167
+ def send(data)
168
+ if !@handshaked
169
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
170
+ end
171
+ case @web_socket_version
172
+ when "hixie-75", "hixie-76"
173
+ data = force_encoding(data.dup(), "ASCII-8BIT")
174
+ write("\x00#{data}\xff")
175
+ flush()
176
+ else
177
+ send_frame(OPCODE_TEXT, data, !@server)
178
+ end
179
+ end
180
+
181
+ def receive()
182
+ if !@handshaked
183
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
184
+ end
185
+ case @web_socket_version
186
+
187
+ when "hixie-75", "hixie-76"
188
+ packet = gets(force_encoding("\xff", "ASCII-8BIT"))
189
+ return nil if !packet
190
+ if packet =~ /\A\x00(.*)\xff\z/nm
191
+ return force_encoding($1, "UTF-8")
192
+ elsif packet == "\xff" && read(1) == "\x00" # closing
193
+ close(1005, "", :peer)
194
+ return nil
195
+ else
196
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
197
+ end
198
+
199
+ else
200
+ while true
201
+ begin
202
+ bytes = read(2).unpack("C*")
203
+ fin = (bytes[0] & 0x80) != 0
204
+ opcode = bytes[0] & 0x0f
205
+ mask = (bytes[1] & 0x80) != 0
206
+ plength = bytes[1] & 0x7f
207
+ if plength == 126
208
+ bytes = read(2)
209
+ plength = bytes.unpack("n")[0]
210
+ elsif plength == 127
211
+ bytes = read(8)
212
+ (high, low) = bytes.unpack("NN")
213
+ plength = high * (2 ** 32) + low
214
+ end
215
+ if @server && !mask
216
+ # Masking is required.
217
+ @socket.close()
218
+ raise(WebSocket::Error, "received unmasked data")
219
+ end
220
+ mask_key = mask ? read(4).unpack("C*") : nil
221
+ payload = read(plength)
222
+ payload = apply_mask(payload, mask_key) if mask
223
+ if WebSocket.debug
224
+ $stderr.printf("recv_frame> opcode:%d fin:%d payload:%p\n" % [opcode, fin ? 1 : 0, payload])
225
+ end
226
+ case opcode
227
+ when OPCODE_TEXT
228
+ return force_encoding(payload, "UTF-8")
229
+ when OPCODE_BINARY
230
+ raise(WebSocket::Error, "received binary data, which is not supported")
231
+ when OPCODE_CLOSE
232
+ close(1005, "", :peer)
233
+ return nil
234
+ when OPCODE_PING
235
+ raise(WebSocket::Error, "received ping, which is not supported")
236
+ when OPCODE_PONG
237
+ next
238
+ else
239
+ raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
240
+ end
241
+ rescue EOFError
242
+ return nil
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ def tcp_socket
249
+ return @socket
250
+ end
251
+
252
+ def host
253
+ return @header["host"]
254
+ end
255
+
256
+ def origin
257
+ case @web_socket_version
258
+ when "7", "8"
259
+ name = "sec-websocket-origin"
260
+ else
261
+ name = "origin"
262
+ end
263
+ if @header[name]
264
+ return @header[name]
265
+ else
266
+ raise(WebSocket::Error, "%s header is missing" % name)
267
+ end
268
+ end
269
+
270
+ def location
271
+ return "ws://#{self.host}#{@path}"
272
+ end
273
+
274
+ # Does closing handshake.
275
+ def close(code = 1005, reason = "", origin = :self)
276
+ if !@closing_started
277
+ case @web_socket_version
278
+ when "hixie-75", "hixie-76"
279
+ write("\xff\x00")
280
+ else
281
+ if code == 1005
282
+ payload = ""
283
+ else
284
+ payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
285
+ end
286
+ send_frame(OPCODE_CLOSE, payload, false)
287
+ end
288
+ end
289
+ @socket.close() if origin == :peer
290
+ @closing_started = true
291
+ end
292
+
293
+ def close_socket()
294
+ @socket.close()
295
+ end
296
+
297
+ private
298
+
299
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
300
+
301
+ def read_header()
302
+ @header = {}
303
+ while line = gets()
304
+ line = line.chomp()
305
+ break if line.empty?
306
+ if !(line =~ /\A(\S+): (.*)\z/n)
307
+ raise(WebSocket::Error, "invalid request: #{line}")
308
+ end
309
+ @header[$1] = $2
310
+ @header[$1.downcase()] = $2
311
+ end
312
+ if !@header["upgrade"]
313
+ raise(WebSocket::Error, "Upgrade header is missing")
314
+ end
315
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
316
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
317
+ end
318
+ if !@header["connection"]
319
+ raise(WebSocket::Error, "Connection header is missing")
320
+ end
321
+ if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
322
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
323
+ end
324
+ end
325
+
326
+ def send_frame(opcode, payload, mask)
327
+ if WebSocket.debug
328
+ $stderr.printf("send_frame> opcode:%d masked:%d payload:%p\n" % [opcode, mask ? 1 : 0, payload])
329
+ end
330
+ payload = force_encoding(payload.dup(), "ASCII-8BIT")
331
+ # Setting StringIO's encoding to ASCII-8BIT.
332
+ buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
333
+ write_byte(buffer, 0x80 | opcode)
334
+ masked_byte = mask ? 0x80 : 0x00
335
+ if payload.bytesize <= 125
336
+ write_byte(buffer, masked_byte | payload.bytesize)
337
+ elsif payload.bytesize < 2 ** 16
338
+ write_byte(buffer, masked_byte | 126)
339
+ buffer.write([payload.bytesize].pack("n"))
340
+ else
341
+ write_byte(buffer, masked_byte | 127)
342
+ buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
343
+ end
344
+ if mask
345
+ mask_key = Array.new(4){ rand(256) }
346
+ buffer.write(mask_key.pack("C*"))
347
+ payload = apply_mask(payload, mask_key)
348
+ end
349
+ buffer.write(payload)
350
+ write(buffer.string)
351
+ end
352
+
353
+ def gets(rs = $/)
354
+ line = @socket.gets(rs)
355
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
356
+ return line
357
+ end
358
+
359
+ def read(num_bytes)
360
+ str = @socket.read(num_bytes)
361
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
362
+ if str && str.bytesize == num_bytes
363
+ return str
364
+ else
365
+ raise(EOFError)
366
+ end
367
+ end
368
+
369
+ def write(data)
370
+ if WebSocket.debug
371
+ data.scan(/\G(.*?(\n|\z))/n) do
372
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
373
+ end
374
+ end
375
+ @socket.write(data)
376
+ end
377
+
378
+ def flush()
379
+ @socket.flush()
380
+ end
381
+
382
+ def write_byte(buffer, byte)
383
+ buffer.write([byte].pack("C"))
384
+ end
385
+
386
+ def security_digest(key)
387
+ return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
388
+ end
389
+
390
+ def hixie_76_security_digest(key1, key2, key3)
391
+ bytes1 = websocket_key_to_bytes(key1)
392
+ bytes2 = websocket_key_to_bytes(key2)
393
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
394
+ end
395
+
396
+ def apply_mask(payload, mask_key)
397
+ orig_bytes = payload.unpack("C*")
398
+ new_bytes = []
399
+ orig_bytes.each_with_index() do |b, i|
400
+ new_bytes.push(b ^ mask_key[i % 4])
401
+ end
402
+ return new_bytes.pack("C*")
403
+ end
404
+
405
+ def generate_key()
406
+ spaces = 1 + rand(12)
407
+ max = 0xffffffff / spaces
408
+ number = rand(max + 1)
409
+ key = (number * spaces).to_s()
410
+ (1 + rand(12)).times() do
411
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
412
+ pos = rand(key.size + 1)
413
+ key[pos...pos] = char
414
+ end
415
+ spaces.times() do
416
+ pos = 1 + rand(key.size - 1)
417
+ key[pos...pos] = " "
418
+ end
419
+ return key
420
+ end
421
+
422
+ def generate_key3()
423
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
424
+ end
425
+
426
+ def websocket_key_to_bytes(key)
427
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
428
+ return [num].pack("N")
429
+ end
430
+
431
+ def force_encoding(str, encoding)
432
+ if str.respond_to?(:force_encoding)
433
+ return str.force_encoding(encoding)
434
+ else
435
+ return str
436
+ end
437
+ end
438
+
439
+ def ssl_handshake(socket)
440
+ ssl_context = OpenSSL::SSL::SSLContext.new()
441
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
442
+ ssl_socket.sync_close = true
443
+ ssl_socket.connect()
444
+ return ssl_socket
445
+ end
446
+
447
+ end
448
+
449
+
450
+ class WebSocketServer
451
+
452
+ def initialize(params_or_uri, params = nil)
453
+ if params
454
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
455
+ params[:port] ||= uri.port
456
+ params[:accepted_domains] ||= [uri.host]
457
+ else
458
+ params = params_or_uri
459
+ end
460
+ @port = params[:port] || 80
461
+ @accepted_domains = params[:accepted_domains]
462
+ if !@accepted_domains
463
+ raise(ArgumentError, "params[:accepted_domains] is required")
464
+ end
465
+ if params[:host]
466
+ @tcp_server = TCPServer.open(params[:host], @port)
467
+ else
468
+ @tcp_server = TCPServer.open(@port)
469
+ end
470
+ end
471
+
472
+ attr_reader(:tcp_server, :port, :accepted_domains)
473
+
474
+ def run(&block)
475
+ @run_threads = []
476
+ loop do
477
+ @run_threads << Thread.start(accept()) do |s|
478
+ begin
479
+ ws = create_web_socket(s)
480
+ yield(ws) if ws
481
+ rescue => ex
482
+ print_backtrace(ex)
483
+ ensure
484
+ begin
485
+ ws.close_socket() if ws
486
+ rescue
487
+ end
488
+ end
489
+ end
490
+ end
491
+ end
492
+
493
+ def quit
494
+ @run_threads.map(&:kill)
495
+ end
496
+
497
+ def accept()
498
+ return @tcp_server.accept()
499
+ end
500
+
501
+ def accepted_origin?(origin)
502
+ domain = origin_to_domain(origin)
503
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
504
+ end
505
+
506
+ def origin_to_domain(origin)
507
+ if origin == "null" || origin == "file://" # local file
508
+ return "null"
509
+ else
510
+ return URI.parse(origin).host
511
+ end
512
+ end
513
+
514
+ def create_web_socket(socket)
515
+ ch = socket.getc()
516
+ if ch == ?<
517
+ # This is Flash socket policy file request, not an actual Web Socket connection.
518
+ send_flash_socket_policy_file(socket)
519
+ return nil
520
+ else
521
+ socket.ungetc(ch) if ch
522
+ return WebSocket.new(socket, :server => self)
523
+ end
524
+ end
525
+
526
+ private
527
+
528
+ def print_backtrace(ex)
529
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
530
+ for s in ex.backtrace[1..-1]
531
+ $stderr.printf(" %s\n", s)
532
+ end
533
+ end
534
+
535
+ # Handles Flash socket policy file request sent when web-socket-js is used:
536
+ # http://github.com/gimite/web-socket-js/tree/master
537
+ def send_flash_socket_policy_file(socket)
538
+ socket.puts('<?xml version="1.0"?>')
539
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
540
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
541
+ socket.puts('<cross-domain-policy>')
542
+ for domain in @accepted_domains
543
+ next if domain == "file://"
544
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
545
+ end
546
+ socket.puts('</cross-domain-policy>')
547
+ socket.close()
548
+ end
549
+
550
+ end
551
+
552
+
553
+ if __FILE__ == $0
554
+ Thread.abort_on_exception = true
555
+
556
+ if ARGV[0] == "server" && ARGV.size == 3
557
+
558
+ server = WebSocketServer.new(
559
+ :accepted_domains => [ARGV[1]],
560
+ :port => ARGV[2].to_i())
561
+ puts("Server is running at port %d" % server.port)
562
+ server.run() do |ws|
563
+ puts("Connection accepted")
564
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
565
+ if ws.path == "/"
566
+ ws.handshake()
567
+ while data = ws.receive()
568
+ printf("Received: %p\n", data)
569
+ ws.send(data)
570
+ printf("Sent: %p\n", data)
571
+ end
572
+ else
573
+ ws.handshake("404 Not Found")
574
+ end
575
+ puts("Connection closed")
576
+ end
577
+
578
+ elsif ARGV[0] == "client" && ARGV.size == 2
579
+
580
+ client = WebSocket.new(ARGV[1])
581
+ puts("Connected")
582
+ Thread.new() do
583
+ while data = client.receive()
584
+ printf("Received: %p\n", data)
585
+ end
586
+ end
587
+ $stdin.each_line() do |line|
588
+ data = line.chomp()
589
+ client.send(data)
590
+ printf("Sent: %p\n", data)
591
+ end
592
+
593
+ else
594
+
595
+ $stderr.puts("Usage:")
596
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
597
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
598
+ exit(1)
599
+
600
+ end
601
+ end