Browse Source

Implement authorization header parsing

Basic HTTP authorization according to RFC 2617 is implemented.
Added an example of its usage with REST handler.
Ivan Lisenkov 12 years ago
parent
commit
54c6d3fa3a

+ 3 - 0
examples/README.md

@@ -1,6 +1,9 @@
 Cowboy Examples
 ===============
 
+ *  [basic_auth](./examples/basic_auth):
+    basic HTTP authorization with REST
+
  *  [chunked_hello_world](./examples/chunked_hello_world):
     demonstrates chunked data transfer with two one-second delays
 

+ 44 - 0
examples/basic_auth/README.md

@@ -0,0 +1,44 @@
+Cowboy Basic Authorization Rest Hello World.
+============================================
+
+To compile this example you need rebar in your PATH.
+
+Type the following command:
+```
+$ rebar get-deps compile
+```
+
+You can then start the Erlang node with the following command:
+```
+./start.sh
+```
+
+Then run any given command or point your browser to the indicated URL.
+
+Examples
+--------
+
+### Get 401
+``` bash
+$ curl -i http://localhost:8080
+HTTP/1.1 401 Unauthorized
+connection: keep-alive
+server: Cowboy
+date: Sun, 20 Jan 2013 14:10:27 GMT
+content-length: 0
+www-authenticate: Restricted
+```
+
+### Get 200
+``` bash
+$ curl -i -u "Alladin:open sesame" http://localhost:8080
+HTTP/1.1 200 OK
+connection: keep-alive
+server: Cowboy
+date: Sun, 20 Jan 2013 14:11:12 GMT
+content-length: 16
+content-type: text/plain
+
+Hello, Alladin!
+```
+

+ 4 - 0
examples/basic_auth/rebar.config

@@ -0,0 +1,4 @@
+{deps, [
+	{cowboy, ".*",
+		{git, "git://github.com/extend/cowboy.git", "master"}}
+]}.

+ 15 - 0
examples/basic_auth/src/basic_auth.app.src

@@ -0,0 +1,15 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+{application, basic_auth, [
+	{description, "Cowboy Basic HTTP Authorization example."},
+	{vsn, "1"},
+	{modules, []},
+	{registered, []},
+	{applications, [
+		kernel,
+		stdlib,
+		cowboy
+	]},
+	{mod, {basic_auth_app, []}},
+	{env, []}
+]}.

+ 14 - 0
examples/basic_auth/src/basic_auth.erl

@@ -0,0 +1,14 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(basic_auth).
+
+%% API.
+-export([start/0]).
+
+%% API.
+
+start() ->
+	ok = application:start(crypto),
+	ok = application:start(ranch),
+	ok = application:start(cowboy),
+	ok = application:start(basic_auth).

+ 25 - 0
examples/basic_auth/src/basic_auth_app.erl

@@ -0,0 +1,25 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(basic_auth_app).
+-behaviour(application).
+
+%% API.
+-export([start/2]).
+-export([stop/1]).
+
+%% API.
+
+start(_Type, _Args) ->
+	Dispatch = [
+		{'_', [
+			{[], toppage_handler, []}
+		]}
+	],
+	{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
+		{env, [{dispatch, Dispatch}]}
+	]),
+	basic_auth_sup:start_link().
+
+stop(_State) ->
+	ok.

+ 23 - 0
examples/basic_auth/src/basic_auth_sup.erl

@@ -0,0 +1,23 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(basic_auth_sup).
+-behaviour(supervisor).
+
+%% API.
+-export([start_link/0]).
+
+%% supervisor.
+-export([init/1]).
+
+%% API.
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+	supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% supervisor.
+
+init([]) ->
+	Procs = [],
+	{ok, {{one_for_one, 10, 10}, Procs}}.

+ 32 - 0
examples/basic_auth/src/toppage_handler.erl

@@ -0,0 +1,32 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @doc Basic authorization Hello world handler.
+-module(toppage_handler).
+
+-export([init/3]).
+-export([content_types_provided/2]).
+-export([is_authorized/2]).
+-export([hello_to_text/2]).
+
+init(_Transport, _Req, []) ->
+	{upgrade, protocol, cowboy_rest}.
+
+
+is_authorized(Req, S) ->
+	{ok, Auth, Req1} = cowboy_req:parse_header(<<"authorization">>, Req),
+	case Auth of
+		{<<"basic">>, {User = <<"Alladin">>, <<"open sesame">>}} ->
+			{true, Req1, User};
+		_ ->
+			{{false, <<"Restricted">>}, Req1, S}
+	end.
+
+content_types_provided(Req, State) ->
+	{[
+		{<<"text/plain">>, hello_to_text}
+	], Req, State}.
+
+
+hello_to_text(Req, User) ->
+	{<< <<"Hello, ">>/binary, User/binary, <<"!\n">>/binary >>, Req, User}.
+

+ 4 - 0
examples/basic_auth/start.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+erl -pa ebin deps/*/ebin -s basic_auth \
+	-eval "io:format(\"Get 401: curl -i http://localhost:8080~n\")." \
+	-eval "io:format(\"Get 200: curl -i -u \\\"Alladin:open sesame\\\" http://localhost:8080~n\")."

+ 59 - 0
src/cowboy_http.erl

@@ -36,6 +36,7 @@
 -export([token/2]).
 -export([token_ci/2]).
 -export([quoted_string/2]).
+-export([authorization/2]).
 
 %% Decoding.
 -export([te_chunked/2]).
@@ -801,6 +802,52 @@ qvalue(<< C, Rest/binary >>, Fun, Q, M)
 qvalue(Data, Fun, Q, _M) ->
 	Fun(Data, Q).
 
+%% @doc Parse user credentials.
+-spec authorization_basic_userid(binary(), fun()) -> any().
+authorization_basic_userid(Data, Fun) ->
+	authorization_basic_userid(Data, Fun, <<>>).
+
+authorization_basic_userid(<<>>, _Fun, _Acc) ->
+	{error, badarg};
+authorization_basic_userid(<<C, _Rest/binary>>, _Fun, Acc)
+		when C < 32; C =:= 127; (C =:=$: andalso Acc =:= <<>>) ->
+	{error, badarg};
+authorization_basic_userid(<<$:, Rest/binary>>, Fun, Acc) ->
+	Fun(Rest, Acc);
+authorization_basic_userid(<<C, Rest/binary>>, Fun, Acc) ->
+	authorization_basic_userid(Rest, Fun, <<Acc/binary, C>>).
+
+-spec authorization_basic_password(binary(), fun()) -> any().
+authorization_basic_password(Data, Fun) ->
+	authorization_basic_password(Data, Fun, <<>>).
+
+authorization_basic_password(<<>>, _Fun, <<>>) ->
+	{error, badarg};
+authorization_basic_password(<<C, _Rest/binary>>, _Fun, _Acc)
+		when C < 32; C=:= 127 ->
+	{error, badarg};
+authorization_basic_password(<<>>, Fun, Acc) ->
+	Fun(Acc);
+authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) ->
+	authorization_basic_password(Rest, Fun, <<Acc/binary, C>>).
+
+%% @doc Parse authorization value according rfc 2617.
+%% Only Basic authorization is supported so far.
+-spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}.
+authorization(UserPass, Type = <<"basic">>) -> 
+	cowboy_http:whitespace(UserPass,
+		fun(D) ->
+			authorization_basic_userid(base64:mime_decode(D),
+				fun(Rest, Userid) ->
+					authorization_basic_password(Rest, 
+						fun(Password) -> 
+							{Type, {Userid, Password}}
+						end)
+				end)
+		end);
+authorization(String, Type) ->
+	{Type, String}.
+
 %% Decoding.
 
 %% @doc Decode a stream of chunks.
@@ -1294,4 +1341,16 @@ urlencode_test_() ->
 	 ?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>))
 	].
 
+http_authorization_test_() ->
+	[?_assertEqual({<<"basic">>, {<<"Alladin">>, <<"open sesame">>}},
+		authorization(<<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, <<"basic">>)),
+	 ?_assertEqual({error, badarg},
+		authorization(<<"dXNlcm5hbWUK">>, <<"basic">>)),
+	 ?_assertEqual({error, badarg},
+		 authorization(<<"_[]@#$%^&*()-AA==">>, <<"basic">>)),
+	 ?_assertEqual({error, badarg},
+		authorization(<<"dXNlcjpwYXNzCA==">>, <<"basic">>))  %% user:pass\010
+	]. 
+
+
 -endif.

+ 5 - 0
src/cowboy_req.erl

@@ -441,6 +441,11 @@ parse_header(Name, Req, Default) when Name =:= <<"accept-language">> ->
 		fun (Value) ->
 			cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2)
 		end);
+parse_header(Name, Req, Default) when Name =:= <<"authorization">> ->
+	parse_header(Name, Req, Default,
+		fun (Value) ->
+			cowboy_http:token_ci(Value, fun cowboy_http:authorization/2)
+		end);
 parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
 	parse_header(Name, Req, Default, fun cowboy_http:digits/1);
 parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->