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 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