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 CHANGED
@@ -1,6 +1,35 @@
1
1
  = rack-bridge
2
2
 
3
- Description goes here.
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", ">= 0.5.0"
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.5.1
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
@@ -0,0 +1,11 @@
1
+ require 'bridge/server'
2
+
3
+ module Rack
4
+ module Handler
5
+ class Bridge
6
+ def self.run(app, options = Bridge::Server::DefaultOptions)
7
+ ::Bridge::Server.new(options).run(app)
8
+ end
9
+ end
10
+ end
11
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
8
- - 1
9
- version: 0.5.1
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-17 00:00:00 -06:00
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
- - 5
29
+ - 6
30
30
  - 0
31
- version: 0.5.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