Skip to content

vans163/stargate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

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}.