Skip to content

Instantly share code, notes, and snippets.

@mnussbaumer
Created September 23, 2019 21:47
Show Gist options
  • Save mnussbaumer/3f08a4c3dbd9d3cd252b8ad87a3a8d4f to your computer and use it in GitHub Desktop.
Save mnussbaumer/3f08a4c3dbd9d3cd252b8ad87a3a8d4f to your computer and use it in GitHub Desktop.
use Mix.Config
import_config "../apps/*/config/config.exs"
config :logger,
handle_otp_reports: true,
handle_sasl_reports: true
defmodule Satellite.Verb do
def parse(ctx, rem, <<"GET">>), do: {:done, %{ctx | verb: :get}, rem}
def parse(ctx, rem, <<"POST">>), do: {:done, %{ctx | verb: :post}, rem}
def parse(ctx, <<>>, acc), do: {:cont, ctx, acc}
def parse(ctx, <<?\s, rem::bits>>, acc), do: parse(ctx, rem, acc)
def parse(ctx, <<h, rem::bits>>, acc), do: parse(ctx, rem, <<acc::bits, h>>)
def parse(ctx, _, _), do: {:error, "Parsing Verb"}
end
alias Satellite.Configuration, as: Config
defp set_defaults(config) do
maybe_set_name(config)
|> maybe_set_port()
|> maybe_set_pipeline()
end
defp maybe_set_name(%Config{name: name} = config), do: maybe_set(:name, name, {:local, __MODULE__}, config)
defp maybe_set_port(%Config{port: port} = config), do: maybe_set(:port, port, 4000, config)
defp maybe_set_pipeline(%Config{pipeline: pipeline} = config) do
maybe_set(:pipeline, pipeline, Satellite.Defaults.default_pipeline(), config)
end
defp maybe_set(key, nil, default, config), do: Map.put(config, key, default)
defp maybe_set(_, _, _, config), do: config
def start_link(%Config{} = config) do
%{name: name} = ok_config = set_defaults(config)
:gen_statem.start_link(name, __MODULE__, ok_config, [])
end
def init(%{port: port} = config) do
Process.flag(:trap_exit, true)
{:ok, socket} = :gen_tcp.listen(port, [:binary, {:packet, :raw}, {:active, false}, {:reuseaddr, true}])
data = %__MODULE__{socket: socket, config: config}
{:ok, :starting, data, [{:next_event, :internal, :create_listener}]}
end
defmodule Satellite do
@behaviour :gen_statem
alias Satellite.Request
defstruct [:socket, :config, :conn, pipeline: [], request: %Request{}]
@impl true
def callback_mode(), do: :handle_event_function
def start_link(socket, opts) do
:gen_statem.start_link(__MODULE__, {socket, opts}, [])
end
@impl true
def init({socket, config}) do
{:ok, :waiting, %__MODULE__{socket: socket, config: config}, [{:next_event, :internal, :wait}]}
end
@impl true
def handle_event(:internal, :wait, :waiting, %{socket: socket, config: %{pipeline: pipeline}} = data) do
{:ok, conn} = :gen_tcp.accept(socket)
:gen_tcp.controlling_process(conn, self())
n_data = %{data | conn: conn, pipeline: pipeline, request: %Request{}}
set_next_step(n_data, <<>>)
end
def handle_event(:internal, :read, _, %{conn: conn} = data) do
case :gen_tcp.recv(conn, 0, 1000) do
{:ok, packet} ->
{:keep_state_and_data, [{:next_event, :internal, {:parse, packet}}]}
{:error, reason} ->
{:next_state, :response, %{data | request: "Error: #{inspect reason}"}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, {:parse, packet}, {type, name, fun, acc}, %{request: request} = data) do
case fun.(request, packet, acc) do
{:cont, n_request, n_acc} ->
{:next_state, {type, name, fun, n_acc}, %{data | request: n_request}, [{:next_event, :internal, :read}]}
{:done, n_request, remaining} ->
set_next_step(%{data | request: n_request}, remaining)
{:error, reason} ->
{:next_state, :response, %{data | request: "#{inspect reason}"}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, :send_response, :response, %{conn: conn, request: request} = data) do
:gen_tcp.send(conn, make_response(request))
{:keep_state, data, [{:next_event, :internal, :close}]}
end
def handle_event(:internal, :close, _, %{conn: conn} = data) do
:gen_tcp.close(conn)
{:next_state, :waiting, %{data | conn: nil}, [{:next_event, :internal, :wait}]}
end
defp set_next_step(%{pipeline: [h | t]} = data, remaining) do
event = case remaining do
<<>> -> :read
_ -> {:parse, remaining}
end
{:next_state, h, %{data | pipeline: t}, [{:next_event, :internal, event}]}
end
defp set_next_step(%{pipeline: [], request: request} = data, _remaining) do
n_request = "#{inspect request}"
{:next_state, :response, %{data | request: n_request}, [{:next_event, :internal, :send_response}]}
end
defp make_response(request) do
b = :erlang.iolist_to_binary(request)
:io_lib.fwrite(
"HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: ~p\n\n~s",
[:erlang.size(b), b]
)
end
end
defmodule Satellite.Defaults do
def default_pipeline() do
[
{:read, :verb, &Satellite.Verb.parse/3, <<>>},
{:read, :path, &Satellite.Path.parse/3, {false, [], <<>>}}
]
end
end
defmodule Satellite.Path do
def parse(ctx, <<>>, acc), do: {:cont, ctx, acc}
def parse(ctx, <<?\s, rem::bits>>, {false, _, _} = acc), do: parse(ctx, rem, acc)
def parse(ctx, <<?\s, rem::bits>>, {true, acc, prior}) do
n_acc = maybe_add_prior(acc, prior)
n_ctx = %{
ctx |
path: Enum.reverse(
List.flatten(n_acc)
)
}
{:done, n_ctx, rem}
end
def parse(%{query: q} = ctx, <<?\s, rem::bits>>, {:query, :value, key, value}) do
{:done, %{ctx | query: Map.put(q, key, value)}, rem}
end
def parse(ctx, <<?\s, rem::bits>>, _), do: {:done, ctx, rem}
def parse(ctx, <<?=, rem::bits>>, {:query, :key, key}) do
parse(ctx, rem, {:query, :value, key, <<>>})
end
def parse(ctx, <<h, rem::bits>>, {:query, :key, key}) do
parse(ctx, rem, {:query, :key, <<key::binary, h>>})
end
def parse(%{query: q} = ctx, <<?&, rem::bits>>, {:query, :value, key, value}) do
parse(%{ctx | query: Map.put(q, key, value)}, rem, {:query, :key, <<>>})
end
def parse(ctx, <<h, rem::bits>>, {:query, :value, key, value}) do
parse(ctx, rem, {:query, :value, key, <<value::binary, h>>})
end
def parse(ctx, <<?/, rem::bits>>, {_, acc, prior}) do
n_acc = maybe_add_prior(acc, prior)
parse(ctx, rem, {true, n_acc, <<>>})
end
def parse(ctx, <<??, rem::bits>>, {:true, acc, prior}) do
n_acc = maybe_add_prior(acc, prior)
n_ctx = %{
ctx |
path: Enum.reverse(
List.flatten(n_acc)
)
}
parse(n_ctx, rem, {:query, :key, <<>>})
end
def parse(ctx, <<h, rem::bits>>, {_, acc, prior}) do
parse(ctx, rem, {true, acc, <<prior::binary, h>>})
end
def parse(_ctx, _, _), do: {:error, "Parsing path"}
defp maybe_add_prior(acc, <<>>), do: acc
defp maybe_add_prior(acc, prior), do: [prior | acc]
end
defmodule Satellite.Conn do
def parse(ctx, rem, <<"HTTP/1.1", ?\r, ?\n>>), do: {:done, %{ctx | version: {1, 1}}, rem}
def parse(ctx, rem, <<"HTTP/1.0", ?\r, ?\n>>), do: {:done, %{ctx | version: {1, 0}}, rem}
def parse(ctx, <<>>, acc), do: {:cont, ctx, acc}
def parse(ctx, <<?\s, rem::bits>>, acc), do: parse(ctx, rem, acc)
def parse(ctx, <<h, rem::bits>>, acc), do: parse(ctx, rem, <<acc::binary, h>>)
def parse(ctx, _, _), do: {:error, "Parsing Conn"}
end
defmodule Satellite.Defaults do
def default_pipeline() do
[
{:read, :verb, &Satellite.Verb.parse/3, <<>>},
{:read, :path, &Satellite.Path.parse/3, {false, [], <<>>}},
{:read, :conn, &Satellite.Conn.parse/3, <<>>}
]
end
end
defmodule Satellite.Headers do
import Satellite.Shared, only: [downcase: 1]
def parse(ctx, <<>>, acc), do: {:cont, ctx, acc}
def parse(ctx, <<?:, rem::bits>>, {:header, key}), do: parse(ctx, rem, {:value, key, <<>>})
def parse(ctx, <<?\s, rem::bits>>, {:header, key}), do: parse(ctx, rem, {:header, key})
def parse(ctx, <<?\s, rem::bits>>, {:value, key, acc}), do: parse(ctx, rem, {:value, key, acc})
def parse(ctx, <<h, rem::bits>>, {:header, key}) do
n_h = downcase(h)
parse(ctx, rem, {:header, <<key::binary, n_h>>})
end
def parse(ctx, <<?\r, rem::bits>>, {:header, _}) do
parse(ctx, rem, {:in_termination, <<?\r>>})
end
def parse(ctx, <<?\n, rem::bits>>, {:in_termination, <<?\r>>}) do
parse(ctx, rem, {:in_termination, <<?\r,?\n>>})
end
def parse(ctx, <<?\r, rem::bits>>, {:in_termination, <<?\r, ?\n>>}) do
parse(ctx, rem, {:in_termination, <<?\r,?\n,?\r>>})
end
def parse(ctx, <<?\n, rem::bits>>, {:in_termination, <<?\r, ?\n, ?\r>>}) do
{:done, %{ctx | finished_headers: true}, rem}
end
def parse(ctx, <<h, rem::bits>>, {:in_termination, <<?\r, ?\n>>}) do
n_h = downcase(h)
parse(ctx, rem, {:header, <<n_h>>})
end
def parse(%{headers: headers} = ctx, <<?\r, rem::bits>>, {:value, key, acc}) do
value = translate_header_content(key, acc)
n_ctx = %{ctx | headers: Map.put(headers, key, value)}
parse(n_ctx, rem, {:in_termination, <<?\r>>})
end
def parse(ctx, <<h, rem::bits>>, {:value, key, acc}) do
n_h = case downcased_header?(key) do
true -> downcase(h)
_ -> h
end
parse(ctx, rem, {:value, key, <<acc::binary, n_h>>})
end
def parse(_ctx, _, _), do: {:error, "Parsing headers"}
def translate_header_content(<<"content-length">>, val) when is_binary(val) do
:erlang.binary_to_integer(val)
catch _ -> 0
end
def translate_header_content(_, val), do: val
def downcased_header?(<<"content-type">>), do: true
def downcased_header?(<<"accept">>), do: true
def downcased_header?(_), do: false
end
defmodule Satellite.Shared do
use Bitwise, only_operators: true
Enum.each(?A..?Z, fn(value) ->
downcased = value ^^^ 32
def downcase(unquote(value)), do: unquote(downcased)
end)
def downcase(val), do: val
end
defmodule Launchpad do
@behaviour :gen_statem
defstruct [:socket, :config, max_pool: 5, pool_count: 0, acceptors: %{}]
def start_link(opts) do
name = Map.get(opts, :name, {:local, __MODULE__})
:gen_statem.start_link(name, __MODULE__, opts, [])
end
@impl true
def callback_mode(), do: :handle_event_function
@impl true
def init(opts) do
Process.flag(:trap_exit, true)
port = Map.get(opts, :port, 4000)
{:ok, socket} = :gen_tcp.listen(port, [:binary, {:packet, :raw}, {:active, true}, {:reuseaddr, true}])
data = %__MODULE__{socket: socket, config: opts}
{:ok, :starting, data, [{:next_event, :internal, :create_listener}]}
end
@impl true
def handle_event(:internal, :create_listener, _state,
%__MODULE__{
socket: socket,
config: config,
max_pool: max,
pool_count: pc,
acceptors: acceptors
} = data
) when pc < max do
{:ok, pid} = Satellite.start_link(socket, Map.put(config, :number, pc))
n_acceptors = Map.put(acceptors, pid, true)
{:keep_state, %{data | pool_count: pc + 1, acceptors: n_acceptors}, [{:next_event, :internal, :create_listener}]}
end
def handle_event(:internal, :create_listener, :starting, data), do: {:next_state, :running, data, []}
def handle_event(:internal, :create_listener, _state, _data), do: {:keep_state_and_data, []}
def handle_event(:info, {:EXIT, pid, _reason}, _, %{pool_count: pc, acceptors: acceptors} = data) when :erlang.is_map_key(pid, acceptors) do
{_, n_acceptors} = Map.pop(acceptors, pid)
{:keep_state, %{data | pool_count: pc - 1, acceptors: n_acceptors}, [{:next_event, :internal, :create_listener}]}
end
def handle_event(:info, {:EXIT, pid, reason}, _, _data) do
IO.puts("Received exit from unknown process #{inspect pid} with reason #{reason}")
{:keep_state_and_data, []}
end
end
defmodule Satellite.Host do
import Satellite.Shared, only: [downcase: 1]
def set(%{headers: headers} = ctx, _) do
host = Map.get(headers, "host", "")
{:ok, %{ctx | host: split_host(host)}}
end
def split_host(val), do: split_host(val, <<>>, [])
def split_host(<<>>, segment, acc), do: Enum.reverse([segment | acc])
def split_host(<<?., rem::bits>>, segment, acc), do: split_host(rem, <<>>, [segment | acc])
def split_host(<<?:, _::bits>>, segment, acc), do: Enum.reverse([segment | acc])
def split_host(<<h, rem::bits>>, segment, acc) do
n_h = downcase(h)
split_host(rem, <<segment::binary, n_h>>, acc)
end
def split_host(_, _, _), do: []
end
def handle_event(:internal, event, {:noread, _name, fun, acc}, %{request: request} = data) do
case fun.(request, acc) do
{:ok, n_request} ->
set_next_step(%{data | request: n_request}, {:event, event})
{:error, reason} ->
{:next_state, :response, %{data | request: "Error: #{inspect reason}"}, [{:next_event, :internal, :send_response}]}
end
end
defp set_next_step(%{pipeline: [h | t]} = data, remaining) do
event = case remaining do
<<>> -> :read
{:event, event} -> event
_ -> {:parse, remaining}
end
{:next_state, h, %{data | pipeline: t}, [{:next_event, :internal, event}]}
end
defmodule Satellite.Accept do
def extract(%{headers: headers} = ctx, _) do
{:ok, %{ctx | accept: extract_accept(headers)}}
end
def extract_accept(%{"accept" => <<"application/json", _::bits>>}), do: :json
def extract_accept(%{"accept" => <<"text/html", _::bits>>}), do: :html
def extract_accept(_), do: :any
end
defmodule Satellite.Check do
def check(%{verb: :get} = ctx, _), do: {:dispatch, ctx}
def check(%{verb: :post} = ctx, _), do: {:next, ctx}
def check(ctx, _), do: {:dispatch, ctx}
end
def handle_event(:internal, event, {:check, _name, fun, acc}, %{request: request} = data) do
case fun.(request, acc) do
{:next, n_request} ->
set_next_step(%{data | request: n_request}, {:event, event})
{:dispatch, n_request} ->
{:next_state, :dispatching, %{data | request: n_request}, [{:next_event, :internal, :dispatch}]}
{:response, resp} ->
{:next_state, :response, %{data | request: resp}, [{:next_event, :internal, :send_response}]}
{:error, reason} ->
{:next_state, :response, %{data | request: "Error, #{inspect reason}"}, [{:next_event, :internal, :send_response}]}
end
end
defmodule Satellite.Routing do
defmacro __using__(_opts) do
quote do
import Satellite.Routing
@before_compile Satellite.Routing
end
end
defmacro route(verb, path, module, function, domain \\ "*") do
verb_atom = String.to_atom(String.downcase(verb))
{path_splitted, vars} = split_path(path)
{domain_splitted, domain_vars} = split_domain(domain)
all_vars = vars ++ domain_vars
quote do
def route(unquote(verb_atom), unquote(path_splitted), unquote(domain_splitted), %{params: params} = request) do
ctx = %{
request |
params: Enum.reduce(unquote(all_vars), params, fn({key, var}, acc) ->
Map.put(acc, key, Macro.escape(var))
end)
}
apply(unquote(module), unquote(function), [ctx])
end
end
end
defp split_path(path) do
case String.split(path, "/", trim: true) do
["*"] -> {(quote do: _), []}
split ->
Enum.reduce(split, {[], []}, fn
("*", {acc1, acc2}) ->
{[(quote do: _) | acc1], acc2}
(<<?:, rem::binary>>, {acc1, acc2}) ->
{[Macro.var(String.to_atom(rem), nil) | acc1], [{String.to_atom(rem), Macro.var(String.to_atom(rem), nil)} | acc2]}
(other, {acc1, acc2}) ->
{[other | acc1], acc2}
end)
|> case do
{paths, vars} -> {Enum.reverse(paths), Enum.reverse(vars)}
end
end
end
defp split_domain(domain) do
case String.split(domain, ".", trim: true) do
["*"] -> {(quote do: _), []}
[<<?:, rem::binary>>] ->
host_var = String.to_atom("host_#{rem}")
{Macro.var(host_var, nil), [{host_var, Macro.var(host_var, nil)}]}
split ->
Enum.reduce(split, {[], []}, fn
("*", {acc1, acc2}) ->
{[(quote do: _) | acc1], acc2}
(<<?\\, rem::binary>>, {acc1, acc2}) -> {[rem | acc1], acc2}
(<<?:, rem::binary>>, {acc1, acc2}) ->
host_var = String.to_atom("host_#{rem}")
{[Macro.var(host_var, nil) | acc1], [{host_var, Macro.var(host_var, nil)} | acc2]}
(other, {acc1, acc2}) ->
{[other | acc1], acc2}
end)
|> case do
{paths, vars} -> {Enum.reverse(paths), Enum.reverse(vars)}
end
end
end
defmacro __before_compile__(_env) do
quote do
def route(_, _, _, _ctx) do
Satellite.Response.not_found()
end
end
end
end
defmodule Test.Router do
use Satellite.Routing
route "get", "/some/:path", AnotherModule, :a_function
end
defmodule Test.Router do
def route(:get, ["some", path], _, %{params: params} = request) do
ctx = %{
request |
params:
Enum.reduce([{:path, ast_representation_of_the_var_variable}], params, fn({key, var}, acc) ->
Map.put(acc, key, var)
end)
}
apply(AnotherModule, :a_function, [ctx])
end
def route(_, _, _, _ctx), do: Satellite.Response.not_found()
end
defmodule Satellite do
@behaviour :gen_statem
alias Satellite.{Request, Response}
defstruct [:socket, :config, :conn, :response, :router, pipeline: [], request: %Request{}]
@impl true
def callback_mode(), do: :handle_event_function
def start_link(socket, opts) do
:gen_statem.start_link(__MODULE__, {socket, opts}, [])
end
@impl true
def init({socket, config}) do
{:ok, :waiting, %__MODULE__{socket: socket, config: config}, [{:next_event, :internal, :wait}]}
end
@impl true
def handle_event(:internal, :wait, :waiting, %{socket: socket, config: %{router: router, pipeline: pipeline}} = data) do
{:ok, conn} = :gen_tcp.accept(socket)
:gen_tcp.controlling_process(conn, self())
n_data = %{data | conn: conn, router: router, pipeline: pipeline, request: %Request{}}
set_next_step(n_data, <<>>)
end
def handle_event(:internal, event, {:noread, _name, fun, acc}, %{request: request} = data) do
case fun.(request, acc) do
{:ok, n_request} ->
set_next_step(%{data | request: n_request}, {:event, event})
{:error, reason} ->
response = Response.error_resp("#{inspect reason}")
{:next_state, :response, %{data | response: response}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, event, {:check, _name, fun, acc}, %{request: request} = data) do
case fun.(request, acc) do
{:next, n_request} ->
set_next_step(%{data | request: n_request}, {:event, event})
{:dispatch, n_request} ->
{:next_state, :dispatching, %{data | request: n_request}, [{:next_event, :internal, :dispatch}]}
{:response, response} ->
{:next_state, :response, %{data | response: response}, [{:next_event, :internal, :send_response}]}
{:error, reason} ->
response = Response.error_resp("#{inspect reason}")
{:next_state, :response, %{data | response: response}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, :read, _, %{conn: conn} = data) do
case :gen_tcp.recv(conn, 0, 1000) do
{:ok, packet} ->
IO.inspect(packet, label: "packet")
{:keep_state_and_data, [{:next_event, :internal, {:parse, packet}}]}
{:error, reason} ->
response = Response.error_resp("#{inspect reason}")
{:next_state, :response, %{data | response: response}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, {:parse, packet}, {type, name, fun, acc}, %{request: request} = data) do
case fun.(request, packet, acc) do
{:cont, n_request, n_acc} ->
IO.inspect({:cont, n_request, n_acc})
{:next_state, {type, name, fun, n_acc}, %{data | request: n_request}, [{:next_event, :internal, :read}]}
{:done, n_request, remaining} ->
IO.inspect({:done, n_request, remaining})
set_next_step(%{data | request: n_request}, remaining)
{:error, reason} ->
response = Response.error_resp("#{inspect reason}")
{:next_state, :response, %{data | response: response}, [{:next_event, :internal, :send_response}]}
end
end
def handle_event(:internal, :dispatch, :dispatching, %{request: request, conn: conn, router: router} = data) do
try_dispatch(conn, router, request)
{:next_state, :waiting, %{data | conn: nil}, [{:next_event, :internal, :wait}]}
end
def handle_event(:internal, :send_response, :response, %{conn: conn, response: response} = data) do
try_send_response(conn, response)
{:next_state, :waiting, %{data | conn: nil}, [{:next_event, :internal, :wait}]}
end
def handle_event(:internal, :close, _, %{conn: conn} = data) do
:gen_tcp.close(conn)
{:next_state, :waiting, %{data | conn: nil}, [{:next_event, :internal, :wait}]}
end
defp set_next_step(%{pipeline: [h | t]} = data, remaining) do
event = case remaining do
<<>> -> :read
{:event, event} -> event
_ -> {:parse, remaining}
end
{:next_state, h, %{data | pipeline: t}, [{:next_event, :internal, event}]}
end
defp set_next_step(%{pipeline: []} = data, _remaining) do
{:next_state, :dispatching, data, [{:next_event, :internal, :dispatch}]}
end
defp try_dispatch(conn, router, %{verb: verb, path: path, host: host} = request) do
case apply(router, :route, [verb, path, host, request]) do
response -> try_send_response(conn, response)
end
rescue
e -> try_send_response(conn, Response.error_resp())
end
defp try_send_response(conn, %Response{} = response) do
n_response = Response.make_resp(response)
send_response(conn, n_response)
after
:gen_tcp.close(conn)
end
defp try_send_response(conn, response) when is_binary(response) do
send_response(conn, response)
after
:gen_tcp.close(conn)
end
defp send_response(conn, response) do
:gen_tcp.send(conn, response)
end
end
defmodule Satellite do
@behaviour :gen_statem
defstruct [:socket, :config, :conn, pipeline: [], request: %{}]
def start_link(socket, opts) do
:gen_statem.start_link(__MODULE__, {socket, opts}, [])
end
@impl true
def callback_mode(), do: :handle_event_function
@impl true
def init({socket, config}) do
{:ok, :waiting, %__MODULE__{socket: socket, config: config}, [{:next_event, :internal, :wait}]}
end
@impl true
def handle_event(:internal, :wait, :waiting, %{socket: socket} = data) do
{:ok, conn} = :gen_tcp.accept(socket)
:gen_tcp.controlling_process(conn, self())
{:next_state, :parsing, %{data | conn: conn}, [{:next_event, :internal, :read}]}
end
def handle_event(:internal, :read, :parsing, %{conn: conn} = data) do
case :gen_tcp.recv(conn, 0, 1000) do
{:ok, packet} ->
IO.inspect(packet, label: "received")
n_data = %{data | request: packet}
{:next_state, :response, n_data, [{:next_event, :internal, :send_response}]}
{:error, reason} ->
IO.inspect(reason, label: "error on recv")
{:keep_state, data, [{:next_event, :internal, :close}]}
end
end
def handle_event(:internal, :send_response, :response, %{conn: conn, request: request} = data) do
b = :erlang.iolist_to_binary(request)
response = :io_lib.fwrite(
"HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: ~p\n\n~s",
[:erlang.size(b), b]
)
:gen_tcp.send(conn, response)
{:keep_state, data, [{:next_event, :internal, :close}]}
end
def handle_event(:internal, :close, _, %{conn: conn} = data) do
:gen_tcp.close(conn)
{:next_state, :waiting, %{data | conn: nil, request: %{}}, [{:next_event, :internal, :wait}]}
end
end
defmodule Satellite.Response do
defstruct [code: 200, headers: [{<<"content-type">>, <<"text/html">>}], body: <<>>]
@codes [{200, "OK"}, {404, "Not Found"}, {500, "Internal Server Error"}]
def make_resp(
%__MODULE__{
code: code,
headers: headers,
body: body
}
) do
code_prep = make_code(code)
headers_prep = map_headers(headers)
{n_body, content_length_prep} = create_length(body)
<<"HTTP/1.0 ", code_prep::binary, "\n", headers_prep::binary, content_length_prep::binary, "\n", n_body::binary>>
end
defp map_headers(headers), do: map_headers(headers, <<>>)
defp map_headers([{header, value} | t], acc), do: map_headers(t, <<acc::binary, header::binary, ": ", value::binary, "\n">>)
defp map_headers([], acc), do: acc
defp create_length(nil), do: {<<>>, <<"content-length: 0">>}
defp create_length(<<>>), do: {<<>>, <<"content-length: 0">>}
defp create_length(body) do
size = :erlang.integer_to_binary(:erlang.size(body))
{body, <<"content-length: ", size::binary, "\n">>}
end
Enum.each(@codes, fn({code, val}) ->
string_v = Integer.to_string(code)
atom_v = String.to_atom(string_v)
defp make_code(unquote(code)), do: <<unquote(string_v)::binary, " ", unquote(val)::binary>>
defp make_code(unquote(string_v)), do: <<unquote(string_v)::binary, " ", unquote(val)::binary>>
defp make_code(unquote(atom_v)), do: <<unquote(string_v)::binary, " ", unquote(val)::binary>>
end)
def error_resp(body \\ "Internal Server Error") do
%__MODULE__{code: 500, headers: [{<<"content-type">>, <<"text/html">>}], body: body}
end
def not_found(body \\ "Not Found") do
%__MODULE__{code: 404, headers: [{<<"content-type">>, <<"text/html">>}], body: body}
end
end
defmodule Test.Router do
use Satellite.Routing
route "get", "/", Test.Controller, :test
route "get", "/:any/oi/:some", Test.Controller, :test2, "*"
route "get", "*", Test.Controller, :test3
route "post", "/data", Test.Controller, :test4
end
defmodule Test.Controller do
def test(request) do
%Satellite.Response{body: "this is the root path, nothing to see here"}
|> Satellite.Response.make_resp()
end
def test2(request) do
%Satellite.Response{body: "#{inspect request}"}
|> Satellite.Response.make_resp()
end
def test3(request) do
%Satellite.Response{body: """
<html><body><h1>Wildcard match!</h1><br><br><div style="color: red;">#{inspect request}</div></body></html>
"""}
|> Satellite.Response.make_resp()
end
def test4(%{body: parsed_body} = request) do
response_body = "Parsed: " <> Jason.encode!(parsed_body) <> "\n"
%Satellite.Response{body: response_body, headers: [{<<"content-type">>, <<"application/json">>}]}
|> Satellite.Response.make_resp()
end
end
defmodule Satellite.Defaults do
def default_pipeline() do
[
{:read, :verb, &Satellite.Verb.parse/3, <<>>},
{:read, :path, &Satellite.Path.parse/3, {false, [], <<>>}},
{:read, :conn, &Satellite.Conn.parse/3, <<>>},
{:read, :headers, &Satellite.Headers.parse/3, {:header, <<>>}},
{:noread, :host, &Satellite.Host.set/2, nil},
{:noread, :accept, &Satellite.Accept.set/2, nil},
{:check, :check_request_type, &Satellite.Check.check/2, nil},
{:read, :body, &Satellite.Body.parse/3, {0, <<>>}}
]
end
end
defmodule Satellite.Body do
def parse(%{verb: :post, headers: headers} = request, rem, {count, acc}) do
n_size = count + :erlang.size(rem)
case headers do
%{<<"content-length">> => 0} -> {:done, request, <<>>}
%{<<"content-length">> => ^n_size} ->
case parse_content(headers, <<acc::binary, rem::binary>>) do
{:ok, decoded} -> {:done, %{request | body: decoded}, <<>>}
{:error, error} -> {:error, error}
end
_ ->
{:cont, request, {n_size, <<acc::bits, rem::bits>>}}
end
end
def parse(request, _, _), do: {:done, request, <<>>}
def parse_content(%{<<"content-type">> => <<"application/json">>}, rem) do
Jason.decode(rem)
catch e -> {:error, e}
end
end
defp deps do
[
{:satellite_shared, in_umbrella: true},
{:jason, "~> 1.0"}
]
end
defmodule Router do
use Satellite.Routing
route "get", "/", Controller, :test
route "get", "/:any/oi/:some", Controller, :test2, "*"
route "get", "*", Controller, :test3
route "post", "/data", Controller, :test4
end
defmodule Controller do
def test(request) do
%Satellite.Response{body: "#{inspect request}"}
|> Satellite.Response.make_resp()
end
def test2(request) do
%Satellite.Response{body: "#{inspect request}"}
|> Satellite.Response.make_resp()
end
def test3(request) do
%Satellite.Response{body: """
<html><body><h1>Wildcard match!</h1><br><br><div style="color: red;">#{inspect request}</div></body></html>
"""}
|> Satellite.Response.make_resp()
end
def test4(%{body: parsed_body}) do
response_body = "Parsed: " <> Jason.encode!(parsed_body) <> "\n"
%Satellite.Response{body: response_body, headers: [{<<"content-type">>, <<"application/json">>}]}
|> Satellite.Response.make_resp()
end
end
defp deps do
[
{:satellitex, path: "../satellitex"}
]
end
defmodule TestSatellitex.Application do
@moduledoc false
use Application
def start(_type, _args) do
children = [
Supervisor.child_spec(%{id: Server1, start: {Launchpad, :start_link, [%Satellite.Configuration{router: Router}]}}, type: :worker)
]
opts = [strategy: :one_for_one, name: TestSatellitex.Supervisor]
Supervisor.start_link(children, opts)
end
end
# change
def handle_event(:internal, :read, :parsing, %{conn: conn} = data) do
case :gen_tcp.recv(conn, 0, 1000) do
{:ok, packet} ->
IO.inspect(packet, label: "received")
n_data = %{data | request: packet}
{:next_state, :response, n_data, [{:next_event, :internal, :send_response}]}
{:error, reason} ->
IO.inspect(reason, label: "error on recv")
{:keep_state, data, [{:next_event, :internal, :close}]}
end
end
# to
def handle_event(:internal, :read, :parsing, %{conn: conn, config: %{number: number}} = data) do
case :gen_tcp.recv(conn, 0, 1000) do
{:ok, packet} ->
IO.inspect(packet, label: "received")
n_data = %{data | request: packet <> "\n\nAcceptor number: #{number}"}
{:next_state, :response, n_data, [{:next_event, :internal, :send_response}]}
{:error, reason} ->
IO.inspect(reason, label: "error on recv")
{:keep_state, data, [{:next_event, :internal, :close}]}
end
end
defmodule Satellite.Request do
defstruct [
:verb,
:protocol,
:version,
:accept,
:body,
path: [],
host: [],
query: %{},
headers: %{},
finished_headers: false,
halt: false,
params: %{},
]
end
defmodule Satellite.Configuration do
defstruct [
:pipeline,
:name,
:router,
port: 4000,
acceptors: 5,
max_size: 50_000,
keep_full_request: false,
]
end
defp deps do
[
{:satellite, in_umbrella: true},
{:satellite_shared, in_umbrella: true}
]
end
defp deps do
[
{:satellite_shared, in_umbrella: true}
]
end
defmodule Satellite.Defaults do
def default_pipeline() do
[
{:read, :verb, &Satellite.Verb.parse/3, <<>>}
]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment