hookout 0.1.1

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.
@@ -0,0 +1,98 @@
1
+ = Hookout - Reverse HTTP Webhooks
2
+ Developing webhook based applications from behind a firewall or NAT provides many challenges. The
3
+ biggest is allowing external applications to see your hook. Hookout allows you to use a
4
+ ReverseHTTP (http://www.reversehttp.net) server to expose your application to the outside world, without needing
5
+ to configure any complicated firewall holes.
6
+
7
+ == Getting Started
8
+ === Installing
9
+ Hookout can be installed via RubyGems with:
10
+ gem sources -a http://gems.github.com
11
+ gem install paulj-hookout
12
+
13
+ It can also be installed manually with:
14
+ git clone git://github.com/paulj/hookout.git
15
+ cd hookout
16
+ rake build install
17
+
18
+ === Running an application via the hookout helper
19
+ Hookout provides a Rack (http://rack.rubyforge.org) adapter, allowing any standard Rack application to
20
+ be made available. It also bundles a script called hookout that will start up an instance of Thin using
21
+ ReverseHTTP as a backend. This section will cover running a simple Sinatra application with the hookout helper.
22
+
23
+ Firstly, say we have a simple Sinatra application (called simple-app.rb):
24
+ require 'rubygems'
25
+ require 'sinatra'
26
+
27
+ get '/' do
28
+ "Hello World"
29
+ end
30
+
31
+ To run with the hookout adapter, you'll need a simple rackup configuration file. Create a config.ru such as:
32
+ require 'simple-app'
33
+
34
+ set :run, false
35
+
36
+ run Sinatra::Application
37
+
38
+ From the command line, you can now start this application with a command line such as:
39
+ hookout -a http://www.reversehttp.net/reversehttp -n simple-ruby-app -R config.ru start
40
+ Thin should boot, and provide output such as:
41
+ >> Thin web server (v1.0.0 codename That's What She Said)
42
+ >> Maximum connections set to 1024
43
+ >> Listening on simple-ruby-app via http://www.reversehttp.net/reversehttp, CTRL+C to stop
44
+ Bound to location http://simple-ruby-app.www.reversehttp.net/
45
+
46
+ You can now visit http://simple-ruby-app.www.reversehttp.net to see you application.
47
+
48
+ === Configuring Sinatra to use Hookout as the default server
49
+ To make a Sintatra application run Hookout instead of thin, a simple script such as the following can be used:
50
+ require 'rubygems'
51
+ require 'sinatra'
52
+ require 'hookout'
53
+
54
+ set :server, 'hookout'
55
+ set :host, 'http://www.reversehttp.net/reversehttp'
56
+ set :port, 'standalone-ruby-app'
57
+
58
+ get '/' do
59
+ "Hello World"
60
+ end
61
+
62
+ This instructs Sinatra to start Hookout as the Rack adapter; and informs hookout that it should use
63
+ the public http://www.reversehttp.net server; and informs hookout that it should request the
64
+ "standalone-ruby-app" space on that server for this application. (Note: To prevent problems, it is HIGHLY
65
+ recommended to change this to something more unique before trying this!).
66
+
67
+ You can now run the application with:
68
+ ruby test-app.rb
69
+
70
+ You should see it startup with something like:
71
+ == Sinatra/0.9.2 has taken the stage on test-ruby-app for development with backup from Hookout
72
+ Location changed to http://standalone-ruby-app.www.reversehttp.net/
73
+
74
+ You can now visit http://standalone-ruby-app.www.reversehttp.net, and see you own Sinatra app serving pages!
75
+
76
+ == Software License
77
+ Copyright (c) 2008, 2009 Paul Jones <paulj@lshift.net>
78
+ Copyright (c) 2008, 2009 LShift Ltd. <query@lshift.net>
79
+
80
+ Permission is hereby granted, free of charge, to any person
81
+ obtaining a copy of this software and associated documentation
82
+ files (the "Software"), to deal in the Software without
83
+ restriction, including without limitation the rights to use, copy,
84
+ modify, merge, publish, distribute, sublicense, and/or sell copies
85
+ of the Software, and to permit persons to whom the Software is
86
+ furnished to do so, subject to the following conditions:
87
+
88
+ The above copyright notice and this permission notice shall be
89
+ included in all copies or substantial portions of the Software.
90
+
91
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
92
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
93
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
94
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
95
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
96
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
97
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
98
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/clean'
4
+ require 'rake/rdoctask'
5
+ require 'rubygems/specification'
6
+
7
+ desc "Run all specs"
8
+ Spec::Rake::SpecTask.new('spec') do |t|
9
+ t.spec_opts = ["-cfs"]
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ t.libs = ['lib']
12
+ end
13
+
14
+ desc "Print specdocs"
15
+ Spec::Rake::SpecTask.new(:doc) do |t|
16
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
17
+ t.spec_files = FileList['spec/*_spec.rb']
18
+ end
19
+
20
+ desc "Generate RCov code coverage report"
21
+ Spec::Rake::SpecTask.new('rcov') do |t|
22
+ t.spec_files = FileList['spec/*_spec.rb']
23
+ t.rcov = true
24
+ t.rcov_opts = ['--exclude', 'examples']
25
+ end
26
+
27
+ def gemspec
28
+ @gemspec ||= begin
29
+ file = File.expand_path('../hookout.gemspec', __FILE__)
30
+ eval(File.read(file), binding, file)
31
+ end
32
+ end
33
+
34
+ begin
35
+ require 'rake/gempackagetask'
36
+ rescue LoadError
37
+ task(:gem) { $stderr.puts '`gem install rake` to package gems' }
38
+ else
39
+ Rake::GemPackageTask.new(gemspec) do |pkg|
40
+ pkg.gem_spec = gemspec
41
+ end
42
+ task :gem => :gemspec
43
+ end
44
+
45
+ desc "install the gem locally"
46
+ task :install => :package do
47
+ sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}}
48
+ end
49
+
50
+ desc "validate the gemspec"
51
+ task :gemspec do
52
+ gemspec.validate
53
+ end
54
+
55
+ task :package => :gemspec
56
+
57
+
58
+ Rake::RDocTask.new do |t|
59
+ t.rdoc_dir = 'rdoc'
60
+ t.title = "Hookout"
61
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
62
+ t.options << '--charset' << 'utf-8'
63
+ t.rdoc_files.include('README.rdoc')
64
+ t.rdoc_files.include('lib/hookout/*.rb')
65
+ end
66
+
67
+ CLEAN.include [ 'build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log', 'pkg', 'lib/*.bundle', '*.gem', '.config' ]
68
+
69
+ task :test => [ :spec ]
70
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 1
5
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # Hookout command line interface script.
3
+ # Run <tt>hookout -h</tt> to get more usage.
4
+ require 'rubygems'
5
+ require File.dirname(__FILE__) + '/../lib/hookout'
6
+
7
+ Hookout::Runner.new(ARGV).run!
@@ -0,0 +1,7 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'hookout/reversehttp_connector'
4
+ require 'hookout/rack_adapter'
5
+ require 'hookout/runner'
6
+ require 'hookout/thin_backend'
7
+ require 'rack/handler/hookout'
@@ -0,0 +1,25 @@
1
+ require 'thin'
2
+
3
+ module Hookout
4
+ class RackAdapter
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def handle_request(request)
10
+ thin_request = Thin::Request.new
11
+ thin_request.parse request.body
12
+
13
+ status,headers,body = @app.call(thin_request.env)
14
+
15
+ thin_response = Thin::Response.new
16
+ thin_response.status = status
17
+ thin_response.headers = headers
18
+ thin_response.body = body
19
+
20
+ thin_response.each do |chunk|
21
+ request.write(chunk)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,228 @@
1
+ require 'net/http'
2
+ require 'cgi'
3
+
4
+ module Hookout
5
+ class ReverseHttpConnector
6
+ DEFAULT_SERVER = 'http://localhost:8000/reversehttp'
7
+ DEFAULT_LABEL = 'ruby'
8
+
9
+ attr_accessor :failure_delay, :lease_seconds, :report_poll_exceptions, :location_change_callback
10
+
11
+ def initialize(label, server_address, handler)
12
+ @label = label
13
+ @server_address = server_address
14
+ @handler = handler
15
+
16
+ @next_req = nil
17
+ @location = nil
18
+ @failure_delay = 2
19
+ @token = '-'
20
+ @lease_seconds = 30
21
+ @report_poll_exceptions = false
22
+ @location_change_callback = nil
23
+ @closed = false
24
+ @requestor = Hookout::Utils.new
25
+ end
26
+
27
+ def start
28
+ until @closed
29
+ begin
30
+ (request, client) = next_request()
31
+ unless request.nil?
32
+ @handler.handle_request(request)
33
+ request.close
34
+ end
35
+ rescue
36
+ puts $!
37
+ puts $@
38
+ end
39
+ end
40
+ end
41
+
42
+ def stop
43
+ @closed = true
44
+ @requestor.abort
45
+ end
46
+
47
+ def next_request
48
+ until @closed
49
+ declare_mode = (@next_req == nil)
50
+ begin
51
+ response = nil
52
+ if declare_mode
53
+ response = @requestor.post_form @server_address, {:name => @label, :token => @token}
54
+ else
55
+ response = @requestor.get @next_req
56
+ end
57
+
58
+ return nil if response.nil?
59
+
60
+ @failure_delay = 2
61
+ if declare_mode
62
+ link_headers = parse_link_headers(response)
63
+
64
+ @next_req = link_headers['first']
65
+ location_text = link_headers['related']
66
+ if location_text
67
+ @location = location_text
68
+ on_location_changed()
69
+ end
70
+ elsif response['Requesting-Client']
71
+ client_addr = response['Requesting-Client'].split(":")
72
+ this_req = @next_req
73
+ @next_req = parse_link_headers(response)['next']
74
+ return [ReverseHttpRequest.new(this_req, @server_address, response.body), client_addr]
75
+ end
76
+ rescue
77
+ if @report_poll_exceptions
78
+ report_poll_exception
79
+ end
80
+ sleep(@failure_delay) unless @closed
81
+
82
+ if @failure_delay < 30
83
+ @failure_delay = @failure_delay * 2
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+ def urlencode(hash)
91
+ hash.each { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }.join("&")
92
+ end
93
+
94
+ def parse_link_headers(resp)
95
+ result = {}
96
+ resp['Link'].split(", ").each do |link_header|
97
+ url, rel = nil, nil
98
+
99
+ link_header.split(";").each do |piece|
100
+ piece = piece.strip
101
+
102
+ if piece[0..0] == '<':
103
+ url = piece[1..-2]
104
+ elsif piece[0..4].downcase == 'rel="':
105
+ rel = piece[5..-2]
106
+ end
107
+ end
108
+
109
+ if url and rel
110
+ result[rel] = url
111
+ end
112
+ end
113
+
114
+ result
115
+ end
116
+
117
+ def handle_error(request, client_address)
118
+ if not request.closed:
119
+ begin
120
+ request.write("HTTP/1.0 500 Internal Server Error\r\n\r\n")
121
+ request.close()
122
+ rescue
123
+ end
124
+ end
125
+ end
126
+
127
+ def report_poll_exception
128
+ puts $!
129
+ puts $@
130
+ end
131
+
132
+ def on_location_changed
133
+ if @location_change_callback
134
+ @location_change_callback.call(@location)
135
+ end
136
+ end
137
+ end
138
+
139
+ class ReverseHttpRequest
140
+ attr_reader :body, :server_address
141
+
142
+ def initialize(reply_url, server_address, body)
143
+ @reply_url = reply_url
144
+ @server_address = server_address
145
+ @body = body
146
+ @response_buffer = StringIO.new
147
+ @closed = false
148
+ end
149
+
150
+ def makefile(mode, bufsize)
151
+ if mode[0] == 'r':
152
+ StringIO.new(@body)
153
+ elsif mode[0] == 'w':
154
+ self
155
+ end
156
+ end
157
+
158
+ def write(x)
159
+ @response_buffer.write(x)
160
+ end
161
+
162
+ def flush
163
+ end
164
+
165
+ def close
166
+ if not @closed
167
+ @response_buffer.flush
168
+ resp_body = @response_buffer.string
169
+
170
+ Hookout::Utils.post_data @reply_url, resp_body
171
+ @closed = true
172
+ end
173
+ end
174
+ end
175
+
176
+ class Utils
177
+ def initialize
178
+ @current_http = nil
179
+ @current_thread = nil
180
+ end
181
+
182
+ def get(url)
183
+ parts = URI.parse(url)
184
+ req = Net::HTTP::Get.new(parts.path)
185
+ execute_request(parts, req)
186
+ end
187
+
188
+ def post_form(url, params)
189
+ parts = URI.parse(url)
190
+ req = Net::HTTP::Post.new(parts.path)
191
+ req.set_form_data(params)
192
+ execute_request(parts, req)
193
+ end
194
+
195
+ def abort
196
+ @current_http.finish if @current_http
197
+ @current_thread.raise ConnectorAbortException.new if @current_thread
198
+ end
199
+
200
+ def execute_request(parts, req)
201
+ begin
202
+ Net::HTTP.start(parts.host, parts.port) {|http|
203
+ @current_http = http
204
+ @current_thread = Thread.current
205
+ http.request(req)
206
+ }
207
+ rescue ConnectorAbortException
208
+ return nil
209
+ ensure
210
+ @current_http = nil
211
+ @current_thread = nil
212
+ end
213
+ end
214
+
215
+ def self.post_data(url, data)
216
+ parts = URI.parse(url)
217
+ req = Net::HTTP::Post.new(parts.path)
218
+ req.body = data
219
+ req.content_type = 'message/http'
220
+ Net::HTTP.start(parts.host, parts.port) {|http|
221
+ http.request(req)
222
+ }
223
+ end
224
+
225
+ class ConnectorAbortException < Exception
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,92 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ module Hookout
5
+ # Hookout overrides of the Hookout runner
6
+ class Runner < Thin::Runner
7
+ def parser
8
+ # Load in Hookup specific defaults
9
+ # Default options values
10
+ @options[:address] = ReverseHttpConnector::DEFAULT_SERVER
11
+ @options[:label] = ReverseHttpConnector::DEFAULT_LABEL
12
+ @options[:log] = 'log/hookout.log'
13
+ @options[:pid] = 'tmp/pids/hookout.pid'
14
+ @options[:backend] =Hookout::ThinBackend.name
15
+
16
+ # NOTE: If you add an option here make sure the key in the +options+ hash is the
17
+ # same as the name of the command line option.
18
+ # +option+ keys are used to build the command line to launch other processes,
19
+ # see <tt>lib/thin/command.rb</tt>.
20
+ @parser ||= OptionParser.new do |opts|
21
+ opts.banner = "Usage: hookout [options] #{self.class.commands.join('|')}"
22
+
23
+ opts.separator ""
24
+ opts.separator "Server options:"
25
+
26
+ opts.on("-a", "--address SERVER", "use SERVER for Reverse HTTP bindings " +
27
+ "(default: #{@options[:address]})") { |host| @options[:address] = host }
28
+ opts.on("-n", "--label LABEL", "use LABEL (default: #{@options[:label]})") { |label| @options[:label] = label }
29
+ opts.on("-A", "--adapter NAME", "Rack adapter to use (default: autodetect)",
30
+ "(#{Rack::ADAPTERS.map{|(a,b)|a}.join(', ')})") { |name| @options[:adapter] = name }
31
+ opts.on("-R", "--rackup FILE", "Load a Rack config file instead of " +
32
+ "Rack adapter") { |file| @options[:rackup] = file }
33
+ opts.on("-c", "--chdir DIR", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
34
+ opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
35
+
36
+ opts.separator ""
37
+ opts.separator "Adapter options:"
38
+ opts.on("-e", "--environment ENV", "Framework environment " +
39
+ "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
40
+ opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
41
+
42
+ unless Thin.win? # Daemonizing not supported on Windows
43
+ opts.separator ""
44
+ opts.separator "Daemon options:"
45
+
46
+ opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true }
47
+ opts.on("-l", "--log FILE", "File to redirect output " +
48
+ "(default: #{@options[:log]})") { |file| @options[:log] = file }
49
+ opts.on("-P", "--pid FILE", "File to store PID " +
50
+ "(default: #{@options[:pid]})") { |file| @options[:pid] = file }
51
+ opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| @options[:user] = user }
52
+ opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| @options[:group] = group }
53
+
54
+ opts.separator ""
55
+ opts.separator "Cluster options:"
56
+
57
+ opts.on("-s", "--servers NUM", "Number of servers to start") { |num| @options[:servers] = num.to_i }
58
+ opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| @options[:only] = only }
59
+ opts.on("-C", "--config FILE", "Load options from config file") { |file| @options[:config] = file }
60
+ opts.on( "--all [DIR]", "Send command to each config files in DIR") { |dir| @options[:all] = dir } if Thin.linux?
61
+ end
62
+
63
+ opts.separator ""
64
+ opts.separator "Tuning options:"
65
+
66
+ opts.on("-b", "--backend CLASS", "Backend to use (ignored)") { |name| }
67
+ opts.on("-p", "--port PORT", "Port to use (ignored)") { |name| }
68
+ opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
69
+ "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
70
+ opts.on("-f", "--force", "Force the execution of the command") { @options[:force] = true }
71
+ opts.on( "--max-conns NUM", "Maximum number of connections " +
72
+ "(default: #{@options[:max_conns]})",
73
+ "Might require sudo to set higher then 1024") { |num| @options[:max_conns] = num.to_i } unless Thin.win?
74
+ opts.on( "--max-persistent-conns NUM",
75
+ "Maximum number of persistent connections",
76
+ "(default: #{@options[:max_persistent_conns]})") { |num| @options[:max_persistent_conns] = num.to_i }
77
+ opts.on( "--threaded", "Call the Rack application in threads " +
78
+ "[experimental]") { @options[:threaded] = true }
79
+ opts.on( "--no-epoll", "Disable the use of epoll") { @options[:no_epoll] = true } if Thin.linux?
80
+
81
+ opts.separator ""
82
+ opts.separator "Common options:"
83
+
84
+ opts.on_tail("-r", "--require FILE", "require the library") { |file| @options[:require] << file }
85
+ opts.on_tail("-D", "--debug", "Set debbuging on") { @options[:debug] = true }
86
+ opts.on_tail("-V", "--trace", "Set tracing on (log raw request/response)") { @options[:trace] = true }
87
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
88
+ opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,68 @@
1
+ module Hookout
2
+ # Backend allowing Thin to act as a Reverse HTTP server
3
+ class ThinBackend < Thin::Backends::Base
4
+ # Address and port on which the server is listening for connections.
5
+ attr_accessor :server_address, :label
6
+
7
+ def initialize(server, label, options)
8
+ @server_address = options[:address]
9
+ @label = options[:label]
10
+
11
+ super()
12
+ end
13
+
14
+ # Connect the server
15
+ def connect
16
+ @connector = ReverseHttpConnector.new(@label, @server_address, self)
17
+ @connector.report_poll_exceptions = true
18
+ @connector.location_change_callback = lambda { |l| puts "Bound to location #{l}" }
19
+
20
+ EventMachine.defer do
21
+ @connector.start
22
+ end
23
+ end
24
+
25
+ # Stops the server
26
+ def disconnect
27
+ @connector.stop
28
+ end
29
+
30
+ def to_s
31
+ "#{@label} via #{@server_address}"
32
+ end
33
+
34
+ def handle_request(request)
35
+ connection = ThinConnection.new(request.to_s)
36
+ connection.rhttp_req = request
37
+ connection.backend = self
38
+ initialize_connection(connection)
39
+
40
+ connection.receive_data(request.body)
41
+ end
42
+ end
43
+
44
+ class ThinConnection < Thin::Connection
45
+ attr_accessor :rhttp_req
46
+ attr_accessor :backend
47
+
48
+ def persistent?
49
+ false
50
+ end
51
+
52
+ def comm_inactivity_timeout=(timeout)
53
+ # Ignore
54
+ end
55
+
56
+ def send_data(data)
57
+ @rhttp_req.write(data)
58
+ end
59
+
60
+ def close_connection_after_writing
61
+ begin
62
+ @rhttp_req.close
63
+ ensure
64
+ @backend.connection_finished(self)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,32 @@
1
+ module Rack
2
+ module Handler
3
+ # Rack Handler stricly to be able to use Hookout through the rackup command.
4
+ # To do so, simply require 'hookout' in your Rack config file and run like this
5
+ #
6
+ # rackup --server hookout
7
+ #
8
+ class Hookout
9
+ def self.run(app, options={})
10
+ # Determine our host
11
+ host = options[:Host] || 'http://localhost:8000/reversehttp'
12
+ host = 'http://localhost:8000/reversehttp' if host[0..3].downcase != 'http'
13
+
14
+ # Determine our label
15
+ label = options[:Port] || 'ruby'
16
+ label = 'ruby' if label.to_i != 0
17
+
18
+ server = ::Hookout::ReverseHttpConnector.new(
19
+ label,
20
+ host,
21
+ ::Hookout::RackAdapter.new(app))
22
+ server.report_poll_exceptions = options[:report_poll_exceptions] || true # false
23
+ server.location_change_callback = lambda {|l| puts "Location changed to #{l}"} # if options[:log_location_change]
24
+
25
+ yield server if block_given?
26
+ server.start
27
+ end
28
+ end
29
+
30
+ register 'hookout', 'Rack::Handler::Hookout'
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hookout
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Paul Jones
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-10 00:00:00 +01:00
18
+ default_executable: hookout
19
+ dependencies: []
20
+
21
+ description: Hookout allows you to expose your web hook applications to the web via Reverse HTTP.
22
+ email: pauljones23@gmail.com
23
+ executables:
24
+ - hookout
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - bin/hookout
31
+ - lib/hookout/rack_adapter.rb
32
+ - lib/hookout/reversehttp_connector.rb
33
+ - lib/hookout/runner.rb
34
+ - lib/hookout/thin_backend.rb
35
+ - lib/hookout.rb
36
+ - lib/rack/handler/hookout.rb
37
+ - Rakefile
38
+ - README.rdoc
39
+ - VERSION.yml
40
+ has_rdoc: true
41
+ homepage: http://github.com/paulj/hookout/
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --inline-source
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.6
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: Hookout Reverse HTTP Connector
71
+ test_files: []
72
+