rack-bridge 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|