Skip to content

Latest commit

 

History

History
397 lines (301 loc) · 10.3 KB

README.md

File metadata and controls

397 lines (301 loc) · 10.3 KB

stargate

Elixir fast and featured webserver

Status

Very fast and customizable.
No support for HTTP2.

Releases

These releases are breaking changes.

0.1-genserver

  • 1 acceptor
  • Ticking gen_server

0.2-gen_statem

  • R19.1+ only
  • OTP supervision trees
  • Tickless gen_statem
  • Multiple acceptors
  • Query and Headers on websocket connection

0.3-proc_lib

  • R19.2+ only
  • websocket connection becomes a gen_server process
  • keep_alive http connection currently under question
  • all headers normalized to lowercase binary
    • preparation for http/2

0.4-elixir

  • requires Elixir now
  • performance upgrades
  • headers as lists now
  • benchmarks
  • streaming bodies

Current Features

  • Simple support for HTTP
  • hot-loading new paths
  • GZIP
  • SSL
  • Streams API (Binary streaming)
  • Simple plugins
    • Templates
    • Static File Server
  • Websockets
    • Compression
    • gen_server behavior

Roadmap

  • half-closed sockets
  • HTTP/2 ** Postponed until Websockets/other raw streaming is supported
  • QUIC ** Postponed until Websockets/other raw streaming is supported

Benchmarks

Thinness

Stargate is currently 1144 lines of code ``` git ls-files | grep -P ".*(erl|hrl)" | xargs wc -l

43 src/app/acceptor/stargate_acceptor_gen.erl 25 src/app/acceptor/stargate_acceptor_sup.erl 8 src/app/stargate_app.erl 69 src/app/stargate_child_gen.erl 25 src/app/stargate_sup.erl 6 src/handler/stargate_handler_redirect_https.erl 11 src/handler/stargate_handler_wildcard.erl 39 src/handler/stargate_handler_wildcard_ws.erl 21 src/plugin/stargate_plugin.erl 88 src/plugin/stargate_static_file.erl 96 src/plugin/stargate_template.erl 172 src/proto/stargate_proto_http.erl 162 src/proto/stargate_proto_ws.erl 103 src/stargate.erl 16 src/stargate_transport.erl 260 src/stargate_vessel.erl

1144 total

</details> 
 

### Example
<details>
<summary>Basic example</summary>
```erlang

%Listen on all interfaces for any non-ssl request /w websocket on port 8000
% SSL requests on port 8443  ./priv/cert.pem   ./priv/key.pem  

stargate:launch_demo().
Live configuration example
{ok, _} = application:ensure_all_started(stargate),

{ok, HttpPid} = stargate:warp_in(
  #{
      port=> 80, 
      ip=> {0,0,0,0},
      listen_args=> [{nodelay, false}],
      hosts=> #{
          {http, "public.templar-archive.aiur"}=> {templar_archive_public, #{}},
          {http, "*"}=> {handler_redirect_https, #{}},
      }
  }
),

WSCompress = #{window_bits=> 15, level=>best_speed, mem_level=>8, strategy=>default},
{ok, HttpsPid} = stargate:warp_in(
  #{
      port=> 443,
      ip=> {0,0,0,0},
      listen_args=> [{nodelay, false}],
      ssl_opts=> [
          {certfile, "./priv/lets-encrypt-cert.pem"},
          {keyfile, "./priv/lets-encrypt-key.pem"},

          {cacertfile, "./priv/lets-encrypt-x3-cross-signed.pem"}
      ],
      hosts=> #{
          {http, "templar-archive.aiur"}=> {templar_archive, #{}},
          {http, "www.templar-archive.aiur"}=> {templar_archive, #{}},

          {http, "research.templar-archive.aiur"}=> {templar_archive_research, #{}},

          {ws, {"ws.templar-archive.aiur", "/emitter"}}=> 
              {ws_emitter, #{compress=> WSCompress}},
          {ws, {"ws.templar-archive.aiur", "/transmission"}}=> 
              {ws_transmission, #{compress=> WSCompress}}
      }
  }
).

-module(templar_archive_public).
-compile(export_all).

http('GET', Path, Query, Headers, Body, S) ->
    stargate_plugin:serve_static(<<"./priv/public/">>, Path, Headers, S).


-module(templar_archive).
-compile(export_all).

http('GET', <<"/">>, Query, Headers, Body, S) ->
    Socket = maps:get(socket, S),
    {ok, {SourceAddr, _}} = ?TRANSPORT_PEERNAME(Socket),

    SourceIp = unicode:characters_to_binary(inet:ntoa(SourceAddr)),
    Resp =  <<"Welcome to the templar archives ", SourceIp/binary>>,
    {200, #{}, Resp, S}
    .


-module(templar_archive_research).
-compile(export_all).

http('GET', Path, Query, #{'Cookie':= <<"power_overwhelming">>}, Body, S) ->
    stargate_plugin:serve_static(<<"./priv/research/">>, Path, Headers, S);

http('GET', Path, Query, Headers, Body, S) ->
    Resp =  <<"Access Denied">>,
    {200, #{}, Resp, S}.


-module(ws_emitter).
-behavior(gen_server).
-compile(export_all).

handle_cast(_Message, S) -> {noreply, S}.
handle_call(_Message, _From, S) -> {reply, ok, S}.
code_change(_OldVersion, S, _Extra) -> {ok, S}. 

start_link(Params) -> gen_server:start_link(?MODULE, Params, []).

init({ParentPid, Query, Headers, State}) ->
    %If we dont trap_exit plus catch 'EXIT' we cant have terminate called, up to you
    process_flag(trap_exit, true),

    {ok, State#{parent=> ParentPid}}.

terminate(Reason, _S) -> 
    io:format("~p:~n disconnect~n ~p~n", [?MODULE, Reason]).

handle_info({'EXIT', _, _Reason}, D) ->
    {stop, {shutdown, got_exit_signal}, D};



handle_info({text, Bin}, S=#{parent:= ParentPid}) ->
    ParentPid ! {ws_send, {bin, <<"hello">>}},
    ParentPid ! {ws_send, {bin_compress, <<"hello compressed">>}},
    {noreply, S};

handle_info({bin, Bin}, S) ->
    io:format("~p:~n Got bin~n ~p~n", [?MODULE, Bin]),
    ParentPid ! {ws_send, {text, <<"a websocket text msg">>}},
    ParentPid ! {ws_send, {text_compress, <<"a websocket text msg compressed">>}},
    {noreply, S};

handle_info(Message, S) -> 
    io:format("~p:~n Unhandled handle_info~n ~p~n ~p~n", [?MODULE, Message, S]),
    {noreply, S}.
Hotloading example
%Pid gotten from return value of warp_in/[1,2].

stargate:update_params(HttpsPid, #{
  hosts=> #{ 
      {http, <<"new_quarters.templar-archive.aiur">>}=> {new_quarters, #{}}
  }, 
  ssl_opts=> [
      {certfile, "./priv/new_cert.pem"},
      {keyfile, "./priv/new_key.pem"}
  ]
})
Gzip example
Headers = #{'Accept-Encoding'=> <<"gzip">>, <<"ETag">>=> <<"12345">>},
S = old_state,
{ReplyCode, ReplyHeaders, ReplyBody, NewState} = 
    stargate_plugin:serve_static(<<"./priv/website/">>, <<"index.html">>, Headers, S),

ReplyCode = 200,
ReplyHeaders = #{<<"Content-Encoding">>=> <<"gzip">>, <<"ETag">>=> <<"54321">>},
Websockets example

Keep-alives are sent from server automatically
Defaults are in global.hrl
Max sizes protect vs DDOS

Keep in mind that encoding/decoding json + websocket frames produces alot of eheap_allocs; fragmenting the process heap beyond possible GC cleanup. Make sure to do these operations inside the stargate_vessel process itself or a temporary process. You greatly risk crashing the entire beam VM otherwise due to it not being able to allocate anymore eheap.

Using max_heap_size erl vm arg can somewhat remedy this problem.

-module(ws_transmission).
-behavior(gen_server).
-compile(export_all).

handle_cast(_Message, S) -> {noreply, S}.
handle_call(_Message, _From, S) -> {reply, ok, S}.
code_change(_OldVersion, S, _Extra) -> {ok, S}. 

start_link(Params) -> gen_server:start_link(?MODULE, Params, []).

init({ParentPid, Query, Headers, State}) ->
    %If we dont trap_exit plus catch 'EXIT' we cant have terminate called, up to you
    process_flag(trap_exit, true),

    Cookies = maps:get(<<"cookie">>, Headers, undefined),
    case Cookies of
        <<"token=mysecret">> -> {ok, State#{parent=> ParentPid}};
        _ -> ignore
    end.

terminate(Reason, _S) -> 
    io:format("~p:~n disconnect~n ~p~n", [?MODULE, Reason]).

handle_info({'EXIT', _, _Reason}, D) ->
    {stop, {shutdown, got_exit_signal}, D};



handle_info({text, Bin}, S=#{parent:= ParentPid}) ->
    ParentPid ! {ws_send, {bin, <<"hello">>}},
    ParentPid ! {ws_send, {bin_compress, <<"hello compressed">>}},
    {noreply, S};

handle_info({bin, Bin}, S) ->
    io:format("~p:~n Got bin~n ~p~n", [?MODULE, Bin]),
    ParentPid ! {ws_send, {text, "a websocket text list"}},
    ParentPid ! {ws_send, {text, <<"a websocket text bin">>}},
    ParentPid ! {ws_send, {text_compress, <<"a websocket text msg compressed">>}},
    {noreply, S};

handle_info(Message, S) -> 
    io:format("~p:~n Unhandled handle_info~n ~p~n ~p~n", [?MODULE, Message, S]),
    {noreply, S}.
//Chrome javascript WS example:
var socket = new WebSocket("ws://127.0.0.1:8000");
socket.send("Hello Mike");
Websockets inject_headers

Sometimes we need to send back custom headers in the handshake. We can now add an inject_headers param (which is a map) to the site definition.

NoVNCServer = #{
    port=> 5600, ip=> {0,0,0,0},
    hosts=> #{
        {ws, {"localhost:5000", "/websockify"}}=> {handler_panel_vnc, #{
            inject_headers=> #{<<"Sec-WebSocket-Protocol">>=> <<"binary">>}
        }}
    }
}
Cookie Parser example ```erlang Map = stargate_plugin:cookie_parse(<<"token=mysecret; other_stuff=some_other_thing">>) ```
Templating example

Basic templating system uses the default regex of "<%=(.*?)%>" to pull out captures from a binary.

For example writing html like:

<li class='my-nav-list <%= case :category of <<\"index\">>-> 'my-nav-list-active'; _-> '' end. %>'>
  <a href='/' class='link'>
    <span class='act'>Home</span>
    <span class='hov'>Home</span>
  </a>
</li>

You can now do:

KeyValue = #{category=> <<"index">>},
TransformedBin = stargate_plugin:template(HtmlBin, KeyValue).

The return is the evaluation of the expressions between the match with the :terms substituted.

You may pass your own regex to match against using stargate_plugin:template/3:

stargate_plugin:template("{{(.*?)}}", HtmlBin, KeyValue).
Streams API (binary streaming)

Binary streaming for non-chunked encoding responses.

-module(http_handler_stream).
-compile(export_all).

close_stream(Pid) ->
    Pid ! close_connection.

ticker(Pid) ->
      timer:sleep(1000),
      Pid ! {send_chunk, <<"hi">>},
      ticker(Pid).

http('GET', <<"/stream">>, _Query, _Headers, _Body, S) ->
      io:format("Streaming.. ~p ~p ~n", [S, self()]),
      spawn_link(http_handler_stream, ticker, [self()]),
      {200, #{}, stream, S}.