├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── app.rb ├── config.ru ├── public └── script │ └── app.js └── views └── index.haml /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.3.1' 3 | 4 | gem 'sinatra', '~> 1.4.7' 5 | gem 'sinatra-websocket', '~> 0.3.1' 6 | gem 'haml', '~> 4.0.7' 7 | gem 'foreman', '~> 0.81.0' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.7) 5 | daemons (1.1.9) 6 | em-websocket (0.3.8) 7 | addressable (>= 2.1.1) 8 | eventmachine (>= 0.12.9) 9 | eventmachine (1.0.7) 10 | foreman (0.81.0) 11 | thor (~> 0.19.1) 12 | haml (4.0.7) 13 | tilt 14 | rack (1.6.0) 15 | rack-protection (1.5.3) 16 | rack 17 | sinatra (1.4.7) 18 | rack (~> 1.5) 19 | rack-protection (~> 1.4) 20 | tilt (>= 1.3, < 3) 21 | sinatra-websocket (0.3.1) 22 | em-websocket (~> 0.3.6) 23 | eventmachine 24 | thin (>= 1.3.1, < 2.0.0) 25 | thin (1.6.3) 26 | daemons (~> 1.0, >= 1.0.9) 27 | eventmachine (~> 1.0) 28 | rack (~> 1.0) 29 | thor (0.19.1) 30 | tilt (2.0.2) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | foreman (~> 0.81.0) 37 | haml (~> 4.0.7) 38 | sinatra (~> 1.4.7) 39 | sinatra-websocket (~> 0.3.1) 40 | 41 | RUBY VERSION 42 | ruby 2.3.1p112 43 | 44 | BUNDLED WITH 45 | 1.12.3 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Factor.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | service: bundle exec rackup -p $PORT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocketHook.io 2 | [![Code Climate](https://codeclimate.com/github/factor-io/websockethook.png)](https://codeclimate.com/github/factor-io/websockethook) 3 | [![Dependency Status](https://gemnasium.com/factor-io/websockethook.svg)](https://gemnasium.com/factor-io/websockethook) 4 | 5 | A simple web service to receive web hooks over a web socket. 6 | 7 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 8 | 9 | This is a Sinatra based web service. When you run it, you can connect to it using any standard Web Socket client, and register a web hook. When you perform a POST on that newly created web hook, you will receive a message via the web socket. 10 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websockethook", 3 | "description": "A simple web service to receive web hooks over a web socket.", 4 | "keywords": ["hook", "websocket"], 5 | "addons": [], 6 | "env": {} 7 | } 8 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra-websocket' 3 | require 'json' 4 | require 'haml' 5 | 6 | set :server, 'thin' 7 | set :sockets, {} 8 | 9 | helpers do 10 | def logger 11 | request.logger 12 | end 13 | end 14 | 15 | def register(ws, id) 16 | socket_info = { 17 | id: id, 18 | path: "/hook/#{id}" 19 | } 20 | settings.sockets[ws] ||= [] 21 | settings.sockets[ws] << socket_info 22 | message_data = { 23 | type: 'registered', 24 | data: socket_info 25 | } 26 | 27 | message = message_data.to_json 28 | logger.info "sending: #{message}" 29 | ws.send message 30 | socket_info 31 | end 32 | 33 | def unregister(ws, id) 34 | end 35 | 36 | get '/' do 37 | if !request.websocket? 38 | haml :index 39 | else 40 | logger.info "websocket initializing" 41 | request.websocket do |ws| 42 | ws.onopen do 43 | id = SecureRandom.hex(8) 44 | register(ws, id) 45 | end 46 | 47 | ws.onmessage do |message| 48 | logger.info "received: #{message}" 49 | data = JSON.parse(message) 50 | if data['type']=='register' && data['id'] 51 | if /^\w+$/ === data['id'] 52 | register(ws,data['id']) 53 | else 54 | ws.send({type:'error', message:'ID must be a letter, number, or underscore'}.to_json) 55 | end 56 | elsif data['type']=='unregister' && data['id'] 57 | unregister(ws,data['id']) 58 | else 59 | ws.send({type:'error', message:'No such command'}.to_json) 60 | end 61 | end 62 | 63 | ws.onclose do 64 | logger.warn "websocket closed" 65 | settings.sockets.delete(ws) 66 | end 67 | end 68 | end 69 | end 70 | 71 | post '/hook/:id' do 72 | id = params[:id] 73 | sockets = settings.sockets.select {|ws,hooks| hooks.any?{|hook| hook[:id] == id}} 74 | halt 404 unless sockets.count > 0 75 | 76 | ['splat','captures','id'].each {|k| params.delete k} 77 | 78 | message_data = { 79 | type: 'hook', 80 | id: id, 81 | data: params 82 | } 83 | message = message_data.to_json 84 | logger.info "sending: #{message}" 85 | sockets.keys.each { |ws| ws.send message } 86 | {}.to_json 87 | end -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './app' 2 | 3 | run Sinatra::Application -------------------------------------------------------------------------------- /public/script/app.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var ws = new WebSocket(window.location.protocol.replace('http','ws') + '//' + window.location.host + window.location.pathname); 3 | 4 | 5 | var status = function(type,message){ 6 | var close_button = ''; 7 | $('#alerts').append(""); 8 | 9 | }; 10 | 11 | ws.onopen = function(){ 12 | status('success','Web socket is open'); 13 | }; 14 | 15 | ws.onclose = function(){ 16 | status('danger','Web socket closed'); 17 | } 18 | 19 | ws.onmessage = function(m){ 20 | data = JSON.parse(m.data); 21 | 22 | if(data.type=='registered'){ 23 | $('#hooks > #waiting').remove(); 24 | var id = data.data.id 25 | var curl = "curl https://" + window.location.host + data.data.path + " --data \"foo=bar\""; 26 | var row = $(""); 27 | $('td.id', row).text(id).html(); 28 | $('td.curl', row).text(curl).html(); 29 | 30 | $('#hooks').append(row) 31 | } 32 | 33 | if(data.type=='hook'){ 34 | message = JSON.stringify(data.data); 35 | $('#messages > #waiting').remove(); 36 | 37 | var row = $(""); 38 | 39 | $('td.id',row).text(data.id).html(); 40 | $('td.message',row).text(message).html(); 41 | 42 | $('#messages').append(row); 43 | } 44 | 45 | if(data.type=='error'){ 46 | status('danger', data.message); 47 | } 48 | 49 | } 50 | 51 | $('#register').submit(function(e){ 52 | e.preventDefault(); 53 | message = {type:'register', id: $('#id').val()} 54 | console.log(JSON.stringify(message)); 55 | ws.send(JSON.stringify(message)); 56 | }); 57 | 58 | 59 | }); -------------------------------------------------------------------------------- /views/index.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title Web Socket Hook 5 | %script(src='//code.jquery.com/jquery-2.2.3.min.js' type='text/javascript') 6 | %script(src='//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js' type='text/javascript') 7 | %script(src='/script/app.js' type='text/javascript') 8 | %link(href='//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' rel='stylesheet' type='text/css' media='all') 9 | %link(href='//maxcdn.bootstrapcdn.com/font-awesome/4.6.2/css/font-awesome.min.css' rel='stylesheet' type='text/css' media='all') 10 | %script 11 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 12 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 13 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 14 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 15 | 16 | ga('create', 'UA-2687661-19', 'auto'); 17 | ga('send', 'pageview'); 18 | 19 | %body 20 | %a(href='https://github.com/factor-io/websockethook') 21 | %img(style='position: absolute; top: 0; right: 0; border: 0;' src='https://camo.githubusercontent.com/e7bbb0521b397edbd5fe43e7f760759336b5e05f/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677265656e5f3030373230302e706e67' alt='Fork me on GitHub' data-canonical-src='https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png') 22 | 23 | .container 24 | .row 25 | .col-md-12 26 | %h1 27 | Web Socket Hook 28 | .lead 29 | Built by 30 | %a(href='https://factor.io/') Factor.io 31 | and hosted on 32 | %a(href='https://websockethook.io') websockethook.io 33 | %blockquote 34 | %strong Web Socket Hook 35 | is a bridge between web hooks and web sockets. You can connect to the service using a web socket client and register a web hook. On the other end you can POST messages to that web hook address, which will be received by your web socket client. 36 | %blockquote 37 | %strong Why? 38 | So that you can build apps that respond to web hooks even though your app is not directly accessible from the internet. And that's exactly what 39 | %a(href='https://factor.io/') Factor.io 40 | needed to run your workflows on/with your private infrastructure. 41 | 42 | .row 43 | .col-md-12 44 | %h2 Take it for a spin 45 | .row 46 | .col-md-6 47 | 48 | %p By opening this page you already initiated a new Web Socket connection to the server. You should see a green message "Web socket is open" to the right. 49 | %p The initiation of a new web socket also registered a new random default web hook. You can see the list of registered webhooks on the right. 50 | %p From the command line run the command from the "cURL Command" column in the "Registered Hooks" table. 51 | %p 52 | You should see 53 | %code {"foo":"bar"} 54 | appear in the "Incoming Messages" table. 55 | %p Pretty cool huh? 56 | %p 57 | Now try to change the values in the 58 | %code --data 59 | value in the curl command. 60 | %p Lastly, you can also set your own IDs. 61 | %form.form-inline#register 62 | .form-group 63 | %input.form-control#id(type='text' placeholder='ID' value='foo') 64 | %button.btn.btn-primary(type='submit') Register 65 | %p This way you can create a web hook with a third party service and always listen on the same address, even if the web socket connection is restarted. It also allows multiple web socket connections to share the same web hook address. You can test this by opening two browser windows and registered a web-hook with the same address and POSTing data to it. 66 | 67 | .col-md-6 68 | #alerts 69 | %h4 Registered Hooks: 70 | %table.table.table-bordered 71 | %thead 72 | %tr 73 | %th ID 74 | %th cURL command 75 | %tbody#hooks 76 | %tr#waiting 77 | %td 78 | .fa.fa-refresh.fa-spin 79 | waiting... 80 | %h4 Incoming messages: 81 | %table.table.table-bordered 82 | %thead 83 | %tr 84 | %th Hook ID 85 | %th Message 86 | %tbody#messages 87 | %tr#waiting 88 | %td(colspan='2') 89 | .fa.fa-refresh.fa-spin 90 | waiting... 91 | 92 | .row 93 | .col-md-12 94 | %h2 How does it work? 95 | %p When a connection is established the service sends a message similar to this. 96 | %pre 97 | %code 98 | :preserve 99 | { 100 | "type": "registered", 101 | "data": { 102 | "id": "31dd0a8df6dc6d1b", 103 | "url": "/hook/31dd0a8df6dc6d1b" 104 | } 105 | } 106 | %p 107 | which tells us that it created a web hook at the address 108 | %code /hook/31dd0a8df6dc6d1b 109 | and can receive HTTP POST messages. 110 | %p 111 | Now you can perform a 112 | %code curl 113 | command to POST to this endpoint. When the POST occurs, the service will send another message over the web socket similar to this. 114 | %pre 115 | %code 116 | :preserve 117 | { 118 | "type": "hook", 119 | "id": "31dd0a8df6dc6d1b", 120 | "data": { 121 | "foo": "bar" 122 | } 123 | } 124 | %p You can also register a new web hook over the open web socket by sending a message like this. 125 | %pre 126 | %code 127 | :preserve 128 | { 129 | "type": "register", 130 | "id": "my_static_address" 131 | } 132 | %p And of course you can un-register this hook with this command 133 | %pre 134 | %code 135 | :preserve 136 | { 137 | "type": "unregister", 138 | "id": "my_static_address" 139 | } 140 | .row 141 | .col-md-12 142 | %h2 Disclaimer 143 | %p No tests! This is currently only tested manually. --------------------------------------------------------------------------------