vans163 / stargate

Erlang customizable webserver
GNU General Public License v3.0
21 stars 5 forks source link

stargate

Elixir fast and featured webserver

Status

Very fast and customizable.
No support for HTTP2.

Releases

These releases are breaking changes.

0.1-genserver

0.2-gen_statem

0.3-proc_lib

0.4-elixir

Current Features

Roadmap

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 ```

Example

Basic example ```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 ```erlang {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 ```erlang %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 ```erlang 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. ```erlang -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}. ``` ```javascript //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. ```erlang 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: ```html
  • '' end. %>'> Home Home
  • ``` You can now do: ```erlang 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: ```erlang stargate_plugin:template("{{(.*?)}}", HtmlBin, KeyValue). ```
    Streams API (binary streaming) Binary streaming for non-chunked encoding responses. ```erlang -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}. ```