rack-bridge 0.5.1 → 0.6.0
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.rdoc +30 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/bin/bridge-store-key +44 -0
- data/lib/bridge/key_tools.rb +81 -0
- data/lib/bridge/server.rb +93 -0
- data/lib/bridge/tcp_server.rb +154 -0
- data/lib/rack/handler/bridge.rb +11 -0
- metadata +15 -10
data/README.rdoc
CHANGED
@@ -1,6 +1,35 @@
|
|
1
1
|
= rack-bridge
|
2
2
|
|
3
|
-
|
3
|
+
Rack-Bridge provides a rack-based server for connecting to a BRIDGE-capable server (like CloudBridge). This can be used with any rack-capable framework that supports automatic loading of the handler, which includes at least rackup and rails. Examples for those two frameworks are below.
|
4
|
+
|
5
|
+
== Examples
|
6
|
+
|
7
|
+
Save a key you got from oncloud.org in your user key store:
|
8
|
+
|
9
|
+
bridge_store_key 1178fbd04c4aea4c8a7f09627bbf67df83a51805:1267585275:pickles.oncloud.org
|
10
|
+
|
11
|
+
Save a key you got from oncloud.org in your app's key store:
|
12
|
+
|
13
|
+
bridge_store_key --site 1178fbd04c4aea4c8a7f09627bbf67df83a51805:1267585275:pickles.oncloud.org
|
14
|
+
|
15
|
+
Use Rackup to connect to pickles.oncloud.org (assumes you stored the key as above):
|
16
|
+
|
17
|
+
rackup -s Bridge -o pickles.oncloud.org config.ru
|
18
|
+
|
19
|
+
Use Rails >= 2.3 to connect to pickles.oncloud.org (assumes you stored the key as above):
|
20
|
+
|
21
|
+
script/server Bridge -b pickles.oncloud.org
|
22
|
+
|
23
|
+
Specify a key through the environment without saving it:
|
24
|
+
|
25
|
+
BRIDGE_KEYS=1178fbd04c4aea4c8a7f09627bbf67df83a51805:1267585275:pickles.oncloud.org rackup -s Bridge -o pickles.oncloud.org config.ru
|
26
|
+
BRIDGE_KEYS=1178fbd04c4aea4c8a7f09627bbf67df83a51805:1267585275:pickles.oncloud.org script/server Bridge -b pickles.oncloud.org
|
27
|
+
|
28
|
+
== More Information
|
29
|
+
|
30
|
+
CloudBridge: http://www.github.com/stormbrew/cloudbridge
|
31
|
+
OnCloud: http://www.oncloud.org
|
32
|
+
Summary of CloudBridge/OnCloud: http://www.stormbrew.ca/2010/03/18/oncloud-org-and-cloudbridge-and-how-the-web-is-like-donkey-kong-country/
|
4
33
|
|
5
34
|
== Note on Patches/Pull Requests
|
6
35
|
|
data/Rakefile
CHANGED
@@ -10,7 +10,7 @@ begin
|
|
10
10
|
gem.email = "graham@stormbrew.ca"
|
11
11
|
gem.homepage = "http://github.com/stormbrew/rack-bridge"
|
12
12
|
gem.authors = ["Graham Batty"]
|
13
|
-
gem.add_dependency "jaws", "
|
13
|
+
gem.add_dependency "jaws", "= 0.6.0"
|
14
14
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
15
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
16
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'bridge/key_tools'
|
4
|
+
|
5
|
+
user_path = Bridge::KeyTools.user_path
|
6
|
+
global_path = Bridge::KeyTools.global_path
|
7
|
+
site_path = Bridge::KeyTools.site_path
|
8
|
+
|
9
|
+
type = :user
|
10
|
+
opt = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{$0} [options] KEY"
|
12
|
+
|
13
|
+
if (user_path)
|
14
|
+
opts.on("u", "--user", "Add to the user registry (#{user_path}) DEFAULT") do
|
15
|
+
type = :user
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if (global_path)
|
20
|
+
opts.on("g", "--global", "Add to the global registry (#{global_path})") do
|
21
|
+
type = :global
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if (site_path)
|
26
|
+
opts.on("s", "--site", "Add to the site registry (#{site_path})") do
|
27
|
+
type = :site
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("h", "--help", "Show this help") do
|
32
|
+
puts(opts)
|
33
|
+
exit(1)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
opt.parse!
|
37
|
+
key = ARGV.shift
|
38
|
+
if (!key)
|
39
|
+
puts("Must supply a key!")
|
40
|
+
puts(opt)
|
41
|
+
exit(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
Bridge::KeyTools.save_key(key, type)
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Bridge
|
2
|
+
module KeyTools
|
3
|
+
# Finds a parent path with name in it recursively. Must pass
|
4
|
+
# a normalized path in (see File.expand_path)
|
5
|
+
def self.find_nearest(path, name)
|
6
|
+
if (Dir.glob(File.join(path, name)).empty?)
|
7
|
+
parent = File.dirname(path)
|
8
|
+
if (parent == path) # can't collapse any further, no match
|
9
|
+
return nil
|
10
|
+
end
|
11
|
+
return find_nearest(parent, name)
|
12
|
+
else
|
13
|
+
return path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the path to a key file in the local site if it can be determined.
|
18
|
+
# Specifically, it'll look for the nearest parent directory with any of the
|
19
|
+
# following under it (in priority order)
|
20
|
+
# script/server
|
21
|
+
# *.ru
|
22
|
+
# .git
|
23
|
+
# .hg
|
24
|
+
def self.site_path(path = Dir.getwd)
|
25
|
+
nearest = find_nearest(path, "script/server") ||
|
26
|
+
find_nearest(path, "*.ru") ||
|
27
|
+
find_nearest(path, ".git") ||
|
28
|
+
find_nearest(path, ".hg")
|
29
|
+
return nearest && File.join(nearest, ".bridge_keys")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the path to a key file in the user's home directory if one exists.
|
33
|
+
# Returns nil if not found
|
34
|
+
def self.user_path
|
35
|
+
File.join(ENV["HOME"], ".bridge_keys") if (ENV["HOME"])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the path to the global key file
|
39
|
+
def self.global_path
|
40
|
+
File.join("/etc", "bridge_keys")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns an array of saved keys for the hosts passed in. Note
|
44
|
+
# that it will never load a key for a full wildcard host ("*")
|
45
|
+
# as that would be a serious security concern.
|
46
|
+
def self.load_keys(for_hosts)
|
47
|
+
paths = [site_path, user_path, global_path].compact
|
48
|
+
keys = []
|
49
|
+
|
50
|
+
paths.each do |path|
|
51
|
+
if (File.exists? path)
|
52
|
+
lines = File.readlines(path)
|
53
|
+
lines.each do |key|
|
54
|
+
key.gsub!(%r{\n$}, '')
|
55
|
+
key_string, timestamp, host = key.split(":", 3)
|
56
|
+
next if host == '*'
|
57
|
+
for_hosts.each do |for_host|
|
58
|
+
if (%r{(^|\*|\.)#{Regexp.escape(host)}$}.match(for_host))
|
59
|
+
keys << key
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
return keys.uniq
|
66
|
+
end
|
67
|
+
|
68
|
+
# Saves +key+ in the path identified by +type+ (:global, :user, :site).
|
69
|
+
# Will not save a key for host '*'
|
70
|
+
def self.save_key(key, type = :user)
|
71
|
+
key_string, timestamp, host = key.split(":", 3)
|
72
|
+
return if host == '*'
|
73
|
+
path = send(:"#{type}_path")
|
74
|
+
if (path)
|
75
|
+
File.open(path, "a") do |f|
|
76
|
+
f.puts(key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'jaws/server'
|
2
|
+
require 'bridge/tcp_server'
|
3
|
+
require 'bridge/key_tools'
|
4
|
+
|
5
|
+
module Bridge
|
6
|
+
# An HTTP Rack-based server based on the Jaws web server.
|
7
|
+
class Server < Jaws::Server
|
8
|
+
DefaultOptions = Jaws::Server::DefaultOptions.merge({
|
9
|
+
:UseKeyFiles => true,
|
10
|
+
:Keys => "",
|
11
|
+
:Port => 8079,
|
12
|
+
})
|
13
|
+
|
14
|
+
# The host information to set up where to listen. It's formatted
|
15
|
+
# as follows:
|
16
|
+
# [listen.host1,listen.host2,...@]bridge.host
|
17
|
+
# where each of the listen.hosts is a host that this server
|
18
|
+
# should handle requests for and the bridge.host is the address
|
19
|
+
# of the bridge host itself. Note that if you're only listening
|
20
|
+
# on one host and it shares its ip with the bridge server itself,
|
21
|
+
# you can simply give it that host (ie. pickles.oncloud.org will
|
22
|
+
# connect to pickles.oncloud.org to server for pickles.oncloud.org)
|
23
|
+
# An example of a more complicated example might be:
|
24
|
+
# www.blah.com,blah.com@blorp.com
|
25
|
+
# You can also have leading wildcards in the hosts:
|
26
|
+
# *.blorp.com,blorp.com@blorp.com
|
27
|
+
# Or more concisely (*. matches subdomains, *blah matches *.blah and blah):
|
28
|
+
# *blorp.com@blorp.com
|
29
|
+
# Or yet more concisely (without an @, wildcards are removed to find the address):
|
30
|
+
# *blorp.com
|
31
|
+
# And of course, to match everything on a bridge server:
|
32
|
+
# *@blorp.com
|
33
|
+
attr_accessor :host
|
34
|
+
# Whether or not the local key files (.bridge_keys in
|
35
|
+
# the current app's directory, the user's homedir, and
|
36
|
+
# /etc) should be searched for matching keys when connecting
|
37
|
+
# to the server. Also set with options[:UseKeyFiles]
|
38
|
+
attr_accessor :use_key_files
|
39
|
+
# The keys that should be sent to the BRIDGE server.
|
40
|
+
# Also set with options[:Keys]
|
41
|
+
attr_accessor :keys
|
42
|
+
|
43
|
+
# The hosts this server responds to. Derived from #host
|
44
|
+
attr_reader :listen_hosts
|
45
|
+
# The bridge server we intend to connect to. Derived from #host
|
46
|
+
attr_reader :bridge_server
|
47
|
+
|
48
|
+
def initialize(options = DefaultOptions)
|
49
|
+
super(DefaultOptions.merge(options))
|
50
|
+
|
51
|
+
@use_key_files = @options[:UseKeyFiles]
|
52
|
+
|
53
|
+
hosts, @bridge_server = @host.split("@", 2)
|
54
|
+
if (@bridge_server.nil?)
|
55
|
+
# no bridge specified, we expect there to be a single host
|
56
|
+
# and (once trimmed of wildcards) it becomes our bridge server
|
57
|
+
# address.
|
58
|
+
@bridge_server = hosts.gsub(%r{^\*\.?}, '')
|
59
|
+
hosts = [hosts]
|
60
|
+
else
|
61
|
+
# there was a bridge, so we can allow multiple hosts and don't need
|
62
|
+
# any magic for the bridge
|
63
|
+
hosts = hosts.split(',')
|
64
|
+
end
|
65
|
+
@listen_hosts = hosts.collect do |h|
|
66
|
+
# We need to expand *blah into *.blah and blah. This is a client-side
|
67
|
+
# convenience, not something the server deals with.
|
68
|
+
if (match = %r{^\*[^\.](.+)$}.match(h))
|
69
|
+
["*." << match[1], match[1]]
|
70
|
+
else
|
71
|
+
h
|
72
|
+
end
|
73
|
+
end.flatten
|
74
|
+
|
75
|
+
@keys = @options[:Keys].split(",")
|
76
|
+
@keys << ENV["BRIDGE_KEYS"] if (ENV["BRIDGE_KEYS"])
|
77
|
+
if (@use_key_files)
|
78
|
+
@keys << Bridge::KeyTools.load_keys(@listen_hosts)
|
79
|
+
end
|
80
|
+
@keys.flatten!
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_listener(options)
|
84
|
+
l = Bridge::TCPServer.new(bridge_server, port, listen_hosts, keys)
|
85
|
+
# There's no shared state externally accessible, so we just make
|
86
|
+
# synchronize on the listener a no-op.
|
87
|
+
def l.synchronize
|
88
|
+
yield
|
89
|
+
end
|
90
|
+
return l
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Bridge
|
4
|
+
# This is the class returned by TCPServer.accept. It is a TCPSocket with a couple of extra features.
|
5
|
+
class TCPSocket < ::TCPSocket
|
6
|
+
class RetryError < RuntimeError
|
7
|
+
attr_reader :timeout
|
8
|
+
def initialize(msg, timeout = 5)
|
9
|
+
super(msg)
|
10
|
+
@timeout = timeout
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(cloud_host, cloud_port, listen_hosts, listen_keys)
|
15
|
+
@cloud_host = cloud_host
|
16
|
+
@cloud_port = cloud_port
|
17
|
+
@listen_hosts = listen_hosts
|
18
|
+
@listen_keys = listen_keys
|
19
|
+
|
20
|
+
super(@cloud_host, @cloud_port)
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_bridge_request()
|
24
|
+
write("BRIDGE / HTTP/1.1\r\n")
|
25
|
+
write("Expect: 100-continue\r\n")
|
26
|
+
@listen_hosts.each {|host|
|
27
|
+
write("Host: #{host}\r\n")
|
28
|
+
}
|
29
|
+
@listen_keys.each {|key|
|
30
|
+
write("Host-Key: #{key}\r\n")
|
31
|
+
}
|
32
|
+
write("\r\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
# This just tries to determine if the server will honor
|
36
|
+
# requests as specified above so that the TCPServer initializer
|
37
|
+
# can error out early if it won't.
|
38
|
+
def verify()
|
39
|
+
send_bridge_request()
|
40
|
+
begin
|
41
|
+
line = gets()
|
42
|
+
match = line.match(%r{^HTTP/1\.[01] ([0-9]{3,3}) (.*)$})
|
43
|
+
if (!match)
|
44
|
+
raise "HTTP BRIDGE error: bridge server sent incorrect reply to bridge request."
|
45
|
+
end
|
46
|
+
case code = match[1].to_i
|
47
|
+
when 100, 101
|
48
|
+
return true
|
49
|
+
when 401 # 401 Access Denied, key wasn't right.
|
50
|
+
raise "HTTP BRIDGE error #{code}: host key was invalid or missing, but required."
|
51
|
+
when 503, 504 # 503 Service Unavailable or 504 Gateway Timeout
|
52
|
+
raise "HTTP BRIDGE error #{code}: could not verify server can handle requests because it's overloaded."
|
53
|
+
else
|
54
|
+
raise "HTTP BRIDGE error #{code}: #{match[2]} unknown error connecting to bridge server."
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
close() # once we do this, we just assume the connection is useless.
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# This does the full setup process on the request, returning only
|
62
|
+
# when the connection is actually available.
|
63
|
+
def setup()
|
64
|
+
send_bridge_request
|
65
|
+
code = nil
|
66
|
+
name = nil
|
67
|
+
headers = []
|
68
|
+
while (line = gets())
|
69
|
+
line = line.strip
|
70
|
+
if (line == "")
|
71
|
+
case code.to_i
|
72
|
+
when 100 # 100 Continue, just a ping. Ignore.
|
73
|
+
code = name = nil
|
74
|
+
headers = []
|
75
|
+
next
|
76
|
+
when 101 # 101 Upgrade, successfuly got a connection.
|
77
|
+
write("HTTP/1.1 100 Continue\r\n\r\n") # let the server know we're still here.
|
78
|
+
return self
|
79
|
+
when 401 # 401 Access Denied, key wasn't right.
|
80
|
+
close()
|
81
|
+
raise "HTTP BRIDGE error #{code}: host key was invalid or missing, but required."
|
82
|
+
when 503, 504 # 503 Service Unavailable or 504 Gateway Timeout, just retry.
|
83
|
+
close()
|
84
|
+
sleep_time = headers.find {|header| header["Retry-After"] } || 5
|
85
|
+
raise RetryError.new("BRIDGE server timed out or is overloaded, wait #{sleep_time}s to try again.", sleep_time)
|
86
|
+
else
|
87
|
+
raise "HTTP BRIDGE error #{code}: #{name} waiting for connection."
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if (!code && !name) # This is the initial response line
|
92
|
+
if (match = line.match(%r{^HTTP/1\.[01] ([0-9]{3,3}) (.*)$}))
|
93
|
+
code = match[1]
|
94
|
+
name = match[2]
|
95
|
+
next
|
96
|
+
else
|
97
|
+
raise "Parse error in BRIDGE request reply."
|
98
|
+
end
|
99
|
+
else
|
100
|
+
if (match = line.match(%r{^(.+?):\s+(.+)$}))
|
101
|
+
headers.push({match[1] => match[2]})
|
102
|
+
else
|
103
|
+
raise "Parse error in BRIDGE request reply's headers."
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
return nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# This class emulates the behaviour of TCPServer, but 'listens' on a cloudbridge
|
112
|
+
# server rather than a local interface. Otherwise attempts to have a mostly identical
|
113
|
+
# interface to ::TCPServer.
|
114
|
+
class TCPServer
|
115
|
+
def initialize(cloud_host, cloud_port, listen_hosts = ['*'], listen_keys = [])
|
116
|
+
@cloud_host = cloud_host
|
117
|
+
@cloud_port = cloud_port
|
118
|
+
@listen_hosts = listen_hosts
|
119
|
+
@listen_keys = listen_keys
|
120
|
+
@closed = false
|
121
|
+
begin
|
122
|
+
TCPSocket.new(@cloud_host, @cloud_port, @listen_hosts, @listen_keys).verify
|
123
|
+
rescue Errno::ECONNREFUSED
|
124
|
+
raise "HTTP BRIDGE Error: No Bridge server at #{@cloud_host}:#{@cloud_port}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def accept()
|
129
|
+
begin
|
130
|
+
# Connect to the cloudbridge and let it know we're available for a connection.
|
131
|
+
# This is all entirely syncronous.
|
132
|
+
begin
|
133
|
+
socket = TCPSocket.new(@cloud_host, @cloud_port, @listen_hosts, @listen_keys)
|
134
|
+
rescue Errno::ECONNREFUSED
|
135
|
+
sleep(0.5)
|
136
|
+
retry
|
137
|
+
end
|
138
|
+
socket.setup
|
139
|
+
return socket
|
140
|
+
rescue RetryError => e
|
141
|
+
sleep(e.timeout)
|
142
|
+
retry
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def close()
|
147
|
+
@closed = true
|
148
|
+
end
|
149
|
+
|
150
|
+
def closed?()
|
151
|
+
return @closed
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 6
|
8
|
+
- 0
|
9
|
+
version: 0.6.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Graham Batty
|
@@ -14,21 +14,21 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-03-
|
18
|
-
default_executable:
|
17
|
+
date: 2010-03-19 00:00:00 -06:00
|
18
|
+
default_executable: bridge-store-key
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: jaws
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- - "
|
25
|
+
- - "="
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
segments:
|
28
28
|
- 0
|
29
|
-
-
|
29
|
+
- 6
|
30
30
|
- 0
|
31
|
-
version: 0.
|
31
|
+
version: 0.6.0
|
32
32
|
type: :runtime
|
33
33
|
version_requirements: *id001
|
34
34
|
- !ruby/object:Gem::Dependency
|
@@ -47,8 +47,8 @@ dependencies:
|
|
47
47
|
version_requirements: *id002
|
48
48
|
description: Rack handler and other tools for communicating with a BRIDGE-capable server (like CloudBridge)
|
49
49
|
email: graham@stormbrew.ca
|
50
|
-
executables:
|
51
|
-
|
50
|
+
executables:
|
51
|
+
- bridge-store-key
|
52
52
|
extensions: []
|
53
53
|
|
54
54
|
extra_rdoc_files:
|
@@ -61,6 +61,11 @@ files:
|
|
61
61
|
- README.rdoc
|
62
62
|
- Rakefile
|
63
63
|
- VERSION
|
64
|
+
- bin/bridge-store-key
|
65
|
+
- lib/bridge/key_tools.rb
|
66
|
+
- lib/bridge/server.rb
|
67
|
+
- lib/bridge/tcp_server.rb
|
68
|
+
- lib/rack/handler/bridge.rb
|
64
69
|
- spec/rack-bridge_spec.rb
|
65
70
|
- spec/spec.opts
|
66
71
|
- spec/spec_helper.rb
|