Created
          September 23, 2019 21:47 
        
      - 
      
- 
        Save mnussbaumer/3f08a4c3dbd9d3cd252b8ad87a3a8d4f to your computer and use it in GitHub Desktop. 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | use Mix.Config | |
| import_config "../apps/*/config/config.exs" | |
| config :logger, | |
| handle_otp_reports: true, | |
| handle_sasl_reports: true | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defmodule Satellite.Defaults do | |
| def default_pipeline() do | |
| [ | |
| {:read, :verb, &Satellite.Verb.parse/3, <<>>}, | |
| {:read, :path, &Satellite.Path.parse/3, {false, [], <<>>}} | |
| ] | |
| end | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defmodule Test.Router do | |
| use Satellite.Routing | |
| route "get", "/some/:path", AnotherModule, :a_function | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defp deps do | |
| [ | |
| {:satellite_shared, in_umbrella: true}, | |
| {:jason, "~> 1.0"} | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defp deps do | |
| [ | |
| {:satellitex, path: "../satellitex"} | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | # 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 | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defmodule Satellite.Request do | |
| defstruct [ | |
| :verb, | |
| :protocol, | |
| :version, | |
| :accept, | |
| :body, | |
| path: [], | |
| host: [], | |
| query: %{}, | |
| headers: %{}, | |
| finished_headers: false, | |
| halt: false, | |
| params: %{}, | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defmodule Satellite.Configuration do | |
| defstruct [ | |
| :pipeline, | |
| :name, | |
| :router, | |
| port: 4000, | |
| acceptors: 5, | |
| max_size: 50_000, | |
| keep_full_request: false, | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defp deps do | |
| [ | |
| {:satellite, in_umbrella: true}, | |
| {:satellite_shared, in_umbrella: true} | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | defp deps do | |
| [ | |
| {:satellite_shared, in_umbrella: true} | |
| ] | |
| end | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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