Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
Checking mergeability… Don’t worry, you can still create the pull request.
  • 1 commit
  • 3 files changed
  • 0 commit comments
  • 1 contributor
Commits on Mar 26, 2012
@jkvor jkvor V2 API resources b05a015
Showing with 369 additions and 14 deletions.
  1. +227 −0 src/http_uri_r15b.erl
  2. +125 −13 src/logplex_api.erl
  3. +17 −1 src/logplex_channel.erl
View
227 src/http_uri_r15b.erl
@@ -0,0 +1,227 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2012. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+%% This is from chapter 3, Syntax Components, of RFC 3986:
+%%
+%% The generic URI syntax consists of a hierarchical sequence of
+%% components referred to as the scheme, authority, path, query, and
+%% fragment.
+%%
+%% URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+%%
+%% hier-part = "//" authority path-abempty
+%% / path-absolute
+%% / path-rootless
+%% / path-empty
+%%
+%% The scheme and path components are required, though the path may be
+%% empty (no characters). When authority is present, the path must
+%% either be empty or begin with a slash ("/") character. When
+%% authority is not present, the path cannot begin with two slash
+%% characters ("//"). These restrictions result in five different ABNF
+%% rules for a path (Section 3.3), only one of which will match any
+%% given URI reference.
+%%
+%% The following are two example URIs and their component parts:
+%%
+%% foo://example.com:8042/over/there?name=ferret#nose
+%% \_/ \______________/\_________/ \_________/ \__/
+%% | | | | |
+%% scheme authority path query fragment
+%% | _____________________|__
+%% / \ / \
+%% urn:example:animal:ferret:nose
+%%
+%% scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+%% authority = [ userinfo "@" ] host [ ":" port ]
+%% userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+%%
+%%
+
+-module(http_uri_r15b).
+
+-export([parse/1, parse/2,
+ scheme_defaults/0,
+ encode/1, decode/1]).
+
+-export_type([scheme/0, default_scheme_port_number/0]).
+
+
+%%%=========================================================================
+%%% API
+%%%=========================================================================
+
+-type scheme() :: atom().
+-type default_scheme_port_number() :: pos_integer().
+
+-spec scheme_defaults() ->
+ [{scheme(), default_scheme_port_number()}].
+
+scheme_defaults() ->
+ [{http, 80},
+ {https, 443},
+ {ftp, 21},
+ {ssh, 22},
+ {sftp, 22},
+ {tftp, 69}].
+
+parse(AbsURI) ->
+ parse(AbsURI, []).
+
+parse(AbsURI, Opts) ->
+ case parse_scheme(AbsURI, Opts) of
+ {error, Reason} ->
+ {error, Reason};
+ {Scheme, DefaultPort, Rest} ->
+ case (catch parse_uri_rest(Scheme, DefaultPort, Rest, Opts)) of
+ {ok, {UserInfo, Host, Port, Path, Query}} ->
+ {ok, {Scheme, UserInfo, Host, Port, Path, Query}};
+ {error, Reason} ->
+ {error, {Reason, Scheme, AbsURI}};
+ _ ->
+ {error, {malformed_url, Scheme, AbsURI}}
+ end
+ end.
+
+reserved() ->
+ sets:from_list([$;, $:, $@, $&, $=, $+, $,, $/, $?,
+ $#, $[, $], $<, $>, $\", ${, $}, $|,
+ $\\, $', $^, $%, $ ]).
+
+encode(URI) ->
+ Reserved = reserved(),
+ lists:append([uri_encode(Char, Reserved) || Char <- URI]).
+
+decode(String) ->
+ do_decode(String).
+
+do_decode([$%,Hex1,Hex2|Rest]) ->
+ [hex2dec(Hex1)*16+hex2dec(Hex2)|do_decode(Rest)];
+do_decode([First|Rest]) ->
+ [First|do_decode(Rest)];
+do_decode([]) ->
+ [].
+
+
+%%%========================================================================
+%%% Internal functions
+%%%========================================================================
+
+which_scheme_defaults(Opts) ->
+ Key = scheme_defaults,
+ case lists:keysearch(Key, 1, Opts) of
+ {value, {Key, SchemeDefaults}} ->
+ SchemeDefaults;
+ false ->
+ scheme_defaults()
+ end.
+
+parse_scheme(AbsURI, Opts) ->
+ case split_uri(AbsURI, ":", {error, no_scheme}, 1, 1) of
+ {error, no_scheme} ->
+ {error, no_scheme};
+ {SchemeStr, Rest} ->
+ Scheme = list_to_atom(http_util:to_lower(SchemeStr)),
+ SchemeDefaults = which_scheme_defaults(Opts),
+ case lists:keysearch(Scheme, 1, SchemeDefaults) of
+ {value, {Scheme, DefaultPort}} ->
+ {Scheme, DefaultPort, Rest};
+ false ->
+ {Scheme, no_default_port, Rest}
+ end
+ end.
+
+parse_uri_rest(Scheme, DefaultPort, "//" ++ URIPart, Opts) ->
+ {Authority, PathQuery} =
+ case split_uri(URIPart, "/", URIPart, 1, 0) of
+ Split = {_, _} ->
+ Split;
+ URIPart ->
+ case split_uri(URIPart, "\\?", URIPart, 1, 0) of
+ Split = {_, _} ->
+ Split;
+ URIPart ->
+ {URIPart,""}
+ end
+ end,
+ {UserInfo, HostPort} = split_uri(Authority, "@", {"", Authority}, 1, 1),
+ {Host, Port} = parse_host_port(Scheme, DefaultPort, HostPort, Opts),
+ {Path, Query} = parse_path_query(PathQuery),
+ {ok, {UserInfo, Host, Port, Path, Query}}.
+
+
+parse_path_query(PathQuery) ->
+ {Path, Query} = split_uri(PathQuery, "\\?", {PathQuery, ""}, 1, 0),
+ {path(Path), Query}.
+
+%% In this version of the function, we no longer need
+%% the Scheme argument, but just in case...
+parse_host_port(_Scheme, DefaultPort, "[" ++ HostPort, Opts) -> %ipv6
+ {Host, ColonPort} = split_uri(HostPort, "\\]", {HostPort, ""}, 1, 1),
+ Host2 = maybe_ipv6_host_with_brackets(Host, Opts),
+ {_, Port} = split_uri(ColonPort, ":", {"", DefaultPort}, 0, 1),
+ {Host2, int_port(Port)};
+
+parse_host_port(_Scheme, DefaultPort, HostPort, _Opts) ->
+ {Host, Port} = split_uri(HostPort, ":", {HostPort, DefaultPort}, 1, 1),
+ {Host, int_port(Port)}.
+
+split_uri(UriPart, SplitChar, NoMatchResult, SkipLeft, SkipRight) ->
+ case inets_regexp:first_match(UriPart, SplitChar) of
+ {match, Match, _} ->
+ {string:substr(UriPart, 1, Match - SkipLeft),
+ string:substr(UriPart, Match + SkipRight, length(UriPart))};
+ nomatch ->
+ NoMatchResult
+ end.
+
+maybe_ipv6_host_with_brackets(Host, Opts) ->
+ case lists:keysearch(ipv6_host_with_brackets, 1, Opts) of
+ {value, {ipv6_host_with_brackets, true}} ->
+ "[" ++ Host ++ "]";
+ _ ->
+ Host
+ end.
+
+
+int_port(Port) when is_integer(Port) ->
+ Port;
+int_port(Port) when is_list(Port) ->
+ list_to_integer(Port);
+%% This is the case where no port was found and there was no default port
+int_port(no_default_port) ->
+ throw({error, no_default_port}).
+
+path("") ->
+ "/";
+path(Path) ->
+ Path.
+
+uri_encode(Char, Reserved) ->
+ case sets:is_element(Char, Reserved) of
+ true ->
+ [ $% | http_util:integer_to_hexlist(Char)];
+ false ->
+ [Char]
+ end.
+
+hex2dec(X) when (X>=$0) andalso (X=<$9) -> X-$0;
+hex2dec(X) when (X>=$A) andalso (X=<$F) -> X-$A+10;
+hex2dec(X) when (X>=$a) andalso (X=<$f) -> X-$a+10.
+
View
138 src/logplex_api.erl
@@ -110,7 +110,7 @@ handlers() ->
{RespCode, iolist_to_binary(mochijson2:encode(Json))}
end},
- {['POST', "/channels$"], fun(Req, _Match) ->
+ {['POST', "^/channels$"], fun(Req, _Match) ->
authorize(Req),
Body = Req:recv_body(),
{struct, Params} = mochijson2:decode(Body),
@@ -129,20 +129,37 @@ handlers() ->
{201, iolist_to_binary(mochijson2:encode({struct, Info}))}
end},
- {['POST', "/channels/(\\d+)/addon$"], fun(Req, [_ChannelId]) ->
+ %% V2
+ {['GET', "^/v2/channels/(\\d+)$"], fun(Req, [ChannelId]) ->
authorize(Req),
- {200, <<"OK">>}
+ Info = logplex_channel:info_v2(list_to_integer(ChannelId)),
+ not is_list(Info) andalso exit({expected_list, Info}),
+
+ {200, iolist_to_binary(mochijson2:encode({struct, Info}))}
end},
- {['DELETE', "/channels/(\\d+)$"], fun(Req, [ChannelId]) ->
+ {['DELETE', "^/channels/(\\d+)$"], fun(Req, [ChannelId]) ->
authorize(Req),
case logplex_channel:delete(list_to_integer(ChannelId)) of
ok -> {200, <<"OK">>};
{error, not_found} -> {404, <<"Not found">>}
end
end},
- {['POST', "/channels/(\\d+)/token$"], fun(Req, [ChannelId]) ->
+ %% V2
+ {['DELETE', "^/v2/channels/(\\d+)$"], fun(Req, [ChannelId]) ->
+ authorize(Req),
+ case logplex_channel:delete(list_to_integer(ChannelId)) of
+ ok ->
+ {200, <<>>};
+ {error, not_found} ->
+ {404, iolist_to_binary(mochijson2:encode({struct, [
+ {error, <<"Not found">>}
+ ]}))}
+ end
+ end},
+
+ {['POST', "^/channels/(\\d+)/token$"], fun(Req, [ChannelId]) ->
authorize(Req),
{struct, Params} = mochijson2:decode(Req:recv_body()),
@@ -160,15 +177,50 @@ handlers() ->
{201, Token}
end},
- {['POST', "/sessions$"], fun(Req, _Match) ->
+ %% V2
+ {['POST', "^/v2/channels/(\\d+)/tokens$"], fun(Req, [ChannelId]) ->
+ authorize(Req),
+ {struct, Params} = mochijson2:decode(Req:recv_body()),
+
+ Name = proplists:get_value(<<"name">>, Params),
+ Name == undefined andalso
+ error_resp(422, iolist_to_binary(mochijson2:encode({struct, [
+ {error, <<"NAME is a required field">>}
+ ]}))),
+
+ A = os:timestamp(),
+ Token = logplex_token:create(list_to_integer(ChannelId), Name),
+ B = os:timestamp(),
+ not is_binary(Token) andalso exit({expected_binary, Token}),
+
+ ?INFO("at=create_token name=~s channel_id=~s time=~w~n",
+ [Name, ChannelId, timer:now_diff(B,A) div 1000]),
+
+ {201, iolist_to_binary(mochijson2:encode({struct, [
+ {name, Name}, {token, Token}
+ ]}))}
+ end},
+
+ {['POST', "^/sessions$"], fun(Req, _Match) ->
authorize(Req),
Body = Req:recv_body(),
Session = logplex_session:create(Body),
not is_binary(Session) andalso exit({expected_binary, Session}),
{201, Session}
end},
- {['GET', "/sessions/([\\w-]+)$"], fun(Req, [Session]) ->
+ %% V2
+ {['POST', "^/v2/sessions$"], fun(Req, _Match) ->
+ authorize(Req),
+ Body = Req:recv_body(),
+ Session = logplex_session:create(Body),
+ not is_binary(Session) andalso exit({expected_binary, Session}),
+ {201, iolist_to_binary(mochijson2:encode({struct, [
+ {url, Session}
+ ]}))}
+ end},
+
+ {['GET', "^/sessions/([\\w-]+)$"], fun(Req, [Session]) ->
proplists:get_value("srv", Req:parse_qs()) == undefined
andalso error_resp(400, <<"[Error]: Please update your Heroku client to the most recent version. If this error message persists then uninstall the Heroku client gem completely and re-install.\n">>),
Body = logplex_session:lookup(list_to_binary("/sessions/" ++ Session)),
@@ -218,15 +270,15 @@ handlers() ->
{200, ""}
end},
- {['GET', "/channels/(\\d+)/info$"], fun(Req, [ChannelId]) ->
+ {['GET', "^/channels/(\\d+)/info$"], fun(Req, [ChannelId]) ->
authorize(Req),
Info = logplex_channel:info(list_to_integer(ChannelId)),
not is_list(Info) andalso exit({expected_list, Info}),
{200, iolist_to_binary(mochijson2:encode({struct, Info}))}
end},
- {['POST', "/channels/(\\d+)/drains/tokens$"], fun(Req, [ChannelId]) ->
+ {['POST', "^/channels/(\\d+)/drains/tokens$"], fun(Req, [ChannelId]) ->
authorize(Req),
{ok, DrainId, Token} = logplex_drain:reserve_token(),
@@ -238,7 +290,7 @@ handlers() ->
{201, iolist_to_binary(mochijson2:encode({struct, Resp}))}
end},
- {['POST', "/channels/(\\d+)/drains/(\\d+)$"], fun(Req, [ChannelId, DrainId]) ->
+ {['POST', "^/channels/(\\d+)/drains/(\\d+)$"], fun(Req, [ChannelId, DrainId]) ->
authorize(Req),
{struct, Data} = mochijson2:decode(Req:recv_body()),
@@ -262,7 +314,7 @@ handlers() ->
end
end},
- {['POST', "/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
+ {['POST', "^/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
authorize(Req),
{struct, Data} = mochijson2:decode(Req:recv_body()),
@@ -293,7 +345,51 @@ handlers() ->
end
end},
- {['GET', "/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
+ %% V2
+ {['POST', "^/v2/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
+ authorize(Req),
+
+ {struct, Data} = mochijson2:decode(Req:recv_body()),
+
+ Url = proplists:get_value(<<"url">>, Data, <<>>),
+
+ {Host, Port} =
+ case Url of
+ <<>> -> {undefined, undefined};
+ _ ->
+ case catch http_uri_r15b:parse(binary_to_list(Url)) of
+ {ok, {_Proto, _Auth, Host0, Port0, _Path, _}} ->
+ {list_to_binary(Host0), Port0};
+ _ ->
+ error_resp(422,
+ iolist_to_binary(mochijson2:encode(
+ {struct, [{error, <<"Invalid drain url">>}]})))
+ end
+ end,
+
+ case logplex_channel:lookup_drains(list_to_integer(ChannelId)) of
+ List when length(List) >= ?MAX_DRAINS ->
+ {422, iolist_to_binary(mochijson2:encode({struct, [
+ {error, <<"You have already added the maximum number of drains allowed">>}]}))};
+ _ ->
+ {ok, DrainId, Token} = logplex_drain:reserve_token(),
+ case logplex_drain:create(DrainId, Token, list_to_integer(ChannelId), Host, Port) of
+ #drain{} ->
+ Resp = [
+ {id, DrainId},
+ {token, Token},
+ {url, Url}
+ ],
+ {201, iolist_to_binary(mochijson2:encode({struct, Resp}))};
+ {error, already_exists} ->
+ {409, iolist_to_binary(mochijson2:encode({struct, [{error, <<"Already exists">>}]}))};
+ {error, invalid_drain} ->
+ {422, iolist_to_binary(mochijson2:encode({struct, [{error, <<"Invalid drain">>}]}))}
+ end
+ end
+ end},
+
+ {['GET', "^/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
authorize(Req),
Drains = logplex_channel:lookup_drains(list_to_integer(ChannelId)),
not is_list(Drains) andalso exit({expected_list, Drains}),
@@ -302,7 +398,7 @@ handlers() ->
{200, iolist_to_binary(mochijson2:encode(Drains1))}
end},
- {['DELETE', "/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
+ {['DELETE', "^/channels/(\\d+)/drains$"], fun(Req, [ChannelId]) ->
authorize(Req),
Data = Req:parse_qs(),
@@ -320,6 +416,22 @@ handlers() ->
{error, not_found} -> {404, io_lib:format("Drain syslog://~s:~s does not exist", [Host, Port])}
end
end
+ end},
+
+ %% V2
+ {['DELETE', "^/v2/channels/(\\d+)/drains/(\\S+)$"], fun(Req, [ChannelId, DrainId]) ->
+ authorize(Req),
+
+ ChannelIdInt = list_to_integer(ChannelId),
+ case logplex_drain:lookup(list_to_integer(DrainId)) of
+ #drain{channel_id = Ch} when Ch == ChannelIdInt ->
+ ok = logplex_drain:delete(list_to_integer(DrainId)),
+ {200, <<>>};
+ _ ->
+ {404, iolist_to_binary(mochijson2:encode({struct, [
+ {error, <<"Not found">>}
+ ]}))}
+ end
end}].
serve([], _Method, _Path, _Req) ->
View
18 src/logplex_channel.erl
@@ -27,7 +27,7 @@
]).
-export([create/0, delete/1, lookup/1,
- lookup_tokens/1, lookup_drains/1, logs/2, info/1]).
+ lookup_tokens/1, lookup_drains/1, logs/2, info/1, info_v2/1]).
-compile({no_auto_import,[whereis/1]}).
@@ -119,3 +119,19 @@ info(ChannelId) when is_integer(ChannelId) ->
_ ->
[]
end.
+
+info_v2(ChannelId) when is_integer(ChannelId) ->
+ case lookup(ChannelId) of
+ #channel{} ->
+ Tokens = lookup_tokens(ChannelId),
+ Drains = lookup_drains(ChannelId),
+ [{channel_id, ChannelId},
+ {tokens, lists:sort([[{name, Name}, {token, Token}] || #token{id=Token, name=Name} <- Tokens])},
+ {drains, lists:sort([
+ [{id, DrainId},
+ {token, DrainToken},
+ {url, iolist_to_binary([<<"syslog://">>, Host, ":", integer_to_list(Port)])}]
+ || #drain{id=DrainId, token=DrainToken, host=Host, port=Port} <- Drains, Port>0])}];
+ _ ->
+ []
+ end.

No commit comments for this range

Something went wrong with that request. Please try again.