funnel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +56 -0
- data/bin/funnel +33 -0
- data/funnel.gemspec +37 -0
- data/lib/funnel.rb +19 -0
- data/lib/funnel/logger.rb +24 -0
- data/lib/funnel/routing/route.rb +19 -0
- data/lib/funnel/routing/router.rb +54 -0
- data/lib/funnel/routing/routes.rb +40 -0
- data/lib/funnel/server.rb +25 -0
- data/lib/funnel/servers/dummy_server.rb +20 -0
- data/lib/funnel/web_socket/connection.rb +71 -0
- data/lib/funnel/web_socket/frame.rb +22 -0
- data/lib/funnel/web_socket/headers.rb +48 -0
- data/templates/application/config/boot.rb +23 -0
- data/templates/application/config/config.rb +16 -0
- data/templates/application/config/routes.rb +3 -0
- data/templates/application/script/run +2 -0
- metadata +79 -0
data/README.markdown
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#funnel - a realtime resource-centric application framework
|
2
|
+
|
3
|
+
##Installation
|
4
|
+
|
5
|
+
gem install funnel -s http://gemcutter.org
|
6
|
+
|
7
|
+
##Creating a project
|
8
|
+
|
9
|
+
funnel myapp
|
10
|
+
|
11
|
+
##Project Hierarchy
|
12
|
+
|
13
|
+
/config
|
14
|
+
boot.rb
|
15
|
+
routes.rb
|
16
|
+
config.rb
|
17
|
+
/handlers -> your real time resource handlers
|
18
|
+
time.rb
|
19
|
+
game.rb
|
20
|
+
xmpp.rb
|
21
|
+
amqp.rb
|
22
|
+
/script
|
23
|
+
server
|
24
|
+
|
25
|
+
|
26
|
+
## your first "handler"
|
27
|
+
|
28
|
+
echo_handler.rb
|
29
|
+
|
30
|
+
class EchoHandler < Funnel::WebSocket::Connection
|
31
|
+
|
32
|
+
#called when the connection is ready to send
|
33
|
+
#and receive data
|
34
|
+
def on_ready
|
35
|
+
end
|
36
|
+
|
37
|
+
#called on disconnect
|
38
|
+
def on_disconnect
|
39
|
+
end
|
40
|
+
|
41
|
+
#right back at you
|
42
|
+
def on_data msg
|
43
|
+
send_message msg
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
set up the route in config/routes.rb
|
49
|
+
|
50
|
+
map.connect "/echo" => EchoHandler
|
51
|
+
|
52
|
+
##Dependencies
|
53
|
+
- eventmachine http://github.com/eventmachine/eventmachine
|
54
|
+
|
55
|
+
|
56
|
+
# tired... more info coming soon
|
data/bin/funnel
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'funnel'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
|
12
|
+
opts.banner = "Usage: funnel [options] application_name"
|
13
|
+
|
14
|
+
options[:verbose] = false
|
15
|
+
|
16
|
+
opts.on( '-v', '--verbose', 'Output more information' ) do
|
17
|
+
options[:verbose] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
21
|
+
puts opts
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
if ARGV.length < 1
|
26
|
+
puts opts
|
27
|
+
exit
|
28
|
+
end
|
29
|
+
|
30
|
+
end.parse!
|
31
|
+
|
32
|
+
app = Funnel::Generators::Application.new
|
33
|
+
app.generate "#{Dir.getwd}/#{ARGV[0]}", options
|
data/funnel.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = 'funnel'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.date = '2009-12-30'
|
5
|
+
s.summary = 'A realtime resource-centric application framework'
|
6
|
+
s.email = 'dan.simpson@gmail.com'
|
7
|
+
s.homepage = 'http://github.com/dansimpson/funnel'
|
8
|
+
s.description = 'A realtime resource-centric application framework'
|
9
|
+
s.has_rdoc = false
|
10
|
+
|
11
|
+
s.bindir = 'bin'
|
12
|
+
s.executables = ['funnel']
|
13
|
+
|
14
|
+
s.authors = ['Dan Simpson']
|
15
|
+
s.add_dependency('eventmachine', '>= 0.12.4')
|
16
|
+
|
17
|
+
|
18
|
+
s.files = [
|
19
|
+
'README.markdown',
|
20
|
+
'funnel.gemspec',
|
21
|
+
'lib/funnel.rb',
|
22
|
+
'lib/funnel/logger.rb',
|
23
|
+
'lib/funnel/web_socket/frame.rb',
|
24
|
+
'lib/funnel/web_socket/headers.rb',
|
25
|
+
'lib/funnel/web_socket/connection.rb',
|
26
|
+
'lib/funnel/routing/route.rb',
|
27
|
+
'lib/funnel/routing/routes.rb',
|
28
|
+
'lib/funnel/routing/router.rb',
|
29
|
+
'lib/funnel/server.rb',
|
30
|
+
'lib/funnel/servers/dummy_server.rb',
|
31
|
+
'templates/application/config/boot.rb',
|
32
|
+
'templates/application/config/config.rb',
|
33
|
+
'templates/application/config/routes.rb',
|
34
|
+
'templates/application/script/run',
|
35
|
+
'templates/application/handlers'
|
36
|
+
]
|
37
|
+
end
|
data/lib/funnel.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
module Funnel
|
6
|
+
module WebSocket
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'funnel/logger.rb'
|
11
|
+
require 'funnel/generators/application.rb'
|
12
|
+
require 'funnel/web_socket/frame.rb'
|
13
|
+
require 'funnel/web_socket/headers.rb'
|
14
|
+
require 'funnel/web_socket/connection.rb'
|
15
|
+
require 'funnel/routing/route.rb'
|
16
|
+
require 'funnel/routing/routes.rb'
|
17
|
+
require 'funnel/routing/router.rb'
|
18
|
+
require 'funnel/server.rb'
|
19
|
+
require 'funnel/servers/dummy_server.rb'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Logger
|
2
|
+
|
3
|
+
@@threshhold = 3
|
4
|
+
|
5
|
+
def self.info msg
|
6
|
+
log msg if @@threshhold <= 3
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.debug msf
|
10
|
+
log msg if @@threshhold <= 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.warning msg
|
14
|
+
log msg if @@threshhold <= 2
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def self.log msg
|
20
|
+
puts "#{Time.now} - #{msg}"
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Funnel
|
2
|
+
module Routing
|
3
|
+
|
4
|
+
class Router < WebSocket::Connection
|
5
|
+
|
6
|
+
# parse headers, validate headers, accept
|
7
|
+
# connection, and change the default handler
|
8
|
+
def receive_data data
|
9
|
+
|
10
|
+
@headers = WebSocket::Headers.parse(data)
|
11
|
+
|
12
|
+
if valid_connection? && valid_route?
|
13
|
+
accept_connection
|
14
|
+
defer_connection
|
15
|
+
else
|
16
|
+
close_connection
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
# is this a websocket connection?
|
22
|
+
def valid_connection?
|
23
|
+
@headers[:connection] =~ /upgrade/i && @headers[:upgrade] =~ /websocket/i
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: add WebSocket-Protocol
|
27
|
+
def accept_connection
|
28
|
+
response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
29
|
+
response << "Upgrade: WebSocket\r\n"
|
30
|
+
response << "Connection: Upgrade\r\n"
|
31
|
+
response << "WebSocket-Origin: #{origin}\r\n"
|
32
|
+
response << "WebSocket-Location: ws://#{host}#{path}\r\n\r\n"
|
33
|
+
|
34
|
+
send_data response
|
35
|
+
end
|
36
|
+
|
37
|
+
# change the handler from this instance, to a new instance
|
38
|
+
# of the type defined by the routing map
|
39
|
+
def defer_connection
|
40
|
+
puts "got this far"
|
41
|
+
EM.instance_variable_get("@conns")[@signature] = Routes.get_handler(path).send(:new, @signature)
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid_route?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def requested_protocol
|
49
|
+
@header[:websocket_protocol] || :default
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Funnel
|
2
|
+
module Routing
|
3
|
+
|
4
|
+
class Routes
|
5
|
+
|
6
|
+
@@instance = nil
|
7
|
+
|
8
|
+
def self.get_handler path
|
9
|
+
@@instance.get_handler(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.draw
|
13
|
+
@@instance = Routes.new unless @@instance
|
14
|
+
yield @@instance
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@routes = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect path, opts
|
23
|
+
@routes[path] = Route.new(path, opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
def default opts
|
27
|
+
@routes[:default] = Route.new(:default, opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_default
|
31
|
+
@routes.key?(:default) ? @routes[:default].handler : Funnel::Servers::DummyServer
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_handler path
|
35
|
+
@routes.key?(path) ? @routes[path].handler : @routes[:default].handler
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Funnel
|
2
|
+
class Server
|
3
|
+
|
4
|
+
def self.start host, port
|
5
|
+
|
6
|
+
EM.epoll if EM.epoll?
|
7
|
+
EM.kqueue if EM.kqueue?
|
8
|
+
|
9
|
+
EM.run do
|
10
|
+
|
11
|
+
trap("TERM") { stop }
|
12
|
+
trap("INT") { stop }
|
13
|
+
|
14
|
+
EM.start_server(host, port, Routing::Router)
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.stop
|
21
|
+
EM.stop
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Funnel
|
2
|
+
module Servers
|
3
|
+
class DummyServer < Funnel::WebSocket::Connection
|
4
|
+
|
5
|
+
def on_ready
|
6
|
+
Logger.info("DummyServer - on_ready")
|
7
|
+
end
|
8
|
+
|
9
|
+
def on_data data
|
10
|
+
Logger.info("DummyServer - on_data")
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_disconnect
|
14
|
+
Logger.info("DummyServer - on_disconnect")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Funnel
|
2
|
+
module WebSocket
|
3
|
+
|
4
|
+
class Connection < EM::Connection
|
5
|
+
|
6
|
+
attr_accessor :headers
|
7
|
+
|
8
|
+
###
|
9
|
+
# helper methods
|
10
|
+
###
|
11
|
+
|
12
|
+
def cookies
|
13
|
+
@headers[:cookies]
|
14
|
+
end
|
15
|
+
|
16
|
+
def host
|
17
|
+
@headers[:host]
|
18
|
+
end
|
19
|
+
|
20
|
+
def origin
|
21
|
+
@headers[:origin]
|
22
|
+
end
|
23
|
+
|
24
|
+
def path
|
25
|
+
@headers[:path]
|
26
|
+
end
|
27
|
+
|
28
|
+
def protocol
|
29
|
+
@headers[:websocket_protocol]
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
#override this
|
35
|
+
def on_ready
|
36
|
+
puts "Connect"
|
37
|
+
end
|
38
|
+
|
39
|
+
#override this
|
40
|
+
def on_disconnect
|
41
|
+
puts "Disconnect"
|
42
|
+
end
|
43
|
+
|
44
|
+
#override this
|
45
|
+
def on_data data
|
46
|
+
puts "Data"
|
47
|
+
end
|
48
|
+
|
49
|
+
#called from EM
|
50
|
+
def unbind
|
51
|
+
on_disconnect
|
52
|
+
end
|
53
|
+
|
54
|
+
#called from EM
|
55
|
+
def post_init
|
56
|
+
@headers = {}
|
57
|
+
on_ready
|
58
|
+
end
|
59
|
+
|
60
|
+
#called from EM
|
61
|
+
def receive_data data
|
62
|
+
on_data Frame.decode(data)
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_message msg
|
66
|
+
send_data Frame.encode(msg)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Funnel
|
2
|
+
module WebSocket
|
3
|
+
|
4
|
+
class Frame
|
5
|
+
|
6
|
+
# Frames need to start with 0x00-0x7f byte and end with
|
7
|
+
# an 0xFF byte. Per spec, we can also set the first
|
8
|
+
# byte to a value betweent 0x80 and 0xFF, followed by
|
9
|
+
# a leading length indicator. No support yet.
|
10
|
+
def self.encode data
|
11
|
+
"\x00#{data}\xff"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Strip leading and trailing bytes
|
15
|
+
def self.decode data
|
16
|
+
data.gsub(/^(\x00)|(\xff)$/, "")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Funnel
|
2
|
+
module WebSocket
|
3
|
+
|
4
|
+
class Headers
|
5
|
+
|
6
|
+
PATH_PATTERN = /^GET (\/[^\s]*) HTTP\/1\.1$/
|
7
|
+
HEADER_PATTERN = /^([^:]+):\s*([^$]+)/
|
8
|
+
|
9
|
+
# Parse http style headers into a ruby hash
|
10
|
+
def self.parse data
|
11
|
+
lines = data.split("\r\n")
|
12
|
+
line = lines.shift
|
13
|
+
|
14
|
+
headers = {}
|
15
|
+
|
16
|
+
if line =~ PATH_PATTERN
|
17
|
+
headers[:path] = PATH_PATTERN.match(line)[1]
|
18
|
+
else
|
19
|
+
throw "Unrecognized Header!"
|
20
|
+
end
|
21
|
+
|
22
|
+
lines.each do |line|
|
23
|
+
kvp = HEADER_PATTERN.match(line)
|
24
|
+
headers[kvp[1].strip.downcase.to_sym] = kvp[2].strip
|
25
|
+
end
|
26
|
+
|
27
|
+
headers
|
28
|
+
end
|
29
|
+
|
30
|
+
# encode ruby hash into HTTP style headers
|
31
|
+
def self.encode data
|
32
|
+
result = ""
|
33
|
+
|
34
|
+
data.each_pair do |k,v|
|
35
|
+
result << k.to_s
|
36
|
+
result << ": "
|
37
|
+
result << v.to_s
|
38
|
+
result << "\r\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
result << "\r\n"
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
FUNNEL_ROOT = File.expand_path('../', __FILE__)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'funnel'
|
6
|
+
|
7
|
+
#local includes
|
8
|
+
require 'config'
|
9
|
+
require 'funnel'
|
10
|
+
|
11
|
+
#load servers defined by user
|
12
|
+
handlers = Dir.entries(FUNNEL_ROOT + "/handlers").select { |n| not n =~ /^\.+$/ }
|
13
|
+
if handlers
|
14
|
+
handlers.each do |h|
|
15
|
+
require FUNNEL_ROOT + "/handlers/#{h}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
#require routes and map them
|
20
|
+
require 'routes'
|
21
|
+
|
22
|
+
#start the server
|
23
|
+
Funnel::Server.start(Configuration.get(:host), Configuration.get(:port))
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: funnel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Simpson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-30 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: eventmachine
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.12.4
|
24
|
+
version:
|
25
|
+
description: A realtime resource-centric application framework
|
26
|
+
email: dan.simpson@gmail.com
|
27
|
+
executables:
|
28
|
+
- funnel
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- README.markdown
|
35
|
+
- funnel.gemspec
|
36
|
+
- lib/funnel.rb
|
37
|
+
- lib/funnel/logger.rb
|
38
|
+
- lib/funnel/web_socket/frame.rb
|
39
|
+
- lib/funnel/web_socket/headers.rb
|
40
|
+
- lib/funnel/web_socket/connection.rb
|
41
|
+
- lib/funnel/routing/route.rb
|
42
|
+
- lib/funnel/routing/routes.rb
|
43
|
+
- lib/funnel/routing/router.rb
|
44
|
+
- lib/funnel/server.rb
|
45
|
+
- lib/funnel/servers/dummy_server.rb
|
46
|
+
- templates/application/config/boot.rb
|
47
|
+
- templates/application/config/config.rb
|
48
|
+
- templates/application/config/routes.rb
|
49
|
+
- templates/application/script/run
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/dansimpson/funnel
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.3.5
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: A realtime resource-centric application framework
|
78
|
+
test_files: []
|
79
|
+
|