hookout 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+