Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit d8043d67d7b4faca3cac1faea779b0411718901d 0 parents
@maxlapshin maxlapshin authored
2  .gitignore
@@ -0,0 +1,2 @@
+ebin
+erl_crash.dump
12 Makefile
@@ -0,0 +1,12 @@
+
+test:
+ ERL_LIBS=../../deps rebar compile
+ erl -pa ebin -noshell -eval 'io:format("~p~n", [http_router_config:file("../../priv/flussonic.conf")])' -s init stop
+
+tests:
+ erl -pa ebin -pa ../../deps/proper/ebin -noshell -eval 'io:format("~p~n", [routes_proper_tests:prop_tests()])' -s init stop
+
+raw:
+ ERL_LIBS=../../deps rebar compile
+ erl -pa ebin -noshell -eval 'io:format("~p~n", [http_router_parser:file("../../priv/flussonic.conf")])' -s init stop
+
40 README.md
@@ -0,0 +1,40 @@
+HTTP Router
+===========
+
+
+HTTP Router is an application that read nginx-like config file from specified file, tries to reload it regularly
+and process HTTP request from the beginning to the latest module.
+
+Currently there is a Cowboy handler:
+
+
+```erlang
+application:load(http_router),
+application:set_env(http_router, config_path, "priv/flussonic.conf"),
+application:set_env(http_router, frequency, 1000),
+application:start(http_router),
+application:start(cowboy),
+
+Dispatch = [
+ {'_', [
+ {['...'], http_router_handler, []}
+ ]}
+],
+cowboy:start_listener(http, 100,
+ cowboy_tcp_transport, [{port, 8080}],
+ cowboy_http_protocol, [{dispatch, Dispatch}]
+),
+
+```
+
+Now take a look at self-explanatory priv/routes.conf
+
+
+You can add your own handlers with command:
+
+```
+handler module function;
+```
+
+and you will be called as module:function(Req, Env) where Req is your webserver request (currently cowboy) and
+Env is a proplist with some special variables like regexp matched entries from location.
69 priv/routes.conf
@@ -0,0 +1,69 @@
+location(rtsp) /cam/rtsp:/(:rtsp_path) (rtsp_path~.+){
+ rewrite /cam/rtsp:/(.*) rtsp://$1;
+ rtsp;
+}
+
+location /cam0 { rtsp rtsp://admin:admin@192.168.0.55/h264; }
+
+location /vod (!session) {
+ rack /var/www/site/video/config.ru;
+}
+
+location /vod (session) {
+ rewrite /vod/(.*) /movies/$1;
+}
+
+location /movies (internal) {
+ rewrite /movies/(.*) $1;
+
+ root wwwroot;
+ file;
+ root /Users/max/Movies;
+
+ include @hds;
+ include @hls;
+ file;
+}
+
+location /live {
+ include hds.conf;
+ include hls.conf;
+}
+
+location /flu {
+ root wwwroot;
+ file;
+}
+
+root /home/www/railsapp/public;
+file;
+
+rack /home/www/railsapp;
+
+
+section hds {
+
+location(hds_manifest) (:path)/manifest.f4m (path~.+) {
+ hds_manifest;
+}
+
+location (:path)/(:bitrate)/Seg(:segment)-Frag(:fragment) (bitrate~\d+;fragment~\d+;segment~\d+;path~.+) {
+ hds_segment;
+}
+
+
+}
+
+section hls {
+location (:path)/index.m3u8 (path~.+){
+ handler hls manifest $path;
+}
+
+location (:path)/(:bitrate)/index.m3u8 (path~.+;bitrate~\d+) {
+ handler hls manifest $path $bitrate;
+}
+
+location (:path)/(:bitrate)/segment(:segment).ts (path~.+;bitrate~\d+;segment~\d+) {
+ handler hls segment $path $bitrate $segment;
+}
+}
1  src/.gitignore
@@ -0,0 +1 @@
+http_router_parser.erl
268 src/dynamic_compile.erl
@@ -0,0 +1,268 @@
+%% Copyright (c) 2007
+%% Mats Cronqvist <mats.cronqvist@ericsson.com>
+%% Chris Newcombe <chris.newcombe@gmail.com>
+%% Jacob Vorreuter <jacob.vorreuter@gmail.com>
+%%
+%% Permission is hereby granted, free of charge, to any person
+%% obtaining a copy of this software and associated documentation
+%% files (the "Software"), to deal in the Software without
+%% restriction, including without limitation the rights to use,
+%% copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the
+%% Software is furnished to do so, subject to the following
+%% conditions:
+%%
+%% The above copyright notice and this permission notice shall be
+%% included in all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+%% OTHER DEALINGS IN THE SOFTWARE.
+
+%%%-------------------------------------------------------------------
+%%% File : dynamic_compile.erl
+%%% Description :
+%%% Authors : Mats Cronqvist <mats.cronqvist@ericsson.com>
+%%% Chris Newcombe <chris.newcombe@gmail.com>
+%%% Jacob Vorreuter <jacob.vorreuter@gmail.com>
+%%% TODO :
+%%% - add support for limit include-file depth (and prevent circular references)
+%%% prevent circular macro expansion set FILE correctly when -module() is found
+%%% -include_lib support $ENVVAR in include filenames
+%%% substitute-stringize (??MACRO)
+%%% -undef/-ifdef/-ifndef/-else/-endif
+%%% -file(File, Line)
+%%%-------------------------------------------------------------------
+-module(dynamic_compile).
+
+%% API
+-export([from_string/1, from_string/2]).
+
+-import(lists, [reverse/1, keyreplace/4]).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function:
+%% Description:
+%% Returns a binary that can be used with
+%% code:load_binary(Module, ModuleFilenameForInternalRecords, Binary).
+%%--------------------------------------------------------------------
+from_string(CodeStr) ->
+ from_string(CodeStr, []).
+
+% takes Options as for compile:forms/2
+from_string(CodeStr, CompileFormsOptions) ->
+ %% Initialise the macro dictionary with the default predefined macros,
+ %% (adapted from epp.erl:predef_macros/1
+ Filename = "compiled_from_string",
+ %%Machine = list_to_atom(erlang:system_info(machine)),
+ Ms0 = dict:new(),
+ % Ms1 = dict:store('FILE', {[], "compiled_from_string"}, Ms0),
+ % Ms2 = dict:store('LINE', {[], 1}, Ms1), % actually we might add special code for this
+ % Ms3 = dict:store('MODULE', {[], undefined}, Ms2),
+ % Ms4 = dict:store('MODULE_STRING', {[], undefined}, Ms3),
+ % Ms5 = dict:store('MACHINE', {[], Machine}, Ms4),
+ % InitMD = dict:store(Machine, {[], true}, Ms5),
+ InitMD = Ms0,
+
+ %% From the docs for compile:forms:
+ %% When encountering an -include or -include_dir directive, the compiler searches for header files in the following directories:
+ %% 1. ".", the current working directory of the file server;
+ %% 2. the base name of the compiled file;
+ %% 3. the directories specified using the i option. The directory specified last is searched first.
+ %% In this case, #2 is meaningless.
+ IncludeSearchPath = ["." | reverse([Dir || {i, Dir} <- CompileFormsOptions])],
+ {RevForms, _OutMacroDict} = scan_and_parse(CodeStr, Filename, 1, [], InitMD, IncludeSearchPath),
+ Forms = reverse(RevForms),
+
+ %% note: 'binary' is forced as an implicit option, whether it is provided or not.
+ case compile:forms(Forms, CompileFormsOptions) of
+ {ok, ModuleName, CompiledCodeBinary} when is_binary(CompiledCodeBinary) ->
+ {ModuleName, CompiledCodeBinary};
+ {ok, ModuleName, CompiledCodeBinary, []} when is_binary(CompiledCodeBinary) -> % empty warnings list
+ {ModuleName, CompiledCodeBinary};
+ {ok, _ModuleName, _CompiledCodeBinary, Warnings} ->
+ throw({?MODULE, warnings, Warnings});
+ Other ->
+ throw({?MODULE, compile_forms, Other})
+ end.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+%%% Code from Mats Cronqvist
+%%% See http://www.erlang.org/pipermail/erlang-questions/2007-March/025507.html
+%%%## 'scan_and_parse'
+%%%
+%%% basically we call the OTP scanner and parser (erl_scan and
+%%% erl_parse) line-by-line, but check each scanned line for (or
+%%% definitions of) macros before parsing.
+%% returns {ReverseForms, FinalMacroDict}
+scan_and_parse([], _CurrFilename, _CurrLine, RevForms, MacroDict, _IncludeSearchPath) ->
+ {RevForms, MacroDict};
+
+scan_and_parse(RemainingText, CurrFilename, CurrLine, RevForms, MacroDict, IncludeSearchPath) ->
+ case scanner(RemainingText, CurrLine, MacroDict) of
+ {tokens, NLine, NRemainingText, Toks} ->
+ {ok, Form} = erl_parse:parse_form(Toks),
+ scan_and_parse(NRemainingText, CurrFilename, NLine, [Form | RevForms], MacroDict, IncludeSearchPath);
+ {macro, NLine, NRemainingText, NMacroDict} ->
+ scan_and_parse(NRemainingText, CurrFilename, NLine, RevForms,NMacroDict, IncludeSearchPath);
+ {include, NLine, NRemainingText, IncludeFilename} ->
+ IncludeFileRemainingTextents = read_include_file(IncludeFilename, IncludeSearchPath),
+ %%io:format("include file ~p contents: ~n~p~nRemainingText = ~p~n", [IncludeFilename,IncludeFileRemainingTextents, RemainingText]),
+ %% Modify the FILE macro to reflect the filename
+ %%IncludeMacroDict = dict:store('FILE', {[],IncludeFilename}, MacroDict),
+ IncludeMacroDict = MacroDict,
+
+ %% Process the header file (inc. any nested header files)
+ {RevIncludeForms, IncludedMacroDict} = scan_and_parse(IncludeFileRemainingTextents, IncludeFilename, 1, [], IncludeMacroDict, IncludeSearchPath),
+ %io:format("include file results = ~p~n", [R]),
+ %% Restore the FILE macro in the NEW MacroDict (so we keep any macros defined in the header file)
+ %%NMacroDict = dict:store('FILE', {[],CurrFilename}, IncludedMacroDict),
+ NMacroDict = IncludedMacroDict,
+
+ %% Continue with the original file
+ scan_and_parse(NRemainingText, CurrFilename, NLine, RevIncludeForms ++ RevForms, NMacroDict, IncludeSearchPath);
+ done ->
+ scan_and_parse([], CurrFilename, CurrLine, RevForms, MacroDict, IncludeSearchPath)
+ end.
+
+scanner(Text, Line, MacroDict) ->
+ case erl_scan:tokens([],Text,Line) of
+ {done, {ok,Toks,NLine}, LeftOverChars} ->
+ case pre_proc(Toks, MacroDict) of
+ {tokens, NToks} -> {tokens, NLine, LeftOverChars, NToks};
+ {macro, NMacroDict} -> {macro, NLine, LeftOverChars, NMacroDict};
+ {include, Filename} -> {include, NLine, LeftOverChars, Filename}
+ end;
+ {more, _Continuation} ->
+ %% This is supposed to mean "term is not yet complete" (i.e. a '.' has
+ %% not been reached yet).
+ %% However, for some bizarre reason we also get this if there is a comment after the final '.' in a file.
+ %% So we check to see if Text only consists of comments.
+ case is_only_comments(Text) of
+ true ->
+ done;
+ false ->
+ throw({incomplete_term, Text, Line})
+ end
+ end.
+
+is_only_comments(Text) -> is_only_comments(Text, not_in_comment).
+
+is_only_comments([], _) -> true;
+is_only_comments([$ |T], not_in_comment) -> is_only_comments(T, not_in_comment); % skipping whitspace outside of comment
+is_only_comments([$\t |T], not_in_comment) -> is_only_comments(T, not_in_comment); % skipping whitspace outside of comment
+is_only_comments([$\n |T], not_in_comment) -> is_only_comments(T, not_in_comment); % skipping whitspace outside of comment
+is_only_comments([$% |T], not_in_comment) -> is_only_comments(T, in_comment); % found start of a comment
+is_only_comments(_, not_in_comment) -> false;
+% found any significant char NOT in a comment
+is_only_comments([$\n |T], in_comment) -> is_only_comments(T, not_in_comment); % found end of a comment
+is_only_comments([_ |T], in_comment) -> is_only_comments(T, in_comment). % skipping over in-comment chars
+
+%%%## 'pre-proc'
+%%%
+%%% have to implement a subset of the pre-processor, since epp insists
+%%% on running on a file.
+%%% only handles 2 cases;
+%% -define(MACRO, something).
+%% -define(MACRO(VAR1,VARN),{stuff,VAR1,more,stuff,VARN,extra,stuff}).
+pre_proc([{'-',_},{atom,_,define},{'(',_},{_,_,Name}|DefToks],MacroDict) ->
+ false = dict:is_key(Name, MacroDict),
+ case DefToks of
+ [{',',_} | Macro] ->
+ {macro, dict:store(Name, {[], macro_body_def(Macro, [])}, MacroDict)};
+ [{'(',_} | Macro] ->
+ {macro, dict:store(Name, macro_params_body_def(Macro, []), MacroDict)}
+ end;
+
+pre_proc([{'-',_}, {atom,_,include}, {'(',_}, {string,_,Filename}, {')',_}, {dot,_}], _MacroDict) ->
+ {include, Filename};
+
+pre_proc(Toks,MacroDict) ->
+ {tokens, subst_macros(Toks, MacroDict)}.
+
+macro_params_body_def([{')',_},{',',_} | Toks], RevParams) ->
+ {reverse(RevParams), macro_body_def(Toks, [])};
+macro_params_body_def([{var,_,Param} | Toks], RevParams) ->
+ macro_params_body_def(Toks, [Param | RevParams]);
+macro_params_body_def([{',',_}, {var,_,Param} | Toks], RevParams) ->
+ macro_params_body_def(Toks, [Param | RevParams]).
+
+macro_body_def([{')',_}, {dot,_}], RevMacroBodyToks) ->
+ reverse(RevMacroBodyToks);
+macro_body_def([Tok|Toks], RevMacroBodyToks) ->
+ macro_body_def(Toks, [Tok | RevMacroBodyToks]).
+
+subst_macros(Toks, MacroDict) ->
+ reverse(subst_macros_rev(Toks, MacroDict, [])).
+
+%% returns a reversed list of tokes
+subst_macros_rev([{'?',_}, {_,LineNum,'LINE'} | Toks], MacroDict, RevOutToks) ->
+ %% special-case for ?LINE, to avoid creating a new MacroDict for every line in the source file
+ subst_macros_rev(Toks, MacroDict, [{integer,LineNum,LineNum}] ++ RevOutToks);
+
+subst_macros_rev([{'?',_}, {_,_,Name}, {'(',_} = Paren | Toks], MacroDict, RevOutToks) ->
+ case dict:fetch(Name, MacroDict) of
+ {[], MacroValue} ->
+ %% This macro does not have any vars, so ignore the fact that the invocation is followed by "(...stuff"
+ %% Recursively expand any macro calls inside this macro's value
+ %% TODO: avoid infinite expansion due to circular references (even indirect ones)
+ RevExpandedOtherMacrosToks = subst_macros_rev(MacroValue, MacroDict, []),
+ subst_macros_rev([Paren|Toks], MacroDict, RevExpandedOtherMacrosToks ++ RevOutToks);
+ ParamsAndBody ->
+ %% This macro does have vars.
+ %% Collect all of the passe arguments, in an ordered list
+ {NToks, Arguments} = subst_macros_get_args(Toks, []),
+ %% Expand the varibles
+ ExpandedParamsToks = subst_macros_subst_args_for_vars(ParamsAndBody, Arguments),
+ %% Recursively expand any macro calls inside this macro's value
+ %% TODO: avoid infinite expansion due to circular references (even indirect ones)
+ RevExpandedOtherMacrosToks = subst_macros_rev(ExpandedParamsToks, MacroDict, []),
+ subst_macros_rev(NToks, MacroDict, RevExpandedOtherMacrosToks ++ RevOutToks)
+ end;
+
+subst_macros_rev([{'?',_}, {_,_,Name} | Toks], MacroDict, RevOutToks) ->
+ %% This macro invocation does not have arguments.
+ %% Therefore the definition should not have parameters
+ {[], MacroValue} = dict:fetch(Name, MacroDict),
+
+ %% Recursively expand any macro calls inside this macro's value
+ %% TODO: avoid infinite expansion due to circular references (even indirect ones)
+ RevExpandedOtherMacrosToks = subst_macros_rev(MacroValue, MacroDict, []),
+ subst_macros_rev(Toks, MacroDict, RevExpandedOtherMacrosToks ++ RevOutToks);
+
+subst_macros_rev([Tok|Toks], MacroDict, RevOutToks) ->
+subst_macros_rev(Toks, MacroDict, [Tok|RevOutToks]);
+subst_macros_rev([], _MacroDict, RevOutToks) -> RevOutToks.
+
+subst_macros_get_args([{')',_} | Toks], RevArgs) ->
+ {Toks, reverse(RevArgs)};
+subst_macros_get_args([{',',_}, {var,_,ArgName} | Toks], RevArgs) ->
+ subst_macros_get_args(Toks, [ArgName| RevArgs]);
+subst_macros_get_args([{var,_,ArgName} | Toks], RevArgs) ->
+ subst_macros_get_args(Toks, [ArgName | RevArgs]).
+
+subst_macros_subst_args_for_vars({[], BodyToks}, []) ->
+ BodyToks;
+subst_macros_subst_args_for_vars({[Param | Params], BodyToks}, [Arg|Args]) ->
+ NBodyToks = keyreplace(Param, 3, BodyToks, {var,1,Arg}),
+ subst_macros_subst_args_for_vars({Params, NBodyToks}, Args).
+
+read_include_file(Filename, IncludeSearchPath) ->
+ case file:path_open(IncludeSearchPath, Filename, [read, raw, binary]) of
+ {ok, IoDevice, FullName} ->
+ {ok, Data} = file:read(IoDevice, filelib:file_size(FullName)),
+ file:close(IoDevice),
+ binary_to_list(Data);
+ {error, Reason} ->
+ throw({failed_to_read_include_file, Reason, Filename, IncludeSearchPath})
+ end.
15 src/http_router.app.src
@@ -0,0 +1,15 @@
+{application, http_router,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {mod, { http_router_app, []}},
+ {env, [
+ {config_path, "priv/routes.conf"},
+ {frequency, 3000}
+ ]}
+ ]}.
16 src/http_router_app.erl
@@ -0,0 +1,16 @@
+-module(http_router_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ http_router_sup:start_link().
+
+stop(_State) ->
+ ok.
127 src/http_router_compiler.erl
@@ -0,0 +1,127 @@
+-module(http_router_compiler).
+-author('Max Lapshin <max@maxidoors.ru>').
+-include_lib("kernel/include/file.hrl").
+-include("log.hrl").
+
+-export([generate_and_compile/1, generate_router/1]).
+-export([ensure_loaded/1, check/1]).
+
+
+check(Path) ->
+ {ok, Module} = ensure_loaded(Path),
+ {ok, #file_info{mtime = MTime}} = file:read_file_info(Path),
+ CTime = Module:ctime(),
+ if
+ CTime < MTime ->
+ ?D({reload,Path,CTime,MTime}),
+ code:soft_purge(Module),
+ generate_and_compile(Path);
+ true ->
+ ok
+ end.
+
+ensure_loaded(Path) ->
+ case erlang:module_loaded(http_router) of
+ true -> {ok, http_router};
+ false -> generate_and_compile(Path)
+ end.
+
+
+generate_and_compile(ConfigPath) ->
+ {ok, Code} = generate_router(ConfigPath),
+ {ok, _} = compile_router(http_router, Code).
+
+
+generate_router(ConfigPath) ->
+ Config = http_router_config:file(ConfigPath),
+ {ok, #file_info{mtime = MTime}} = file:read_file_info(ConfigPath),
+ {ok, Code, _Index} = translate_commands(Config),
+ Module = [
+ "-module(http_router).\n",
+ "-export([handle/2, ctime/0]).\n\n",
+ "ctime() -> ", io_lib:format("~p", [MTime]), ".\n\n",
+ "handle(Env, Req) -> \n",
+ " handle0(Env, Req).\n\n",
+ "handle0(Env0, Req0) -> \n",
+ Code
+ ],
+ {ok, iolist_to_binary(Module)}.
+
+
+compile_router(Module, Code) ->
+ Path = lists:flatten(io_lib:format("~s.erl", [Module])),
+ {ModName, Bin} = dynamic_compile:from_string(binary_to_list(Code), [report,verbose]),
+ {module, ModName} = code:load_binary(ModName, Path, Bin),
+ {ok, ModName}.
+
+
+translate_commands(Config) ->
+ translate_commands(Config, 0, 0, 0, []).
+
+translate_commands([{location, _Name, {Re, Keys}, _Flags, LocationBody}|Rest], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ RegexKeys = io_lib:format("~p", [[0|Keys]]),
+ LocationName = io_lib:format("location~p", [FunIdx+1]),
+ NextStep = io_lib:format("location~p", [FunIdx+2]),
+ ReS = io_lib:format("~p", [Re]),
+ Code = [
+ " case re:run(proplists:get_value(path,Env", integer_to_list(EnvIdx), "), ", ReS,", [{capture,", RegexKeys, ",binary}]) of\n",
+
+ case Keys of
+ [] ->
+ [" {match, _} -> \n",
+ " ",LocationName,"(Env", integer_to_list(EnvIdx), ", Req", integer_to_list(ReqIdx), ");\n"];
+ _ ->
+ [" {match, [_MatchedURL|Values]} -> \n",
+ io_lib:format(" Env~p = lists:ukeymerge(1, lists:ukeysort(1,lists:zip(~240p, Values)), Env~p),\n", [EnvIdx+1, Keys, EnvIdx]),
+ " ",LocationName,"(Env", integer_to_list(EnvIdx+1), ", Req", integer_to_list(ReqIdx), ");\n"]
+ end,
+ " nomatch ->
+ ", NextStep, "(Env", integer_to_list(EnvIdx), ", Req", integer_to_list(ReqIdx), ")\n",
+ " end.\n\n",
+
+ LocationName, "(Env0,Req0) -> \n"
+ ],
+
+ {ok, LocationCode, NewFunIdx} = translate_commands(LocationBody, FunIdx+2, 0, 0, []),
+
+ Code1 = [NextStep, "(Env0, Req0) -> \n"],
+
+ translate_commands(Rest, NewFunIdx, 0, 0, Acc ++ Code ++ LocationCode ++ Code1);
+
+translate_commands([{rewrite, Re, Replacement}|Rest], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ Code = [
+ " Env", integer_to_list(EnvIdx+1), " = lists:keyreplace(path, 1, Env", integer_to_list(EnvIdx), ", {path, ",
+ io_lib:format("re:replace(proplists:get_value(path, Env~p), ~p, ~p, [{return, binary}])", [EnvIdx, Re, Replacement]),
+ "}),\n"
+ ],
+ translate_commands(Rest, FunIdx, ReqIdx, EnvIdx+1, Acc ++ Code);
+
+translate_commands([{set, Key, Value}|Rest], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ Code = [
+ io_lib:format(" Env~p = lists:ukeymerge(1, [{~p,~p}], Env~p),\n", [EnvIdx+1, Key, Value, EnvIdx])
+ ],
+ translate_commands(Rest, FunIdx, ReqIdx, EnvIdx+1, Acc ++ Code);
+
+translate_commands([{handler, M, F, A}|Rest], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ Args = case A of
+ [] -> "";
+ _ -> "," ++ string:join([lists:flatten(io_lib:format("~p", [Arg])) || Arg <- A], ", ")
+ end,
+ Code = [
+ io_lib:format(" case ~p:~p(Req~p, Env~p~s) of\n", [M, F, ReqIdx, EnvIdx, Args]),
+ io_lib:format(" {ok, Req~p} -> {ok, Req~p};\n", [ReqIdx+1, ReqIdx+1]),
+ io_lib:format(" unhandled -> handle~p(Env~p, Req~p);\n", [FunIdx+1, EnvIdx, ReqIdx]),
+ io_lib:format(" {unhandled, Env~p, Req~p} -> handle~p(Env~p, Req~p)\n", [EnvIdx+1, ReqIdx+1, FunIdx+1, EnvIdx+1, ReqIdx+1]),
+ " end.\n\n",
+
+ "handle", integer_to_list(FunIdx+1), "(Env0, Req0) -> \n"
+ ],
+ translate_commands(Rest, FunIdx+1, 0, 0, Acc ++ Code);
+
+translate_commands([_|Rest], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ translate_commands(Rest, FunIdx, ReqIdx, EnvIdx, Acc);
+
+translate_commands([], FunIdx, ReqIdx, EnvIdx, Acc) ->
+ Code = io_lib:format(" cowboy_http_req:reply(404, [], <<\"404 \", (proplists:get_value(path, Env~p))/binary, \" not found\\n\">>, Req~p).\n\n",
+ [EnvIdx, ReqIdx]),
+ {ok, Acc ++ Code, FunIdx+1}.
96 src/http_router_config.erl
@@ -0,0 +1,96 @@
+-module(http_router_config).
+-author('Max Lapshin <max@maxidoors.ru>').
+-include("log.hrl").
+
+-export([file/1]).
+
+
+file(Path) ->
+ Config = http_router_parser:file(Path),
+ parse_config(Config).
+ % Config.
+
+parse_config(Config) ->
+ Sections = [{Name, Value} || {section, Name, Value} <- Config],
+ lists:map(fun
+ ({location, Name, Path, Flags, Value}) ->
+ convert_location({location, Name, Path, Flags, parse_config(substitute_secion_includes(Value, Sections))});
+ ({rewrite, Re, Replacement}) ->
+ convert_rewrite(Re, Replacement);
+ ({root, Root}) ->
+ {set, root, Root};
+ (file) ->
+ {handler, static_file, send, []};
+ ({hds,Command}) ->
+ {handler, hds_handler, Command, []};
+ ({hls,Command}) ->
+ {handler, hls_handler, Command, []};
+ ({rack,Path}) ->
+ {handler, cowboy_rack_handler, handle, [Path]};
+ (Else) -> Else
+ end, Config).
+
+
+substitute_secion_includes(Commands, Sections) ->
+ lists:foldr(fun
+ ({include_section, Section}, Acc) -> proplists:get_value(Section, Sections) ++ Acc;
+ (Command, Acc) -> [Command|Acc]
+ end, [], Commands).
+
+
+convert_location({location, Name, Path, Flags, Instructions}) ->
+ Replacement = [{binary_to_atom(Key,latin1), Value} || {re, Key, Value} <- Flags],
+ Re = rewrite_route_entry(Path, Replacement),
+ ProperFlags = lists:filter(fun
+ ({re,_,_}) -> false;
+ (_) -> true
+ end, Flags),
+ {location, Name, Re, ProperFlags, Instructions}.
+
+
+convert_rewrite(Re, Replacement) ->
+ {rewrite, Re, re:replace(Replacement, "\\$(\\d+)", "\\\\\\1", [{return,binary}])}.
+
+
+rewrite_route_entry(URL, ExList) ->
+ {{ok,NewURL},VarList} = case re:run(URL,"\\(:[^\\)]*\\(") of
+ nomatch ->
+ case re:run(URL,"\\(*:([-_0-9a-zA-Z]+)\\)*",[{capture,all_but_first,list},global]) of
+ {match,Vars} -> {convert_to_pattern(URL,ExList), [list_to_atom(Var) || [Var] <- Vars]};
+ _ -> {convert_to_pattern(URL,ExList),[]}
+ end;
+ {match,_} ->
+ {{ok,"parsing_error"},[]}
+ end,
+ {NewURL, VarList}.
+
+convert_to_pattern(URL,ExList)->
+ Convert = fun() ->
+ Pattern1=exlist(URL,ExList),
+ case re:replace(Pattern1,"(\\(*:[-_a-zA-Z0-9]+\\)*)","([^/]+)",[global,{return,list}]) of
+ Value2 when is_list(Value2) -> {ok,Value2};
+ nomatch -> ""
+ end
+ end,
+ case re:run(URL,"(.*\\([^:]+)",[{capture,all_but_first,list},global]) of
+ nomatch ->
+ Convert();
+ {match,Error} ->
+ io:format("Route rule error near ~p~n",[Error]),
+ error
+ end.
+
+exlist(URL,[])->
+ URL;
+exlist(URL,[{Name,Pattern}|ExList]) ->
+ Re = lists:flatten(io_lib:format("\\(?:~s\\)?", [Name])),
+
+ Pattern1 = re:replace(Pattern, "\\\\", "\\\\\\\\", [{return,list}]),
+
+ Pat = lists:flatten(io_lib:format("(?<~s>~s)", [Name, Pattern1])),
+
+ List=re:replace(URL, Re, Pat,[{return,list}]),
+ % io:format("Replace ~p with ~p => ~p~n", [Re, Pat, List]),
+ exlist(List,ExList).
+
+
29 src/http_router_handler.erl
@@ -0,0 +1,29 @@
+-module(http_router_handler).
+-author('Max Lapshin <max@maxidoors.ru>').
+-include("log.hrl").
+
+-export([init/3, handle/2, terminate/2]).
+
+
+init({_Transport, http}, Req, _Options) ->
+ {ok, Req, state}.
+
+handle(Req1, State) ->
+
+ {Path, Req2} = cowboy_http_req:raw_path(Req1),
+ Env = [{path,Path}],
+ case http_router:handle(Env, Req2) of
+ {ok, Req3} -> {ok, Req3, State};
+ unhandled -> {ok, reply_404(Req2), State};
+ {unhandled, _Env, Req3} -> {ok, reply_404(Req3), State}
+ end.
+
+reply_404(Req) ->
+ {ok, Req1} = cowboy_http_req:reply(404, [], <<"404 not found\n">>, Req),
+ Req1.
+
+terminate(_,_) ->
+ ok.
+
+
+
110 src/http_router_parser.peg
@@ -0,0 +1,110 @@
+config <- entry_line* ~;
+
+section <- space? "section" space? name:string space? "{" space? locations:(location)* space? "}" space? `
+{section, proplists:get_value(name,Node), proplists:get_value(locations,Node)}
+`;
+
+location <- space? header:("location(" name:([^\)])+ ")" space? / "location" space?) path:string space? flags:location_flags "{" body:entries "}" space? `
+ Header = proplists:get_value(header,Node),
+ Name = case proplists:get_value(name,Header) of
+ undefined -> undefined;
+ Else -> binary_to_atom(iolist_to_binary(Else), latin1)
+ end,
+ {location, Name, proplists:get_value(path,Node), proplists:get_value(flags,Node), proplists:get_value(body,Node)}
+`;
+
+location_flags <- space? flags:flags space? / space? `
+ proplists:get_value(flags, Node, [])
+`;
+
+flags <- "(" head:flag tail:(";" flag:flag)* ")" / "(" space? ")" `
+case proplists:get_value(head, Node) of
+ undefined -> [];
+ Head ->
+ Tail = [proplists:get_value(flag,N) || N <- proplists:get_value(tail,Node)],
+ [Head|Tail]
+end
+`;
+
+flag <- alpha_string "~" flag_value / alpha_string "=" flag_value / "!" alpha_string / alpha_string`
+case Node of
+ [<<"!">>, Key] -> {'not',Key};
+ [Key, <<"~">>, Value] -> {re, Key, Value};
+ [Key, <<"=">>, Value] -> {eq, Key, Value};
+ Key -> {defined, Key}
+end
+`;
+
+flag_value <- ([^;)])+ `iolist_to_binary(Node)`;
+
+entries <- (entry_line)* ~;
+
+entry_line <- location:location / section:section / space? command:location_command space? ";" space? `
+case Node of
+ {location, Location} -> Location;
+ {section, Section} -> Section;
+ _ -> proplists:get_value(command, Node)
+end
+`;
+
+location_command <- command_root / command_rewrite / command_set / command_rack / command_include /
+ command_rtsp / command_file / command_handler / command_hds / command_hls ~;
+
+command_root <- "root" space? path:path `{root,proplists:get_value(path,Node)}`;
+command_rewrite <- "rewrite" space? path space? path `{rewrite, lists:nth(3,Node), lists:nth(5,Node)}`;
+command_set <- "set" space? key:string space? value:string `{set, proplists:get_value(key,Node), proplists:get_value(value,Node)}`;
+command_rack <- "rack" space? url:string `{rack, proplists:get_value(url,Node)}`;
+command_include <- "include" space? "@" section:path / "include" space? path:path `
+case proplists:get_value(section, Node) of
+ undefined -> {include_file, proplists:get_value(path,Node)};
+ _ -> {include_section, proplists:get_value(section,Node)}
+end
+`;
+command_file <- "file" `file`;
+command_rtsp <- "rtsp" space? url:string / "rtsp" `
+case Node of
+ <<"rtsp">> -> rtsp;
+ _ -> {rtsp, proplists:get_value(url, Node)}
+end
+`;
+command_handler <- "handler" space? module:string space? function:string space? args:(handler_arg space? )* `
+Args = [S || [S, _] <- proplists:get_value(args, Node)],
+{handler, binary_to_atom(proplists:get_value(module,Node),latin1), binary_to_atom(proplists:get_value(function,Node),latin1), Args}
+`;
+
+command_hds <- "hds_manifest" / "hds_segment" `
+case Node of
+ <<"hds_manifest">> -> {hds,manifest};
+ <<"hds_segment">> -> {hds,segment}
+end`;
+
+command_hls <- "hls_manifest" / "hls_segment" `
+case Node of
+ <<"hls_manifest">> -> {hls,manifest};
+ <<"hls_segment">> -> {hls,segment}
+end`;
+
+handler_arg <- "$" variable:string / string `
+case proplists:get_value(variable, Node) of
+ undefined -> Node;
+ Var -> {var, binary_to_atom(Var,latin1)}
+end
+`;
+
+path <- string ~;
+
+string <- (nonspace)+ `iolist_to_binary(Node)`;
+
+nonspace <- [^ ;] ~;
+
+alpha_string <- alpha_char+ `iolist_to_binary(Node)`;
+
+alpha_char <- [A-Za-z_] ~;
+
+
+space <- (white / comment_to_eol)+ `
+[]
+`;
+
+white <- [ \t\n\r] ~;
+comment_to_eol <- !'%{' '#' (!"\n" .)* ~;
34 src/http_router_sup.erl
@@ -0,0 +1,34 @@
+-module(http_router_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, ConfigPath} = application:get_env(http_router, config_path),
+ {ok, Frequency} = application:get_env(http_router, frequency),
+ http_router_compiler:check(ConfigPath),
+ if
+ is_number(Frequency) -> timer:apply_interval(Frequency, http_router_compiler, check, [ConfigPath]);
+ true -> ok
+ end,
+ {ok, { {one_for_one, 5, 10}, []} }.
+
1  src/log.hrl
@@ -0,0 +1 @@
+-define(D(X), (case application:get_env(http_router, logging_function) of undefined -> io:format("~p:~p ~240p~n", [?MODULE, ?LINE, X]); _ -> (element(2,application:get_env(http_router,logging_function)))(?MODULE, ?LINE, X) end)).
8 src/tests/routes.hrl
@@ -0,0 +1,8 @@
+-record(
+ routes,
+ {
+ method,
+ controller,
+ routes=[],
+ options=[]
+ }).
96 src/tests/routes_proper_tests.erl
@@ -0,0 +1,96 @@
+%%%-------------------------------------------------------------------
+%%% @author tthread <ilya@erlyvideo.org>
+%%% Created : 3 Nov 2011 by tthread <ilya@erlyvideo.org>
+%%%-------------------------------------------------------------------
+-module(routes_proper_tests).
+
+-export([prop_tests/0]).
+-import(routes,[get_routes/1,get_params/1,parse/2]).
+-include_lib("proper/include/proper.hrl").
+-include("routes.hrl").
+
+hostname_head_char() ->
+ frequency([{50,choose($a,$z)},{25,choose($A,$Z)},{25,choose($0,$9)}]).
+
+hostname_char() ->
+ frequency([{5,$-},{25,choose($a,$z)},{25,choose($A,$Z)},{25,choose($0,$9)}]).
+
+variable_char() ->
+ frequency([{5,$-},{25,choose($a,$z)},{25,choose($A,$Z)}]).
+
+
+hostname_label() ->
+ ?SUCHTHAT(Label, [hostname_head_char()|list(hostname_char())],
+ length(Label) < 64).
+
+hostname() ->
+ ?SUCHTHAT(Hostname,
+ ?LET(Labels, list(hostname_label()), string:join(Labels, ".")),
+ length(Hostname) > 0 andalso length(Hostname) =< 255).
+
+port_number() ->
+ choose(1, 16#ffff).
+
+port_str() ->
+ oneof(["", ?LET(Port, port_number(), ":" ++ integer_to_list(Port))]).
+
+server() ->
+ ?LET({Hostname, PortStr}, {hostname(), port_str()}, Hostname ++ PortStr).
+
+
+path() ->
+ ?LET(Num,nat(),vector(Num,hostname_label())).
+
+variable_char_vector() ->
+ ?LET(Num,nat(),vector(Num+1,variable_char())).
+
+variable() ->
+ ?LET({Var,Text1,Text2},{variable_char_vector(),variable_char_vector(),variable_char_vector()},
+ {Var,Text1++Var++Text2,Text1++"(:"++Var++")"++Text2}
+ ).
+
+variable_list() ->
+ ?LET(Nat,nat(),vector(Nat,variable())).
+
+complex_variable() ->
+ ?LET(
+ VariableList,variable_list(),
+ begin
+ UrlValues= [Element ||{_,Element,_}<-VariableList],
+ UrlVars = [Element || {_,_,Element}<-VariableList],
+ Values = [Element || {Element,_,_}<-VariableList],
+ {Values,lists:merge(UrlValues),lists:merge(UrlVars)}
+ end).
+
+variable_complex_list()->
+ ?SUCHTHAT(V,?LET(List,list(complex_variable()),List),length(V)<255).
+
+get_params_prop_test() ->
+ ?FORALL(
+ {Server,Path}, {server(),path()},
+ begin
+ get_params("http://"++Server++"/"++string:join(Path,"/"))=:=string:join(Path,"/")
+ end).
+
+parse_complex_prop_test() ->
+ ?FORALL(
+ {Server,Vars},{server(),variable_complex_list()},
+ begin
+ RoutePath = [Element || {_,_,Element}<-Vars],
+ Vv = [{Element, Element} || {[Element],_,_}<-Vars],
+ URLPath = [Element || {_,Element,_}<-Vars],
+ URL = "http://"++Server++"/"++string:join(URLPath,"/"),
+ Route = [[{string:join(RoutePath,"/"),handler,method}]],
+ case parse(URL,#routes{routes=get_routes(Route)}) of
+ {handler,method,Vv} ->
+ true;
+ {error,route_not_found} ->
+ true;
+ _ ->
+ false
+ end
+ end).
+
+prop_tests()->
+ proper:quickcheck(get_params_prop_test()),
+ proper:quickcheck(parse_complex_prop_test()).
Please sign in to comment.
Something went wrong with that request. Please try again.