libwebsocket 0.0.4
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/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
|