localhook 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8ab14906758ff1242dd8322ec1fb39f7f17d9101
4
+ data.tar.gz: 50e268e4b5d8c5239270fdb32b976577e6c8dd98
5
+ SHA512:
6
+ metadata.gz: 4333fd6974a59eba698b78d5d97e1fc4c2b3486f4dca0fb8fad13b7f79f4b49b9820b6ac45114e40e5d9b5b0c7e29a120f43998491d0f8e4ac50b203ef8349a3
7
+ data.tar.gz: c2d435287c5652fb9814fea89aa6ce6d0911356b995538334745981710145856bf3b5458fb92573e5718221a13ec0ef278a5363c74472219fe8295a46d28f5db
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in localhook.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Francis Chong
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Localhook
2
+
3
+ ## What is Localhook?
4
+
5
+ Localhook let you receive webhooks behind a firewall.
6
+
7
+ A WebHook is an HTTP callback: an HTTP POST that occurs when something happens. Many popular services (GitHub, Stripe, ActiveCampaign, Papertrail, etc) support updates via webhooks. However, since these webhook requests are made over Internet, it's difficult receive them when testing from behind a firewall.
8
+
9
+ Localhook lets you host a public endpoint for other services and tunnels requests to a private endpoint on your computer.
10
+
11
+ ## Installation
12
+
13
+ Install localhook client:
14
+
15
+ ``
16
+ gem install localhook
17
+ ``
18
+
19
+ ## Usage
20
+
21
+ First, you must host your own localhook server on internet. Check [localhook-server](https://github.com/siuying/localhook-server) for details.
22
+
23
+ To expose a local webhook ``http://localhost:3000/webhook`` to internet:
24
+
25
+ ```
26
+ localhook https://localhook.mydomain.com http://localhost:3000 --token=1234
27
+ ```
28
+
29
+ Instead of giving third party url "http://localhost:3000/webhook", you give them
30
+ ``https://localhook.mydomain.com/endpoint1/webhook```.
31
+
32
+ Any POST request sent to ``https://localhook.mydomain.com/endpoint1/webhook`` will be
33
+ forwarded to ``http://localhost:3000/webhook``.
34
+
35
+ ## Contributing
36
+
37
+ 1. Fork it ( http://github.com/<my-github-username>/localhook/fork )
38
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
39
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
40
+ 4. Push to the branch (`git push origin my-new-feature`)
41
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/localhook ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'localhook'
3
+
4
+ Localhook::Command.run(ARGV)
data/lib/localhook.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Localhook
2
+ end
3
+
4
+ require_relative "./localhook/version"
5
+ require_relative './localhook/command'
@@ -0,0 +1,51 @@
1
+ require 'claide'
2
+ require 'colored'
3
+
4
+ require_relative './event_source'
5
+ require_relative './webhook_forwarder'
6
+
7
+ module Localhook
8
+ class Command < ::CLAide::Command
9
+ attr_reader :remote_server, :local_server
10
+ self.command = 'localhook'
11
+ self.description = "Localhook let you receive webhooks on localhost."
12
+ self.arguments = "<remote-server> <local-server>"
13
+
14
+ def initialize(argv)
15
+ @remote_server = argv.shift_argument
16
+ @local_server = argv.shift_argument
17
+ @token = argv.option('token')
18
+ super
19
+ end
20
+
21
+ def self.options
22
+ [
23
+ ['--token=<token>', 'Authentication Token']
24
+ ].concat(super)
25
+ end
26
+
27
+ def validate!
28
+ if @remote_server.nil?
29
+ help! "missing remote-server parameter"
30
+ end
31
+ if @local_server.nil?
32
+ help! "missing local-server parameter"
33
+ end
34
+ if @token.nil?
35
+ help! "missing token"
36
+ end
37
+ end
38
+
39
+ def run
40
+ EventMachine.run do
41
+ puts "forward remote server (#{@remote_server}) webhooks to #{@local_server}"
42
+ @forwarder = WebhookForwarder.new(@local_server)
43
+ @source = EventSource.new(@remote_server, @forwarder, @token)
44
+ @source.error do |error|
45
+ puts "error connecting to eventsource: #{error}"
46
+ end
47
+ @source.start
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ require 'em-eventsource'
2
+ require 'yajl'
3
+
4
+ module Localhook
5
+ class EventSource < ::EventMachine::EventSource
6
+ attr_reader :forwarder
7
+ attr_reader :endpoint
8
+
9
+ VALID_URL = %r{^(https?://[^/]+)/?$}
10
+
11
+ def initialize(base_url, forwarder, token)
12
+ unless base_url =~ VALID_URL
13
+ raise ArgumentError, "Invalid base_url \"#{base_url}\", it should be in format: (https|http)://<host>(:<port>)?/?"
14
+ end
15
+
16
+ base_url = base_url.match(VALID_URL)[1]
17
+ super("#{base_url}/_localhook?token=#{token}")
18
+
19
+ @forwarder = forwarder
20
+
21
+ @parser = Yajl::Parser.new(:symbolize_keys => true)
22
+ @parser.on_parse_complete = method(:data_parsed)
23
+ self.on "webhook" do |message|
24
+ @parser.parse(message)
25
+ end
26
+ self.on "endpoint" do |message|
27
+ @endpoint = message
28
+ puts "Connected to endpoint: #{message}"
29
+ end
30
+ self.on "error" do |error|
31
+ puts "Error: #{error}"
32
+ self.close
33
+ raise "Server response with an error: #{error}"
34
+ end
35
+
36
+ # never timeout
37
+ self.inactivity_timeout = 0
38
+ end
39
+
40
+ def data_parsed(data)
41
+ # binding.pry
42
+ case data[:action]
43
+ when "post"
44
+ # convert headers array to Hash, if needed
45
+ headers = data[:headers]
46
+ headers = headers.inject({}){|map, v| map[v[0]] = v[1]; map } if headers.is_a?(Array)
47
+ path = data[:path]
48
+ query = data[:query_string]
49
+ body = data[:body]
50
+ forwarder.post(path, query, headers, body)
51
+ else
52
+ raise "unknown action '#{data[:action]}' (#{data}"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module Localhook
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'em-http-request'
2
+
3
+ module Localhook
4
+ # Forward webhook to destination.
5
+ # Note: this should be used inside eventmachine
6
+ class WebhookForwarder
7
+ attr_reader :url
8
+ attr_reader :http_options
9
+
10
+ VALID_URL = %r{^(https?://[^/]+)/?$}
11
+ DEFAULT_OPTIONS = {:connect_timeout => 5}
12
+
13
+ # Create a WebhookForwarder
14
+ # url - the base url of the destination webhook
15
+ # http_options - the http options passed to em
16
+ def initialize(url, http_options={})
17
+ unless url =~ VALID_URL
18
+ raise ArgumentError, "Invalid url \"#{url}\", it should be in format: (https|http)://<host>(:<port>)?/?"
19
+ end
20
+
21
+ @url = url.match(VALID_URL)[1]
22
+ @http_options = DEFAULT_OPTIONS.merge(http_options)
23
+ end
24
+
25
+ # path - string, the path of the request
26
+ # query - string, the query string
27
+ # headers - Hash, the header of the request
28
+ # body - String, the body of the request
29
+ def post(path, query, headers, body)
30
+ http = EventMachine::HttpRequest.new(url, http_options).post :head => headers, :path => path, :query => query, :body => body
31
+ http.callback { puts "forward url -> #{url}" }
32
+ http.errback { puts "ERROR: failed forward to url #{url}" }
33
+ end
34
+ end
35
+ end
data/localhook.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'localhook/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "localhook"
8
+ spec.version = Localhook::VERSION
9
+ spec.authors = ["Francis Chong"]
10
+ spec.email = ["francis@ignition.hk"]
11
+ spec.summary = %q{Localhook let you receive webhooks on localhost.}
12
+ spec.description = %q{Localhook makes it super easy to connect public webhook endpoints with development environments.}
13
+ spec.homepage = "https://github.com/siuying/localhook"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "claide"
22
+ spec.add_dependency "em-http-request"
23
+ spec.add_dependency "em-eventsource"
24
+ spec.add_dependency "yajl-ruby"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.5"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "pry"
29
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../lib/localhook'
2
+
3
+ describe Localhook::EventSource do
4
+ context "-initialize" do
5
+ it "accept parameters" do
6
+ argv = CLAide::ARGV.new(['http://mylocalhook.com', 'http://localhost:3000'])
7
+ command = Localhook::Command.new(argv)
8
+
9
+ expect(command.remote_server).to eq("http://mylocalhook.com")
10
+ expect(command.local_server).to eq("http://localhost:3000")
11
+ end
12
+ end
13
+
14
+ context "-run" do
15
+ it "raise error on missing parameters" do
16
+ argv = CLAide::ARGV.new(['http://mylocalhook.com'])
17
+ command = Localhook::Command.new(argv)
18
+ expect {
19
+ command.run
20
+ }.to raise_error
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ require_relative '../lib/localhook'
2
+ require 'yajl'
3
+
4
+ describe Localhook::EventSource do
5
+ context "-initialize" do
6
+ it "set url via base_url and token parameter" do
7
+ forwarder = Localhook::EventSource.new("http://localhost:8080", double(:forwarder), "t")
8
+ expect(forwarder.url).to eq("http://localhost:8080/_localhook?token=t")
9
+
10
+ forwarder = Localhook::EventSource.new("http://localhost:8080/", double(:forwarder), "t")
11
+ expect(forwarder.url).to eq("http://localhost:8080/_localhook?token=t")
12
+ end
13
+ end
14
+
15
+ context "-data_parsed" do
16
+ let(:forwarder) { double(:forwarder) }
17
+ subject { Localhook::EventSource.new("http://www.google.com", forwarder, "1") }
18
+
19
+ it "call forwarder.post() when parsed action 'post'" do
20
+ message = {:action => "post",
21
+ :query_string => "a=1",
22
+ :path => "/a/b",
23
+ :body => "{}",
24
+ :headers => [["User-Agent", "rspec"]]
25
+ }
26
+
27
+ expect(forwarder).to receive(:post).with("/a/b", "a=1", {"User-Agent" => "rspec"}, "{}")
28
+ subject.data_parsed(message)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ require_relative '../lib/localhook'
2
+ require 'yajl'
3
+
4
+ describe Localhook::WebhookForwarder do
5
+ context "initialize" do
6
+ it "accept valid url" do
7
+ forwarder = Localhook::WebhookForwarder.new("http://localhost:8080")
8
+ expect(forwarder.url).to eq("http://localhost:8080")
9
+
10
+ forwarder = Localhook::WebhookForwarder.new("http://localhost:8080/")
11
+ expect(forwarder.url).to eq("http://localhost:8080")
12
+ end
13
+
14
+ it "merge input options with default options" do
15
+ options = {a: 1}
16
+ forwarder = Localhook::WebhookForwarder.new("http://localhost:8080", options)
17
+ expect(forwarder.http_options).to eq(Localhook::WebhookForwarder::DEFAULT_OPTIONS.merge(options))
18
+ end
19
+
20
+ it "raise exception for invalid url" do
21
+ expect {
22
+ Localhook::WebhookForwarder.new("http://localhost:8080/a/b")
23
+ }.to raise_error(ArgumentError)
24
+ end
25
+ end
26
+
27
+ context "post" do
28
+ subject { Localhook::WebhookForwarder.new("http://localhost:8080") }
29
+ it "forward request to remote server via em" do
30
+ params = {head: {}, path: "/a/b", query: "", body: ""}
31
+ request = double(:request)
32
+ expect(request).to receive(:post).with(params).and_return(request)
33
+ expect(request).to receive(:callback)
34
+ expect(request).to receive(:errback)
35
+ expect(EventMachine::HttpRequest).to receive(:new).with(subject.url, subject.http_options).and_return(request)
36
+ subject.post("/a/b", "", {}, "")
37
+ end
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: localhook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Francis Chong
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: claide
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: em-http-request
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: em-eventsource
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yajl-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Localhook makes it super easy to connect public webhook endpoints with
112
+ development environments.
113
+ email:
114
+ - francis@ignition.hk
115
+ executables:
116
+ - localhook
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".gitignore"
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - bin/localhook
126
+ - lib/localhook.rb
127
+ - lib/localhook/command.rb
128
+ - lib/localhook/event_source.rb
129
+ - lib/localhook/version.rb
130
+ - lib/localhook/webhook_forwarder.rb
131
+ - localhook.gemspec
132
+ - spec/command_spec.rb
133
+ - spec/event_source_spec.rb
134
+ - spec/webbook_forwarder_spec.rb
135
+ homepage: https://github.com/siuying/localhook
136
+ licenses:
137
+ - MIT
138
+ metadata: {}
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 2.2.0
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: Localhook let you receive webhooks on localhost.
159
+ test_files:
160
+ - spec/command_spec.rb
161
+ - spec/event_source_spec.rb
162
+ - spec/webbook_forwarder_spec.rb