");
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 | });
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------