Skip to content

Commit 0145430

Browse files
committed
initial commit
0 parents  commit 0145430

17 files changed

+411
-0
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package.json
2+
package-lock.json
3+
node_modules/

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# hooked.sh - WebSockets over HTTP
2+
3+
hooked.sh lets you connect to WebSockets from your serverless apps by forwarding events over regular HTTP calls to your application.

Diff for: callbacktest.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
var http = require('http')
2+
http.createServer(function (req, res) {
3+
if (req.method.toLowerCase() == 'post') {
4+
let data = ''
5+
req.on('data', chunk => {
6+
data += chunk
7+
})
8+
req.on('end', () => {
9+
console.log("POST - " + data)
10+
res.end()
11+
});
12+
} else {
13+
res.end()
14+
}
15+
}).listen(8009)

Diff for: js/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error("This package is currently just a placeholder. Stay tuned https://hooked.sh/")

Diff for: server/.formatter.exs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

Diff for: server/.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
hooked-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

Diff for: server/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Hooked
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `hooked` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:hooked, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at <https://hexdocs.pm/hooked>.
21+

Diff for: server/lib/hooked.ex

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule Hooked do
2+
@moduledoc """
3+
Documentation for `Hooked`.
4+
"""
5+
6+
@doc """
7+
Hello world.
8+
9+
## Examples
10+
11+
iex> Hooked.hello()
12+
:world
13+
14+
"""
15+
def hello do
16+
:world
17+
end
18+
end

Diff for: server/lib/hooked/application.ex

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Hooked.Application do
2+
# See https://hexdocs.pm/elixir/Application.html
3+
# for more information on OTP Applications
4+
@moduledoc false
5+
6+
use Application
7+
8+
@impl true
9+
def start(_type, _args) do
10+
children = [
11+
{Bandit, scheme: :http, plug: Hooked.Router, options: [port: 8080]},
12+
{Redix, host: "0.0.0.0", port: 56379, name: :redix},
13+
{Registry, keys: :unique, name: :ws_registry},
14+
{Finch, name: :callback_finch},
15+
{DynamicSupervisor, strategy: :one_for_one, name: Hooked.WSSupervisor}
16+
]
17+
18+
# See https://hexdocs.pm/elixir/Supervisor.html
19+
# for other strategies and supported options
20+
opts = [strategy: :one_for_one, name: Hooked.Supervisor]
21+
Supervisor.start_link(children, opts)
22+
end
23+
end

Diff for: server/lib/hooked/authentication.ex

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Hooked.Authentication do
2+
def validate_bearer_header(header_value_array) when is_list(header_value_array) do
3+
do_validate_bearer_header(header_value_array)
4+
end
5+
6+
# any amount of items left
7+
defp do_validate_bearer_header([first_item | rest]) do
8+
case first_item
9+
|> String.split(" ") do
10+
["Bearer", token] ->
11+
{:ok, uid} = Redix.command(:redix, ["GET", "bearer:#{token}"])
12+
if uid == nil do
13+
# {:error, :not_found}
14+
{:ok, token, "none"}
15+
else
16+
{:ok, token, uid}
17+
end
18+
19+
_ ->
20+
do_validate_bearer_header(rest)
21+
end
22+
end
23+
24+
# no items left
25+
defp do_validate_bearer_header([]), do: {:error, :not_found}
26+
end

Diff for: server/lib/hooked/router.ex

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
defmodule Hooked.Router do
2+
use Plug.Router
3+
4+
plug(:match)
5+
plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason)
6+
plug(:dispatch)
7+
8+
# get "/" do
9+
# conn
10+
# |> handle_extract_auth(fn (token) ->
11+
# {:ok, pong} = Redix.command(:redix, ["PING"])
12+
# {:ok, %{pong: pong}}
13+
# end)
14+
# |> handle_response(conn)
15+
# end
16+
17+
post "/" do
18+
conn
19+
|> handle_extract_auth(&Hooked.WSConnection.start(conn, &1, &2))
20+
|> handle_response(conn)
21+
end
22+
23+
get "/:cid" do
24+
conn
25+
|> handle_extract_auth(fn _, _ -> Hooked.WSConnection.info(conn) end)
26+
|> handle_response(conn)
27+
end
28+
29+
post "/:cid" do
30+
conn
31+
|> handle_extract_auth(fn _, _ -> Hooked.WSConnection.send(conn) end)
32+
|> handle_response(conn)
33+
end
34+
35+
delete "/:cid" do
36+
conn
37+
|> handle_extract_auth(fn _, _ -> Hooked.WSConnection.stop(conn) end)
38+
|> handle_response(conn)
39+
end
40+
41+
match _ do
42+
conn
43+
|> put_resp_header("location", "https://dash.hooked.sh/")
44+
|> send_resp(302, "https://dash.hooked.sh/")
45+
end
46+
47+
defp handle_extract_auth(conn, success_lambda) do
48+
case conn
49+
|> get_req_header("authorization")
50+
|> Hooked.Authentication.validate_bearer_header() do
51+
{:ok, token, uid} ->
52+
success_lambda.(token, uid)
53+
54+
{:error, _} ->
55+
{:malformed_data, "Missing/Invalid authorization header"}
56+
end
57+
end
58+
59+
defp handle_response(response, conn) do
60+
%{code: code, message: message, json: json} =
61+
case response do
62+
{:ok, data} ->
63+
%{code: 200, message: Jason.encode!(data), json: true}
64+
65+
{:not_found, message} ->
66+
%{code: 404, message: Jason.encode!(%{reason: message}), json: true}
67+
68+
{:conflict, message} ->
69+
%{code: 409, message: Jason.encode!(%{reason: message}), json: true}
70+
71+
{:malformed_data, message} ->
72+
%{code: 400, message: Jason.encode!(%{reason: message}), json: true}
73+
74+
{:not_authorized, message} ->
75+
%{code: 401, message: Jason.encode!(%{reason: message}), json: true}
76+
77+
{:server_error, _} ->
78+
%{code: 500, message: Jason.encode!(%{reason: "An error occurred internally"}), json: true}
79+
80+
_ ->
81+
%{code: 500, message: Jason.encode!(%{reason: "An error occurred internally"}), json: true}
82+
end
83+
84+
case json do
85+
true ->
86+
conn
87+
|> put_resp_header("content-type", "application/json")
88+
89+
false ->
90+
conn
91+
end
92+
|> send_resp(code, message)
93+
end
94+
end

Diff for: server/lib/hooked/ws_connection.ex

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
defmodule Hooked.WSConnection do
2+
use WebSockex, restart: :temporary
3+
4+
def start_link([url, state, cid]) do
5+
WebSockex.start_link(url, __MODULE__, state, name: via_tuple(cid), handle_initial_conn_failure: true)
6+
end
7+
8+
# registry lookup handler
9+
defp via_tuple(cid), do: {:via, Registry, {:ws_registry, cid}}
10+
11+
def handle_frame({:text, msg}, state) do
12+
Redix.command(:redix, ["INCR", "usage:#{state.uid}:#{DateTime.utc_now.month}"])
13+
Finch.build(:post, state.callback, [], msg) |> Finch.request(:callback_finch)
14+
{:ok, state}
15+
end
16+
17+
def handle_cast({:send, frame}, state) do
18+
Redix.command(:redix, ["INCR", "usage:#{state.uid}:#{DateTime.utc_now.month}"])
19+
{:reply, frame, state}
20+
end
21+
22+
def handle_disconnect(_, state) do
23+
IO.puts("dc")
24+
:timer.sleep(15000)
25+
{:reconnect, state}
26+
end
27+
28+
def handle_info(:close_socket, {t_ref, state}) do
29+
IO.puts("close socket")
30+
:timer.cancel(t_ref)
31+
{:close, {nil, state}}
32+
end
33+
34+
defp not_nil(val, reason) do
35+
if val != nil and val != "" do
36+
{:ok, val}
37+
else
38+
{:error, reason}
39+
end
40+
end
41+
42+
defp expect_nil(val, reason) do
43+
if val != nil do
44+
{:error, reason}
45+
else
46+
{:ok, val}
47+
end
48+
end
49+
50+
def start_child(spec) do
51+
{status, _} = DynamicSupervisor.start_child(Hooked.WSSupervisor, spec)
52+
if status == :ok do
53+
{:ok}
54+
else
55+
{:server_error, "Failed to connect"}
56+
end
57+
end
58+
59+
def start(conn, token, uid) do
60+
with {:ok, url} <- not_nil(Map.get(conn.body_params, "url"), {:malformed_data, "'url' needs to be provided in the JSON encoded body."}),
61+
{:ok, callback} <- not_nil(Map.get(conn.body_params, "callback"), {:malformed_data, "'callback' needs to be provided in the JSON encoded body."}),
62+
cid <- Base.encode16(:crypto.hash(:sha256, "#{url}#{callback}#{token}")),
63+
{:ok, _} <- expect_nil(whereis(cid), {:conflict, "Connection between this websocket and callback is already open."}),
64+
spec <- {Hooked.WSConnection, [url, %{uid: uid, callback: callback}, cid]},
65+
{:ok} <- start_child(spec) do
66+
{:ok, %{cid: cid}}
67+
else
68+
{:error, reason} -> reason
69+
end
70+
end
71+
72+
def stop(conn) do
73+
with {:ok, cid} <- not_nil(Map.get(conn.path_params, "cid"), {:malformed_data, "'cid' is not specified in the url. Expected '/:cid'"}),
74+
{:ok, pid} <- not_nil(whereis(cid), {:not_found, "This connection does not exist."}) do
75+
Process.exit(pid, :kill)
76+
{:ok, %{}}
77+
else
78+
{:error, reason} -> reason
79+
end
80+
end
81+
82+
def info(conn) do
83+
with {:ok, cid} <- not_nil(Map.get(conn.path_params, "cid"), {:malformed_data, "'cid' is not specified in the url. Expected '/:cid'"}),
84+
{:ok, pid} <- not_nil(whereis(cid), {:not_found, "This connection does not exist."}) do
85+
{:ok, %{cid: cid}}
86+
else
87+
{:error, reason} -> reason
88+
end
89+
end
90+
91+
def send(conn) do
92+
with {:ok, cid} <- not_nil(Map.get(conn.path_params, "cid"), {:malformed_data, "'cid' is not specified in the url. Expected '/:cid'"}),
93+
{:ok, pid} <- not_nil(whereis(cid), {:not_found, "This connection does not exist."}) do
94+
{:not_authorized, "TODO: Not Implemented Yet"}
95+
else
96+
{:error, reason} -> reason
97+
end
98+
end
99+
100+
def whereis(cid) do
101+
case Registry.lookup(:ws_registry, cid) do
102+
[{pid, _}] -> pid
103+
[] -> nil
104+
end
105+
end
106+
end

Diff for: server/mix.exs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule Hooked.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :hooked,
7+
version: "0.1.0",
8+
elixir: "~> 1.13",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger],
18+
mod: {Hooked.Application, []}
19+
]
20+
end
21+
22+
# Run "mix help deps" to learn about dependencies.
23+
defp deps do
24+
[
25+
{:websockex, "~> 0.4.3"},
26+
{:redix, "~> 1.1"},
27+
{:bandit, ">= 0.5.0"},
28+
{:jason, "~> 1.3"},
29+
{:finch, "~> 0.12"}
30+
]
31+
end
32+
end

0 commit comments

Comments
 (0)