Skip to content

Commit 72c4243

Browse files
committed
Add websocket support with Gun
Expand cowboy routing so /ws[...] are redirected to a Gun Websocket handler. To facilitate this a ws macro is added to matcher that sets up 'route' by generating an ID. This ID is a query parameter appended to the redirect URI and mapped to backend host, port and path.
1 parent 213c4a1 commit 72c4243

File tree

12 files changed

+269
-61
lines changed

12 files changed

+269
-61
lines changed

config/config.exs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ config :dispatcher,
4444
# log whenever a layer starts processing
4545
log_layer_start_processing: CH.system_boolean("LOG_LAYER_START_PROCESSING"),
4646
# log whenever a layer matched, and if no matching layer was found
47-
log_layer_matching: CH.system_boolean("LOG_LAYER_MATCHING")
47+
log_layer_matching: CH.system_boolean("LOG_LAYER_MATCHING"),
48+
log_ws_all: CH.system_boolean("LOG_WS_ALL"),
49+
log_ws_backend: CH.system_boolean("LOG_WS_BACKEND"),
50+
log_ws_frontend: CH.system_boolean("LOG_WS_FRONTEND"),
51+
log_ws_unhandled: CH.system_boolean("LOG_WS_UNHANDLED")
4852

4953
# It is also possible to import configuration files, relative to this
5054
# directory. For example, you can emulate configuration per environment

lib/dispatcher.ex

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
defmodule Dispatcher do
22
use Matcher
33

4-
define_accept_types [
5-
text: [ "text/*" ],
6-
html: [ "text/html", "application/xhtml+html" ],
7-
json: [ "application/json", "application/vnd.api+json" ]
8-
]
4+
define_accept_types(
5+
text: ["text/*"],
6+
html: ["text/html", "application/xhtml+html"],
7+
json: ["application/json", "application/vnd.api+json"]
8+
)
99

1010
# get "/*_rest", %{ accept: %{ html: true } } do
1111
# Proxy.forward conn, [], "http://static/ember-app/index.html"
@@ -16,35 +16,39 @@ defmodule Dispatcher do
1616
# end
1717

1818
post "/hello/erika", %{} do
19-
Plug.Conn.send_resp conn, 401, "FORBIDDEN"
19+
Plug.Conn.send_resp(conn, 401, "FORBIDDEN")
2020
end
2121

2222
# 200 microservice dispatching
2323

24-
match "/hello/erika", %{ accept: %{ json: true } } do
25-
Plug.Conn.send_resp conn, 200, "{ \"message\": \"Hello Erika\" }"
24+
match "/hello/erika", %{accept: %{json: true}} do
25+
Plug.Conn.send_resp(conn, 200, "{ \"message\": \"Hello Erika\" }\n")
2626
end
2727

28-
match "/hello/erika", %{ accept: %{ html: true } } do
29-
Plug.Conn.send_resp conn, 200, "<html><head><title>Hello</title></head><body>Hello Erika</body></html>"
28+
match "/hello/erika", %{accept: %{html: true}} do
29+
Plug.Conn.send_resp(
30+
conn,
31+
200,
32+
"<html><head><title>Hello</title></head><body>Hello Erika</body></html>"
33+
)
3034
end
3135

3236
# 404 routes
3337

34-
match "/hello/aad/*_rest", %{ accept: %{ json: true } } do
35-
Plug.Conn.send_resp conn, 200, "{ \"message\": \"Hello Aad\" }"
38+
match "/hello/aad/*_rest", %{accept: %{json: true}} do
39+
Plug.Conn.send_resp(conn, 200, "{ \"message\": \"Hello Aad\" }")
3640
end
3741

38-
match "/*_rest", %{ accept: %{ json: true }, last_call: true } do
39-
Plug.Conn.send_resp conn, 404, "{ \"errors\": [ \"message\": \"Not found\", \"status\": 404 } ] }"
40-
end
42+
# Websocket example route
43+
# This forwards to /ws?target=<...>
44+
# Then forwards websocket from /ws?target=<...> to ws://localhost:7999
4145

42-
match "/*_rest", %{ accept: %{ html: true }, last_call: true } do
43-
Plug.Conn.send_resp conn, 404, "<html><head><title>Not found</title></head><body>No acceptable response found</body></html>"
46+
match "/ws2" do
47+
ws(conn, "ws://localhost:7999")
4448
end
4549

46-
match "/*_rest", %{ last_call: true } do
47-
Plug.Conn.send_resp conn, 404, "No response found"
48-
end
4950

51+
match "__", %{last_call: true} do
52+
send_resp(conn, 404, "Route not found. See config/dispatcher.ex")
53+
end
5054
end

lib/dispatcher/log.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ defmodule Dispatcher.Log do
22
@type log_name ::
33
:log_layer_start_processing
44
| :log_layer_matching
5+
| :log_ws_all
6+
| :log_ws_backend
7+
| :log_ws_frontend
8+
| :log_ws_unhandled
59

610
@spec log(log_name, any()) :: any()
711
def log(name, content) do

lib/manipulators/add_x_rewrite_url_header.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ defmodule Manipulators.AddXRewriteUrlHeader do
22
@behaviour ProxyManipulator
33

44
@impl true
5-
def headers( headers, {frontend_conn, _backend_conn} = connection ) do
5+
def headers(headers, {frontend_conn, _backend_conn} = connection) do
66
new_headers = [{"x-rewrite-url", frontend_conn.request_path} | headers]
77
{new_headers, connection}
88
end
99

1010
@impl true
11-
def chunk(_,_), do: :skip
11+
def chunk(_, _), do: :skip
1212

1313
@impl true
14-
def finish(_,_), do: :skip
14+
def finish(_, _), do: :skip
1515
end

lib/manipulators/remove_accept_encoding_header.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ defmodule Manipulators.RemoveAcceptEncodingHeader do
33

44
@impl true
55
def headers(headers, connection) do
6-
headers =
7-
headers
8-
|> Enum.reject( &match?( {"accept_encoding", _}, &1 ) )
6+
# headers =
7+
# headers
8+
# |> Enum.reject( &match?( {"accept_encoding", _}, &1 ) )
99
{headers, connection}
1010
end
1111

lib/matcher.ex

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ alias Dispatcher.Log
22

33
defmodule Matcher do
44
defmacro __using__(_opts) do
5+
# Set this attribute _BEFORE_ any code is ran
6+
Module.register_attribute(__CALLER__.module, :websocket, accumulate: true)
7+
58
quote do
69
require Matcher
710
import Matcher
11+
import Plug.Router, only: [forward: 2]
812
import Plug.Conn, only: [send_resp: 3]
913
import Proxy, only: [forward: 3]
1014

1115
def layers do
12-
[ :service, :last_call ]
16+
[:service, :last_call]
1317
end
18+
1419
defoverridable layers: 0
1520

1621
def dispatch(conn) do
@@ -28,6 +33,34 @@ defmodule Matcher do
2833
end
2934
end
3035

36+
defmacro ws(conn, host) do
37+
# host = "ws://localhost:8000/test"
38+
39+
parsed =
40+
URI.parse(host)
41+
|> Log.inspect(:log_ws_all, label: "Creating websocket route")
42+
43+
id = for _ <- 1..24, into: "", do: <<Enum.random('0123456789abcdef')>>
44+
45+
host = parsed.host || "localhost"
46+
port = parsed.port || 80
47+
path = parsed.path || "/"
48+
49+
Module.put_attribute(__CALLER__.module, :websocket, %{
50+
host: host,
51+
port: port,
52+
path: path,
53+
id: id
54+
})
55+
56+
# Return redirect things
57+
quote do
58+
unquote(conn)
59+
|> Plug.Conn.resp(:found, "")
60+
|> Plug.Conn.put_resp_header("location", "/ws?target=" <> unquote(id))
61+
end
62+
end
63+
3164
defmacro get(path, options \\ quote(do: %{}), do: block) do
3265
quote do
3366
match_method(get, unquote(path), unquote(options), do: unquote(block))
@@ -98,7 +131,6 @@ defmodule Matcher do
98131
defmacro __before_compile__(_env) do
99132
matchers =
100133
Module.get_attribute(__CALLER__.module, :matchers)
101-
# |> IO.inspect(label: "Discovered matchers")
102134
|> Enum.map(fn {call, path, options, block} ->
103135
make_match_method(call, path, options, block, __CALLER__)
104136
end)
@@ -110,7 +142,18 @@ defmodule Matcher do
110142
end
111143
end
112144

113-
[last_match_def | matchers]
145+
socket_dict_f =
146+
quote do
147+
def websockets() do
148+
Enum.reduce(@websocket, %{}, fn x, acc -> Map.put(acc, x.id, x) end)
149+
end
150+
151+
def get_websocket(id) do
152+
Enum.find(@websocket, fn x -> x.id == id end)
153+
end
154+
end
155+
156+
[socket_dict_f, last_match_def | matchers]
114157
|> Enum.reverse()
115158
end
116159

@@ -171,24 +214,29 @@ defmodule Matcher do
171214

172215
new_accept =
173216
case value do
174-
[item] -> # convert item
217+
# convert item
218+
[item] ->
175219
{:%{}, [], [{item, true}]}
220+
176221
[_item | _rest] ->
177222
raise "Multiple items in accept arrays are not supported."
223+
178224
{:%{}, _, _} ->
179225
value
180226
end
181227

182228
new_list =
183229
list
184-
|> Keyword.drop( [:accept] )
185-
|> Keyword.merge( [accept: new_accept] )
230+
|> Keyword.drop([:accept])
231+
|> Keyword.merge(accept: new_accept)
186232

187233
{:%{}, any, new_list}
188234
else
189235
options
190236
end
191-
_ -> options
237+
238+
_ ->
239+
options
192240
end
193241
end
194242

@@ -223,8 +271,6 @@ defmodule Matcher do
223271
str -> str
224272
end).()
225273

226-
# |> IO.inspect(label: "call name")
227-
228274
# Creates the variable(s) for the parsed path
229275
process_derived_path_elements = fn elements ->
230276
reversed_elements = Enum.reverse(elements)
@@ -310,7 +356,6 @@ defmodule Matcher do
310356
def dispatch_call(conn, accept_types, layers_fn, call_handler) do
311357
# Extract core info
312358
{method, path, accept_header, host} = extract_core_info_from_conn(conn)
313-
# |> IO.inspect(label: "extracted header")
314359

315360
# Extract core request info
316361
accept_hashes =
@@ -321,10 +366,11 @@ defmodule Matcher do
321366

322367
# layers |> IO.inspect(label: "layers" )
323368
# Try to find a solution in each of the layers
324-
layers = layers_fn.()
325-
|> Log.inspect(:log_available_layers, "Available layers")
369+
layers =
370+
layers_fn.()
371+
|> Log.inspect(:log_available_layers, "Available layers")
326372

327-
reverse_host = Enum.reverse( host )
373+
reverse_host = Enum.reverse(host)
328374

329375
response_conn =
330376
layers
@@ -432,7 +478,7 @@ defmodule Matcher do
432478
defp sort_and_group_accept_headers(accept) do
433479
accept
434480
|> safe_parse_accept_header()
435-
# |> IO.inspect(label: "parsed_accept_header")
481+
|> IO.inspect(label: "parsed_accept_header")
436482
|> Enum.sort_by(&elem(&1, 3))
437483
|> Enum.group_by(&elem(&1, 3))
438484
|> Map.to_list()

lib/mu_dispatcher.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,23 @@ defmodule MuDispatcher do
1010
port = 80
1111

1212
children = [
13-
{Plug.Cowboy, scheme: :http, plug: PlugRouterDispatcher, options: [port: port]}
13+
# this is kinda strange, but the 'plug:' field is not used when 'dispatch:' is provided (my understanding)
14+
{Plug.Adapters.Cowboy,
15+
scheme: :http, plug: PlugRouterDispatcher, options: [dispatch: dispatch, port: port]}
1416
]
1517

1618
Logger.info("Mu Dispatcher starting on port #{port}")
1719

1820
Supervisor.start_link(children, strategy: :one_for_one)
1921
end
22+
23+
defp dispatch do
24+
[
25+
{:_,
26+
[
27+
{"/ws/[...]", WebsocketHandler, %{}},
28+
{:_, Plug.Cowboy.Handler, {PlugRouterDispatcher, []}}
29+
]}
30+
]
31+
end
2032
end

lib/plug_router_dispatcher.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
alias Dispatcher.Log
2+
13
defmodule PlugRouterDispatcher do
24
use Plug.Router
35

@@ -6,6 +8,6 @@ defmodule PlugRouterDispatcher do
68
plug(:dispatch)
79

810
match _ do
9-
Dispatcher.dispatch( conn )
11+
Dispatcher.dispatch(conn)
1012
end
1113
end

lib/proxy.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
defmodule Proxy do
2-
@request_manipulators [Manipulators.AddXRewriteUrlHeader,Manipulators.RemoveAcceptEncodingHeader]
2+
@request_manipulators [
3+
Manipulators.AddXRewriteUrlHeader,
4+
Manipulators.RemoveAcceptEncodingHeader
5+
]
36
@response_manipulators [Manipulators.AddVaryHeader]
47
@manipulators ProxyManipulatorSettings.make_settings(
58
@request_manipulators,
@@ -13,6 +16,7 @@ defmodule Proxy do
1316
conn,
1417
path,
1518
base,
16-
@manipulators)
19+
@manipulators
20+
)
1721
end
1822
end

0 commit comments

Comments
 (0)