libwebsocket 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +88 -0
- data/Rakefile +26 -0
- data/examples/eventmachine_server.rb +36 -0
- data/examples/plain_client.rb +59 -0
- data/examples/thin_server.rb +69 -0
- data/lib/libwebsocket.rb +17 -0
- data/lib/libwebsocket/cookie.rb +60 -0
- data/lib/libwebsocket/cookie/request.rb +48 -0
- data/lib/libwebsocket/cookie/response.rb +44 -0
- data/lib/libwebsocket/frame.rb +67 -0
- data/lib/libwebsocket/handshake.rb +28 -0
- data/lib/libwebsocket/handshake/client.rb +129 -0
- data/lib/libwebsocket/handshake/server.rb +114 -0
- data/lib/libwebsocket/message.rb +167 -0
- data/lib/libwebsocket/request.rb +288 -0
- data/lib/libwebsocket/response.rb +215 -0
- data/lib/libwebsocket/stateful.rb +24 -0
- data/lib/libwebsocket/url.rb +67 -0
- data/test/libwebsocket/cookie/request.rb +37 -0
- data/test/libwebsocket/cookie/response.rb +32 -0
- data/test/libwebsocket/handshake/test_client.rb +64 -0
- data/test/libwebsocket/handshake/test_server.rb +39 -0
- data/test/libwebsocket/test_cookie.rb +21 -0
- data/test/libwebsocket/test_frame.rb +65 -0
- data/test/libwebsocket/test_message.rb +16 -0
- data/test/libwebsocket/test_request_75.rb +145 -0
- data/test/libwebsocket/test_request_76.rb +122 -0
- data/test/libwebsocket/test_request_common.rb +26 -0
- data/test/libwebsocket/test_response_75.rb +80 -0
- data/test/libwebsocket/test_response_76.rb +115 -0
- data/test/libwebsocket/test_response_common.rb +17 -0
- data/test/libwebsocket/test_url.rb +49 -0
- data/test/test_helper.rb +4 -0
- metadata +116 -0
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Protocol::WebSocket
|
2
|
+
|
3
|
+
A WebSocket message parser/constructor. It is not a server and is not meant to
|
4
|
+
be one. It can be used in any server, event loop etc.
|
5
|
+
|
6
|
+
## Server handshake
|
7
|
+
|
8
|
+
h = LibWebSocket::Handshake::Server.new
|
9
|
+
|
10
|
+
# Parse client request
|
11
|
+
h.parse \<<EOF
|
12
|
+
GET /demo HTTP/1.1
|
13
|
+
Upgrade: WebSocket
|
14
|
+
Connection: Upgrade
|
15
|
+
Host: example.com
|
16
|
+
Origin: http://example.com
|
17
|
+
Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
18
|
+
Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
19
|
+
|
20
|
+
Tm[K T2u
|
21
|
+
EOF
|
22
|
+
|
23
|
+
h.error # Check if there were any errors
|
24
|
+
h.done? # Returns true
|
25
|
+
|
26
|
+
# Create response
|
27
|
+
h.to_s # HTTP/1.1 101 WebSocket Protocol Handshake
|
28
|
+
# Upgrade: WebSocket
|
29
|
+
# Connection: Upgrade
|
30
|
+
# Sec-WebSocket-Origin: http://example.com
|
31
|
+
# Sec-WebSocket-Location: ws://example.com/demo
|
32
|
+
#
|
33
|
+
# fQJ,fN/4F4!~K~MH
|
34
|
+
|
35
|
+
## Client handshake
|
36
|
+
|
37
|
+
h = LibWebSocket::Handshake::Client.new(url => 'ws://example.com')
|
38
|
+
|
39
|
+
# Create request
|
40
|
+
h.to_s # GET /demo HTTP/1.1
|
41
|
+
# Upgrade: WebSocket
|
42
|
+
# Connection: Upgrade
|
43
|
+
# Host: example.com
|
44
|
+
# Origin: http://example.com
|
45
|
+
# Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
46
|
+
# Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
47
|
+
#
|
48
|
+
# Tm[K T2u
|
49
|
+
|
50
|
+
# Parse server response
|
51
|
+
h.parse \<<EOF
|
52
|
+
HTTP/1.1 101 WebSocket Protocol Handshake
|
53
|
+
Upgrade: WebSocket
|
54
|
+
Connection: Upgrade
|
55
|
+
Sec-WebSocket-Origin: http://example.com
|
56
|
+
Sec-WebSocket-Location: ws://example.com/demo
|
57
|
+
|
58
|
+
fQJ,fN/4F4!~K~MH
|
59
|
+
EOF
|
60
|
+
|
61
|
+
h.error # Check if there were any errors
|
62
|
+
h.done? # Returns true
|
63
|
+
|
64
|
+
## Parsing and constructing frames
|
65
|
+
|
66
|
+
# Create frame
|
67
|
+
frame = LibWebSocket::Frame.new('123')
|
68
|
+
frame.to_s # \x00123\xff
|
69
|
+
|
70
|
+
# Parse frames
|
71
|
+
frame = LibWebSocket::Frame.new
|
72
|
+
frame.append("123\x00foo\xff56\x00bar\xff789")
|
73
|
+
frame.next # foo
|
74
|
+
frame.next # bar
|
75
|
+
|
76
|
+
## Examples
|
77
|
+
|
78
|
+
For examples on how to use LibWebSocket with various event loops see
|
79
|
+
examples directory in the repository.
|
80
|
+
|
81
|
+
## Copyright
|
82
|
+
|
83
|
+
Copyright (C) 2010, Bernard Potocki.
|
84
|
+
|
85
|
+
Based on protocol-websocket perl distribution by Viacheslav Tykhanovskyi.
|
86
|
+
|
87
|
+
This program is free software, you can redistribute it and/or modify it under
|
88
|
+
the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'lib/libwebsocket'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList['test/**/test_*.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gemspec|
|
15
|
+
gemspec.name = "libwebsocket"
|
16
|
+
gemspec.version = LibWebSocket::VERSION
|
17
|
+
gemspec.summary = "Universal Ruby library to handle WebSocket protocol"
|
18
|
+
gemspec.description = "Universal Ruby library to handle WebSocket protocol"
|
19
|
+
gemspec.email = "bernard.potocki@imanel.org"
|
20
|
+
gemspec.homepage = "http://github.com/imanel/libwebsocket"
|
21
|
+
gemspec.authors = ["Bernard Potocki"]
|
22
|
+
gemspec.files.exclude ".gitignore"
|
23
|
+
end
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'eventmachine'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/libwebsocket')
|
6
|
+
|
7
|
+
module EchoServer
|
8
|
+
def receive_data(data)
|
9
|
+
@hs ||= LibWebSocket::Handshake::Server.new
|
10
|
+
@frame ||= LibWebSocket::Frame.new
|
11
|
+
|
12
|
+
if !@hs.done?
|
13
|
+
@hs.parse(data)
|
14
|
+
|
15
|
+
if @hs.done?
|
16
|
+
send_data(@hs.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
@frame.append(data)
|
23
|
+
|
24
|
+
while message = @frame.next
|
25
|
+
send_data @frame.new(message).to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
EventMachine::run do
|
32
|
+
host = '0.0.0.0'
|
33
|
+
port = 8080
|
34
|
+
EventMachine::start_server host, port, EchoServer
|
35
|
+
puts "Started EchoServer on #{host}:#{port}..."
|
36
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "socket"
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/libwebsocket')
|
3
|
+
|
4
|
+
class WebSocket
|
5
|
+
|
6
|
+
def initialize(url, params = {})
|
7
|
+
@hs ||= LibWebSocket::Handshake::Client.new(:url => url, :version => params[:version])
|
8
|
+
@frame ||= LibWebSocket::Frame.new
|
9
|
+
|
10
|
+
@socket = TCPSocket.new(@hs.url.host, @hs.url.port || 80)
|
11
|
+
|
12
|
+
@socket.write(@hs.to_s)
|
13
|
+
@socket.flush
|
14
|
+
|
15
|
+
loop do
|
16
|
+
data = @socket.getc
|
17
|
+
next if data.nil?
|
18
|
+
|
19
|
+
result = @hs.parse(data.chr)
|
20
|
+
|
21
|
+
raise @hs.error unless result
|
22
|
+
|
23
|
+
if @hs.done?
|
24
|
+
@handshaked = true
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def send(data)
|
31
|
+
raise "no handshake!" unless @handshaked
|
32
|
+
|
33
|
+
data = @frame.new(data).to_s
|
34
|
+
@socket.write data
|
35
|
+
@socket.flush
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive
|
39
|
+
raise "no handshake!" unless @handshaked
|
40
|
+
|
41
|
+
data = @socket.gets("\xff")
|
42
|
+
@frame.append(data)
|
43
|
+
|
44
|
+
messages = []
|
45
|
+
while message = @frame.next
|
46
|
+
messages << message
|
47
|
+
end
|
48
|
+
messages
|
49
|
+
end
|
50
|
+
|
51
|
+
def socket
|
52
|
+
@socket
|
53
|
+
end
|
54
|
+
|
55
|
+
def close
|
56
|
+
@socket.close
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'thin'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/libwebsocket')
|
6
|
+
|
7
|
+
# This is required due to thin incompatibility with streamming of data
|
8
|
+
module ThinExtension
|
9
|
+
def self.included(thin_conn)
|
10
|
+
thin_conn.class_eval do
|
11
|
+
alias :pre_process_without_websocket :pre_process
|
12
|
+
alias :pre_process :pre_process_with_websocket
|
13
|
+
|
14
|
+
alias :receive_data_without_websocket :receive_data
|
15
|
+
alias :receive_data :receive_data_with_websocket
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :websocket_client
|
20
|
+
|
21
|
+
def pre_process_with_websocket
|
22
|
+
@request.env['async.connection'] = self
|
23
|
+
pre_process_without_websocket
|
24
|
+
end
|
25
|
+
def receive_data_with_websocket(data)
|
26
|
+
if self.websocket_client
|
27
|
+
self.websocket_client.receive_data(data)
|
28
|
+
else
|
29
|
+
receive_data_without_websocket(data)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
::Thin::Connection.send(:include, ThinExtension)
|
35
|
+
|
36
|
+
class EchoServer
|
37
|
+
def call(env)
|
38
|
+
@hs ||= LibWebSocket::Handshake::Server.new
|
39
|
+
@connection = env['async.connection']
|
40
|
+
|
41
|
+
if !@hs.done?
|
42
|
+
@hs.parse(env)
|
43
|
+
|
44
|
+
if @hs.done?
|
45
|
+
@connection.websocket_client = self
|
46
|
+
resp = @hs.to_rack
|
47
|
+
return resp
|
48
|
+
end
|
49
|
+
|
50
|
+
return
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def receive_data(data)
|
55
|
+
@frame ||= LibWebSocket::Frame.new
|
56
|
+
|
57
|
+
@frame.append(data)
|
58
|
+
|
59
|
+
while message = @frame.next
|
60
|
+
@connection.send_data @frame.new(message).to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Thin::Server.start('127.0.0.1', 8080) do
|
66
|
+
map '/' do
|
67
|
+
run proc{ |env| EchoServer.new.call(env) }
|
68
|
+
end
|
69
|
+
end
|
data/lib/libwebsocket.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Client/server WebSocket message and frame parser/constructor. This module does
|
2
|
+
# not provide a WebSocket server or client, but is made for using in http servers
|
3
|
+
# or clients to provide WebSocket support.
|
4
|
+
module LibWebSocket
|
5
|
+
|
6
|
+
VERSION = '0.0.4' # Version of LibWebSocket
|
7
|
+
|
8
|
+
autoload :Cookie, "#{File.dirname(__FILE__)}/libwebsocket/cookie"
|
9
|
+
autoload :Frame, "#{File.dirname(__FILE__)}/libwebsocket/frame"
|
10
|
+
autoload :Handshake, "#{File.dirname(__FILE__)}/libwebsocket/handshake"
|
11
|
+
autoload :Message, "#{File.dirname(__FILE__)}/libwebsocket/message"
|
12
|
+
autoload :Request, "#{File.dirname(__FILE__)}/libwebsocket/request"
|
13
|
+
autoload :Response, "#{File.dirname(__FILE__)}/libwebsocket/response"
|
14
|
+
autoload :Stateful, "#{File.dirname(__FILE__)}/libwebsocket/stateful"
|
15
|
+
autoload :URL, "#{File.dirname(__FILE__)}/libwebsocket/url"
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
#A base class for LibWebSocket::Cookie::Request and LibWebSocket::Cookie::Response.
|
3
|
+
class Cookie
|
4
|
+
|
5
|
+
autoload :Request, "#{File.dirname(__FILE__)}/cookie/request"
|
6
|
+
autoload :Response, "#{File.dirname(__FILE__)}/cookie/response"
|
7
|
+
|
8
|
+
attr_accessor :pairs
|
9
|
+
|
10
|
+
TOKEN = /[^;,\s"]+/ # Cookie token
|
11
|
+
NAME = /[^;,\s"=]+/ # Cookie name
|
12
|
+
QUOTED_STRING = /"(?:\\"|[^"])+"/ # Cookie quoted value
|
13
|
+
VALUE = /(?:#{TOKEN}|#{QUOTED_STRING})/ # Cookie unquoted value
|
14
|
+
|
15
|
+
def initialize(hash = {})
|
16
|
+
hash.each do |k,v|
|
17
|
+
instance_variable_set("@#{k}",v)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Parse cookie string to array
|
22
|
+
def parse(string)
|
23
|
+
self.pairs = []
|
24
|
+
|
25
|
+
return if string.nil? || string == ''
|
26
|
+
|
27
|
+
while string.slice!(/\s*(#{NAME})\s*(?:=\s*(#{VALUE}))?;?/)
|
28
|
+
attr, value = $1, $2
|
29
|
+
if !value.nil?
|
30
|
+
value.gsub!(/^"/, '')
|
31
|
+
value.gsub!(/"$/, '')
|
32
|
+
value.gsub!(/\\"/, '"')
|
33
|
+
end
|
34
|
+
self.pairs.push([attr, value])
|
35
|
+
end
|
36
|
+
|
37
|
+
return self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert cookie array to string
|
41
|
+
def to_s
|
42
|
+
pairs = []
|
43
|
+
|
44
|
+
self.pairs.each do |pair|
|
45
|
+
string = ''
|
46
|
+
string += pair[0]
|
47
|
+
|
48
|
+
unless pair[1].nil?
|
49
|
+
string += '='
|
50
|
+
string += (!pair[1].match(/^#{VALUE}$/) ? "\"#{pair[1]}\"" : pair[1])
|
51
|
+
end
|
52
|
+
|
53
|
+
pairs.push(string)
|
54
|
+
end
|
55
|
+
|
56
|
+
return pairs.join("; ")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
class Cookie
|
3
|
+
# Construct or parse a WebSocket request cookie.
|
4
|
+
class Request < Cookie
|
5
|
+
|
6
|
+
attr_accessor :name, :value, :version, :path, :domain
|
7
|
+
|
8
|
+
# Parse a WebSocket request cookie.
|
9
|
+
# @example
|
10
|
+
# cookie = LibWebSocket::Cookie::Request.new
|
11
|
+
# cookies = cookie.parse('$Version=1; foo="bar"; $Path=/; bar=baz; $Domain=.example.com')
|
12
|
+
def parse(string)
|
13
|
+
result = super
|
14
|
+
return unless result
|
15
|
+
|
16
|
+
cookies = []
|
17
|
+
|
18
|
+
pair = self.pairs.shift
|
19
|
+
version = pair[1]
|
20
|
+
|
21
|
+
cookie = nil
|
22
|
+
self.pairs.each do |pair|
|
23
|
+
next if pair[0].nil?
|
24
|
+
|
25
|
+
if pair[0].match(/^[^\$]/)
|
26
|
+
cookies.push(cookie) unless cookie.nil?
|
27
|
+
|
28
|
+
cookie = self.build_cookie( :name => pair[0], :value => pair[1], :version => version)
|
29
|
+
elsif pair[0] == '$Path'
|
30
|
+
cookie.path = pair[1]
|
31
|
+
elsif pair[0] == '$Domain'
|
32
|
+
cookie.domain = pair[1]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
cookies.push(cookie) unless cookie.nil?
|
37
|
+
|
38
|
+
return cookies
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def build_cookie(hash)
|
44
|
+
self.class.new(hash)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
class Cookie
|
3
|
+
# Construct or parse a WebSocket response cookie.
|
4
|
+
class Response < Cookie
|
5
|
+
|
6
|
+
attr_accessor :name, :value, :comment, :comment_url, :discard, :max_age, :path, :portlist, :secure
|
7
|
+
|
8
|
+
# Construct a WebSocket response cookie.
|
9
|
+
# @example
|
10
|
+
# cookie = LibWebSocket::Cookie::Response.new(
|
11
|
+
# :name => 'foo',
|
12
|
+
# :value => 'bar',
|
13
|
+
# :discard => 1,
|
14
|
+
# :max_age => 0
|
15
|
+
# )
|
16
|
+
# cookie.to_s # foo=bar; Discard; Max-Age=0; Version=1
|
17
|
+
def to_s
|
18
|
+
pairs = []
|
19
|
+
|
20
|
+
pairs.push([self.name, self.value])
|
21
|
+
|
22
|
+
pairs.push ['Comment', self.comment] if self.comment
|
23
|
+
pairs.push ['CommentURL', self.comment_url] if self.comment_url
|
24
|
+
pairs.push ['Discard'] if self.discard
|
25
|
+
pairs.push ['Max-Age', self.max_age] if self.max_age
|
26
|
+
pairs.push ['Path', self.path] if self.path
|
27
|
+
|
28
|
+
if self.portlist
|
29
|
+
self.portlist = Array(self.portlist)
|
30
|
+
list = self.portlist.join(' ')
|
31
|
+
pairs.push ['Port', "\"#{list}\""]
|
32
|
+
end
|
33
|
+
|
34
|
+
pairs.push ['Secure'] if self.secure
|
35
|
+
pairs.push ['Version', '1']
|
36
|
+
|
37
|
+
self.pairs = pairs
|
38
|
+
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|