couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dam...@apache.org
Subject svn commit: r800938 [2/2] - in /couchdb/trunk: ./ bin/ etc/couchdb/ share/ share/www/script/ share/www/script/test/ src/couchdb/ src/erlang-oauth/ src/mochiweb/ test/ utils/
Date Tue, 04 Aug 2009 19:50:48 GMT
Added: couchdb/trunk/src/couchdb/couch_httpd_auth.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_auth.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_auth.erl (added)
+++ couchdb/trunk/src/couchdb/couch_httpd_auth.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,507 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License.  You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_auth).
+-include("couch_db.hrl").
+
+-export([default_authentication_handler/1,special_test_authentication_handler/1]).
+-export([cookie_authentication_handler/1]).
+-export([null_authentication_handler/1]).
+-export([cookie_auth_header/2]).
+-export([handle_session_req/1]).
+-export([handle_user_req/1]).
+-export([ensure_users_db_exists/1, get_user/2]).
+
+-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
+
+special_test_authentication_handler(Req) ->
+    case header_value(Req, "WWW-Authenticate") of
+    "X-Couch-Test-Auth " ++ NamePass ->
+        % NamePass is a colon separated string: "joe schmoe:a password".
+        [Name, Pass] = re:split(NamePass, ":", [{return, list}]),
+        case {Name, Pass} of
+        {"Jan Lehnardt", "apple"} -> ok;
+        {"Christopher Lenz", "dog food"} -> ok;
+        {"Noah Slater", "biggiesmalls endian"} -> ok;
+        {"Chris Anderson", "mp3"} -> ok;
+        {"Damien Katz", "pecan pie"} -> ok;
+        {_, _} ->
+            throw({unauthorized, <<"Name or password is incorrect.">>})
+        end,
+        Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}};
+    _ ->
+        % No X-Couch-Test-Auth credentials sent, give admin access so the
+        % previous authentication can be restored after the test
+        Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
+    end.
+
+basic_username_pw(Req) ->
+    case header_value(Req, "Authorization") of
+    "Basic " ++ Base64Value ->
+        case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
+        [User, Pass] ->
+            {User, Pass};
+        [User] ->
+            {User, ""};
+        _ ->
+            nil
+        end;
+    _ ->
+        nil
+    end.
+
+default_authentication_handler(Req) ->
+    case basic_username_pw(Req) of
+    {User, Pass} ->
+        case couch_server:is_admin(User, Pass) of
+        true ->
+            Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}};
+        false ->
+            throw({unauthorized, <<"Name or password is incorrect.">>})
+        end;
+    nil ->
+        case couch_server:has_admins() of
+        true ->
+            Req;
+        false ->
+            case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
+                "true" -> Req;
+                % If no admins, and no user required, then everyone is admin!
+                % Yay, admin party!
+                _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
+            end
+        end
+    end.
+
+null_authentication_handler(Req) ->
+    Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
+
+% Cookie auth handler using per-node user db
+cookie_authentication_handler(Req) ->
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    case cookie_auth_user(Req, ?l2b(DbName)) of
+    % Fall back to default authentication handler
+    nil -> default_authentication_handler(Req);
+    Req2 -> Req2
+    end.
+
+% Cookie auth handler using per-db user db
+% cookie_authentication_handler(#httpd{path_parts=Path}=Req) ->
+%     case Path of
+%     [DbName|_] ->
+%         case cookie_auth_user(Req, DbName) of
+%         nil -> default_authentication_handler(Req);
+%         Req2 -> Req2
+%         end;
+%     _Else ->
+%         % Fall back to default authentication handler
+%         default_authentication_handler(Req)
+%     end.
+
+% maybe we can use hovercraft to simplify running this view query
+get_user(Db, UserName) ->
+    DesignId = <<"_design/_auth">>,
+    ViewName = <<"users">>,
+    % if the design doc or the view doesn't exist, then make it
+    ensure_users_view_exists(Db, DesignId, ViewName),
+    
+    case (catch couch_view:get_map_view(Db, DesignId, ViewName, nil)) of
+    {ok, View, _Group} ->
+        FoldlFun = fun
+        ({{Key, _DocId}, Value}, _, nil) when Key == UserName -> {ok, Value};
+        (_, _, Acc) -> {stop, Acc}
+        end,
+        case couch_view:fold(View, {UserName, nil}, fwd, FoldlFun, nil) of
+        {ok, {Result}} -> Result;
+        _Else -> nil
+        end;
+    {not_found, _Reason} ->
+        nil
+        % case (catch couch_view:get_reduce_view(Db, DesignId, ViewName, nil)) of
+        % {ok, _ReduceView, _Group} ->
+        %     not_implemented;
+        % {not_found, _Reason} ->
+        %     nil
+        % end
+    end.
+    
+ensure_users_db_exists(DbName) ->
+    case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+    {ok, Db} ->
+        couch_db:close(Db),
+        ok;
+    _Error -> 
+        ?LOG_ERROR("Create the db ~p", [DbName]),
+        {ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
+        ?LOG_ERROR("Created the db ~p", [DbName]),
+        couch_db:close(Db),
+        ok
+    end.
+    
+ensure_users_view_exists(Db, DDocId, VName) -> 
+    try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of
+        _Foo -> ok
+    catch 
+        _:Error -> 
+            ?LOG_ERROR("create the design document ~p : ~p", [DDocId, Error]),
+            % create the design document
+            {ok, AuthDesign} = auth_design_doc(DDocId, VName),
+            {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []),
+            ?LOG_ERROR("created the design document", []),
+            ok
+    end.
+
+auth_design_doc(DocId, VName) ->
+    DocProps = [
+        {<<"_id">>, DocId},
+        {<<"language">>,<<"javascript">>},
+        {<<"views">>,
+            {[{VName,
+                {[{<<"map">>,
+                    <<"function (doc) {\n if (doc.type == \"user\") {\n        emit(doc.username, doc);\n}\n}">>
+                }]}
+            }]}
+        }],
+    {ok, couch_doc:from_json_obj({DocProps})}.
+    
+
+user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles) ->
+    user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, nil).
+user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, Rev) ->
+    DocProps = [
+        {<<"_id">>, DocId},
+        {<<"type">>, <<"user">>},
+        {<<"username">>, Username},
+        {<<"password_sha">>, PasswordHash},
+        {<<"salt">>, UserSalt},
+        {<<"email">>, Email},
+        {<<"active">>, Active},
+        {<<"roles">>, Roles}],
+    DocProps1 = case Rev of
+    nil -> DocProps;
+    _Rev -> 
+        [{<<"_rev">>, Rev}] ++ DocProps
+    end,
+    {ok, couch_doc:from_json_obj({DocProps1})}.
+
+cookie_auth_user(_Req, undefined) -> nil;
+cookie_auth_user(#httpd{mochi_req=MochiReq}=Req, DbName) ->
+    case MochiReq:get_cookie_value("AuthSession") of
+    undefined -> nil;
+    [] -> nil;
+    Cookie -> 
+        case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+        {ok, Db} ->
+            try
+                AuthSession = couch_util:decodeBase64Url(Cookie),
+                [User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"),
+                % Verify expiry and hash
+                {NowMS, NowS, _} = erlang:now(),
+                CurrentTime = NowMS * 1000000 + NowS,
+                case couch_config:get("couch_httpd_auth", "secret", nil) of
+                nil -> nil;
+                SecretStr ->
+                    Secret = ?l2b(SecretStr),
+                    case get_user(Db, ?l2b(User)) of
+                    nil -> nil;
+                    Result ->
+                        UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>),
+                        FullSecret = <<Secret/binary, UserSalt/binary>>,
+                        ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
+                        Hash = ?l2b(string:join(HashParts, ":")),
+                        Timeout = to_int(couch_config:get("couch_httpd_auth", "timeout", 600)),
+                        ?LOG_DEBUG("timeout ~p", [Timeout]),
+                        case (catch erlang:list_to_integer(TimeStr, 16)) of
+                            TimeStamp when CurrentTime < TimeStamp + Timeout 
+                            andalso ExpectedHash == Hash ->
+                                TimeLeft = TimeStamp + Timeout - CurrentTime,
+                                ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
+                                Req#httpd{user_ctx=#user_ctx{
+                                    name=?l2b(User),
+                                    roles=proplists:get_value(<<"roles">>, Result, [])
+                                }, auth={FullSecret, TimeLeft < Timeout*0.9}};
+                            _Else ->
+                                nil
+                        end
+                    end
+                end
+            after
+                couch_db:close(Db)
+            end;
+        _Else ->
+            nil
+        end
+    end.
+
+cookie_auth_header(#httpd{user_ctx=#user_ctx{name=null}}, _Headers) -> [];
+cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}, Headers) ->
+    % Note: we only set the AuthSession cookie if:
+    %  * a valid AuthSession cookie has been received
+    %  * we are outside a 10% timeout window
+    %  * and if an AuthSession cookie hasn't already been set e.g. by a login
+    %    or logout handler.
+    % The login and logout handlers need to set the AuthSession cookie
+    % themselves.
+    case proplists:get_value("Set-Cookie", Headers) of
+    undefined -> [];
+    Cookie -> 
+        case proplists:get_value("AuthSession",
+            mochiweb_cookies:parse_cookie(Cookie), undefined) of
+        undefined ->
+            {NowMS, NowS, _} = erlang:now(),
+            TimeStamp = NowMS * 1000000 + NowS,
+            [cookie_auth_cookie(?b2l(User), Secret, TimeStamp)];
+        _Else -> []
+        end
+    end;
+cookie_auth_header(_Req, _Headers) -> [].
+
+cookie_auth_cookie(User, Secret, TimeStamp) ->
+    SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
+    Hash = crypto:sha_mac(Secret, SessionData),
+    mochiweb_cookies:cookie("AuthSession",
+        couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
+        [{path, "/"}, {http_only, true}]). % TODO add {secure, true} when SSL is detected
+
+% Login handler with user db
+handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req, #db{}=Db) ->
+    ReqBody = MochiReq:recv_body(),
+    Form = case MochiReq:get_primary_header_value("content-type") of
+        "application/x-www-form-urlencoded" ++ _ ->
+            mochiweb_util:parse_qs(ReqBody);
+        _ ->
+            []
+    end,
+    UserName = ?l2b(proplists:get_value("username", Form, "")),
+    Password = ?l2b(proplists:get_value("password", Form, "")),
+    User = case get_user(Db, UserName) of
+        nil -> [];
+        Result -> Result
+    end,
+    UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
+    PasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+    case proplists:get_value(<<"password_sha">>, User, nil) of
+        ExpectedHash when ExpectedHash == PasswordHash ->
+            Secret = ?l2b(couch_config:get("couch_httpd_auth", "secret", nil)),
+            {NowMS, NowS, _} = erlang:now(),
+            CurrentTime = NowMS * 1000000 + NowS,
+            Cookie = cookie_auth_cookie(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+            {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+                nil ->
+                    {200, [Cookie]};
+                Redirect ->
+                    {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+            end,
+            send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
+                {[{ok, true}]});
+        _Else ->
+            throw({unauthorized, <<"Name or password is incorrect.">>})
+    end.
+
+% Session Handler
+
+handle_session_req(#httpd{method='POST'}=Req) ->
+    % login
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+        {ok, Db} -> handle_login_req(Req, Db)
+    end;
+handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
+    % whoami
+    Name = UserCtx#user_ctx.name,
+    Roles = UserCtx#user_ctx.roles,
+    ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
+    case {Name, ForceLogin} of
+        {null, "true"} ->
+            throw({unauthorized, <<"Please login.">>});
+        _False -> ok
+    end,
+    send_json(Req, {[
+        {ok, true},
+        {name, Name},
+        {roles, Roles}
+    ]});
+handle_session_req(#httpd{method='DELETE'}=Req) ->
+    % logout
+    Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+    {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+        nil ->
+            {200, [Cookie]};
+        Redirect ->
+            {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+    end,
+    send_json(Req, Code, Headers, {[{ok, true}]});
+handle_session_req(Req) ->
+    send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
+    
+create_user_req(#httpd{method='POST', mochi_req=MochiReq}=Req, Db) ->
+    ReqBody = MochiReq:recv_body(),
+    Form = case MochiReq:get_primary_header_value("content-type") of
+        "application/x-www-form-urlencoded" ++ _ ->
+            ?LOG_INFO("body parsed ~p", [mochiweb_util:parse_qs(ReqBody)]),
+            mochiweb_util:parse_qs(ReqBody);
+        _ ->
+            []
+    end,
+    Roles = proplists:get_all_values("roles", Form),
+    UserName = ?l2b(proplists:get_value("username", Form, "")),
+    Password = ?l2b(proplists:get_value("password", Form, "")),
+    Email = ?l2b(proplists:get_value("email", Form, "")),
+    Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
+    case get_user(Db, UserName) of
+    nil -> 
+        Roles1 = case Roles of
+        [] -> Roles;
+        _ ->
+            ok = couch_httpd:verify_is_server_admin(Req),
+            [?l2b(R) || R <- Roles]
+        end,
+            
+        UserSalt = couch_util:new_uuid(),
+        PasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+        DocId = couch_util:new_uuid(),
+        {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1),
+        {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
+        ?LOG_DEBUG("User ~s (~s) with password, ~s created.", [?b2l(UserName), ?b2l(DocId), ?b2l(Password)]),
+        {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+            nil ->
+                {200, []};
+            Redirect ->
+                {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+        end,
+        send_json(Req, Code, Headers, {[{ok, true}]});
+    _Result -> 
+        ?LOG_DEBUG("Can't create ~s: already exists", [?b2l(UserName)]),
+         throw({forbidden, <<"User already exists.">>})
+    end.
+    
+update_user_req(#httpd{method='PUT', mochi_req=MochiReq, user_ctx=UserCtx}=Req, Db, UserName) ->
+    Name = UserCtx#user_ctx.name,
+    UserRoles = UserCtx#user_ctx.roles,
+    case User = get_user(Db, UserName) of
+    nil ->
+        throw({not_found, <<"User don't exist">>});
+    _Result ->
+        ReqBody = MochiReq:recv_body(),
+        Form = case MochiReq:get_primary_header_value("content-type") of
+            "application/x-www-form-urlencoded" ++ _ ->
+                mochiweb_util:parse_qs(ReqBody);
+            _ ->
+                []
+        end,
+        Roles = proplists:get_all_values("roles", Form),
+        Password = ?l2b(proplists:get_value("password", Form, "")),
+        Email = ?l2b(proplists:get_value("email", Form, "")),
+        Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
+        OldPassword = proplists:get_value("old_password", Form, ""),
+        OldPassword1 = ?l2b(OldPassword),
+        UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
+        OldRev = proplists:get_value(<<"_rev">>, User, <<>>),
+        DocId = proplists:get_value(<<"_id">>, User, <<>>),
+        CurrentPasswordHash = proplists:get_value(<<"password_sha">>, User, nil),
+        
+        
+        Roles1 = case Roles of
+        [] -> Roles;
+        _ ->
+            ok = couch_httpd:verify_is_server_admin(Req),
+            [?l2b(R) || R <- Roles]
+        end,
+        
+        PasswordHash = case lists:member(<<"_admin">>, UserRoles) of
+        true ->
+            Hash = case Password of
+                <<>> -> CurrentPasswordHash;
+                _Else ->
+                    H = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+                    H
+                end,
+            Hash;
+        false when Name == UserName ->
+            %% for user we test old password before allowing change
+            Hash = case Password of
+                <<>> -> 
+                    CurrentPasswordHash;
+                _P when length(OldPassword) == 0 ->
+                    throw({forbidden, <<"Old password is incorrect.">>});
+                _Else ->
+                    OldPasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, OldPassword1/binary>>)),
+                    ?LOG_DEBUG("~p == ~p", [CurrentPasswordHash, OldPasswordHash]),
+                    Hash1 = case CurrentPasswordHash of
+                        ExpectedHash when ExpectedHash == OldPasswordHash ->
+                            H = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+                            H;
+                        _ ->
+                            throw({forbidden, <<"Old password is incorrect.">>})
+                        end,
+                    Hash1
+                end,
+            Hash;
+        _ ->
+            throw({forbidden, <<"You aren't allowed to change this password.">>})
+        end, 
+        {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1, OldRev),
+        {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
+        ?LOG_DEBUG("User ~s (~s)updated.", [?b2l(UserName), ?b2l(DocId)]),
+        {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+        nil -> {200, []};
+        Redirect ->
+            {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+        end,
+        send_json(Req, Code, Headers, {[{ok, true}]})
+    end.
+
+handle_user_req(#httpd{method='POST'}=Req) ->
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    ensure_users_db_exists(?l2b(DbName)),
+    case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+        {ok, Db} -> create_user_req(Req, Db)
+    end;
+handle_user_req(#httpd{method='PUT', path_parts=[_]}=_Req) ->
+    throw({bad_request, <<"Username is missing">>});
+handle_user_req(#httpd{method='PUT', path_parts=[_, UserName]}=Req) ->
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    ensure_users_db_exists(?l2b(DbName)),
+    case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+        {ok, Db} -> update_user_req(Req, Db, UserName)
+    end;
+handle_user_req(Req) ->
+     send_method_not_allowed(Req, "GET,HEAD,POST,PUT,DELETE").
+
+to_int(Value) when is_binary(Value) ->
+    to_int(?b2l(Value)); 
+to_int(Value) when is_list(Value) ->
+    erlang:list_to_integer(Value);
+to_int(Value) when is_integer(Value) ->
+    Value.
+
+% % Login handler
+% handle_login_req(#httpd{method='POST'}=Req) ->
+%     DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+%     case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+%         {ok, Db} -> handle_login_req(Req, Db)
+%     end;
+% handle_login_req(Req) ->
+%     send_method_not_allowed(Req, "POST").
+% 
+% % Logout handler
+% handle_logout_req(#httpd{method='POST'}=Req) ->
+%     Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+%     {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+%         nil ->
+%             {200, [Cookie]};
+%         Redirect ->
+%             {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+%     end,
+%     send_json(Req, Code, Headers, {[{ok, true}]});
+% handle_logout_req(Req) ->
+%     send_method_not_allowed(Req, "POST").

Modified: couchdb/trunk/src/couchdb/couch_httpd_external.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_external.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_external.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_external.erl Tue Aug  4 19:50:46 2009
@@ -57,7 +57,8 @@
 json_req_obj(#httpd{mochi_req=Req,
                method=Verb,
                path_parts=Path,
-               req_body=ReqBody
+               req_body=ReqBody,
+               user_ctx=#user_ctx{name=UserName, roles=UserRoles}
             }, Db) ->
     Body = case ReqBody of
         undefined -> Req:recv_body();
@@ -69,6 +70,7 @@
         _ ->
             []
     end,
+    UserCtx = {[{<<"name">>, UserName}, {<<"roles">>, UserRoles}]},
     Headers = Req:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     {ok, Info} = couch_db:get_db_info(Db),
@@ -80,7 +82,8 @@
         {<<"headers">>, to_json_terms(Hlist)},
         {<<"body">>, Body},
         {<<"form">>, to_json_terms(ParsedForm)},
-        {<<"cookie">>, to_json_terms(Req:parse_cookie())}]}.
+        {<<"cookie">>, to_json_terms(Req:parse_cookie())},
+        {<<"userCtx">>, UserCtx}]}.
 
 to_json_terms(Data) ->
     to_json_terms(Data, []).

Modified: couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl Tue Aug  4 19:50:46 2009
@@ -15,7 +15,7 @@
 -export([handle_welcome_req/2,handle_favicon_req/2,handle_utils_dir_req/2,
     handle_all_dbs_req/1,handle_replicate_req/1,handle_restart_req/1,
     handle_uuids_req/1,handle_config_req/1,handle_log_req/1,
-    handle_task_status_req/1,handle_sleep_req/1,handle_whoami_req/1]).
+    handle_task_status_req/1,handle_sleep_req/1]).
 
 -export([increment_update_seq_req/2]).
 
@@ -88,11 +88,13 @@
 get_rep_endpoint(_Req, {Props}) ->
     Url = proplists:get_value(<<"url">>, Props),
     {BinHeaders} = proplists:get_value(<<"headers">>, Props, {[]}),
-    {remote, fix_db_url(Url), [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders]};
+    Auth = proplists:get_value(<<"auth">>, Props, undefined),
+    ?LOG_DEBUG("AUTH ~p", [Auth]),
+    {remote, fix_db_url(Url), [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders], Auth};
 get_rep_endpoint(_Req, <<"http://",_/binary>>=Url) ->
-    {remote, fix_db_url(Url), []};
+    {remote, fix_db_url(Url), [], []};
 get_rep_endpoint(_Req, <<"https://",_/binary>>=Url) ->
-    {remote, fix_db_url(Url), []};
+    {remote, fix_db_url(Url), [], []};
 get_rep_endpoint(#httpd{user_ctx=UserCtx}, <<DbName/binary>>) ->
     {local, DbName, UserCtx}.
 
@@ -218,20 +220,3 @@
     send_method_not_allowed(Req, "GET").
 
 
-% whoami handler
-handle_whoami_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
-    Name = UserCtx#user_ctx.name,
-    Roles = UserCtx#user_ctx.roles,
-    ForceLogin = couch_httpd:qs_value(Req, "force_login", "false"),
-    case {Name, ForceLogin} of
-        {null, "true"} ->
-            throw({unauthorized, <<"Please login.">>});
-        _False -> ok
-    end,
-    send_json(Req, {[
-        {ok, true},
-        {name, Name},
-        {roles, Roles}
-    ]});
-handle_whoami_req(Req) ->
-    send_method_not_allowed(Req, "GET").

Added: couchdb/trunk/src/couchdb/couch_httpd_oauth.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_oauth.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_oauth.erl (added)
+++ couchdb/trunk/src/couchdb/couch_httpd_oauth.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,173 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License.  You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_oauth).
+-include("couch_db.hrl").
+
+-export([oauth_authentication_handler/1, handle_oauth_req/1, consumer_lookup/2]).
+
+% OAuth auth handler using per-node user db
+oauth_authentication_handler(#httpd{method=Method}=Req) ->
+    serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+        AccessToken = proplists:get_value("oauth_token", Params),
+        TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+        case oauth:verify(Signature, atom_to_list(Method), URL, Params, Consumer, TokenSecret) of
+            true ->
+                set_user_ctx(Req, AccessToken);
+            false ->
+                Req
+        end
+    end, true).
+
+% Look up the consumer key and get the roles to give the consumer
+set_user_ctx(Req, AccessToken) ->
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    couch_httpd_auth:ensure_users_db_exists(?l2b(DbName)),
+    case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+        {ok, Db} ->
+            Name = ?l2b(couch_config:get("oauth_token_users", AccessToken)),
+            case couch_httpd_auth:get_user(Db, Name) of
+                nil -> Req;
+                User ->
+                    Roles = proplists:get_value(<<"roles">>, User, []),
+                    Req#httpd{user_ctx=#user_ctx{name=Name, roles=Roles}}
+            end;
+        _Else->
+            Req
+    end.
+
+% OAuth request_token
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"request_token">>], method=Method}=Req) ->
+    serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+        AccessToken = proplists:get_value("oauth_token", Params),
+        TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+        case oauth:verify(Signature, atom_to_list(Method), URL, Params, Consumer, TokenSecret) of
+            true ->
+                ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+            false ->
+                invalid_signature(Req)
+        end
+    end, false);
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"authorize">>]}=Req) ->
+    {ok, serve_oauth_authorize(Req)};
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>], method='GET'}=Req) ->
+    serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+        case oauth:token(Params) of
+            "requestkey" ->
+                case oauth:verify(Signature, "GET", URL, Params, Consumer, "requestsecret") of
+                    true ->
+                        ok(Req, <<"oauth_token=accesskey&oauth_token_secret=accesssecret">>);
+                    false ->
+                        invalid_signature(Req)
+                end;
+            _ ->
+                couch_httpd:send_error(Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>)
+        end
+    end, false);
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>]}=Req) ->
+    couch_httpd:send_method_not_allowed(Req, "GET").
+
+invalid_signature(Req) ->
+    couch_httpd:send_error(Req, 400, <<"invalid_signature">>, <<"Invalid signature value.">>).
+
+% This needs to be protected i.e. force user to login using HTTP Basic Auth or form-based login.
+serve_oauth_authorize(#httpd{method=Method}=Req) ->
+    case Method of
+        'GET' ->
+            % Confirm with the User that they want to authenticate the Consumer
+            serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+                AccessToken = proplists:get_value("oauth_token", Params),
+                TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+                case oauth:verify(Signature, "GET", URL, Params, Consumer, TokenSecret) of
+                    true ->
+                        ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+                    false ->
+                        invalid_signature(Req)
+                end
+            end, false);
+        'POST' ->
+            % If the User has confirmed, we direct the User back to the Consumer with a verification code
+            serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+                AccessToken = proplists:get_value("oauth_token", Params),
+                TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+                case oauth:verify(Signature, "POST", URL, Params, Consumer, TokenSecret) of
+                    true ->
+                        %redirect(oauth_callback, oauth_token, oauth_verifier),
+                        ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+                    false ->
+                        invalid_signature(Req)
+                end
+            end, false);
+        _ ->
+            couch_httpd:send_method_not_allowed(Req, "GET,POST")
+    end.
+
+serve_oauth(#httpd{mochi_req=MochiReq}=Req, Fun, FailSilently) ->
+    % 1. In the HTTP Authorization header as defined in OAuth HTTP Authorization Scheme.
+    % 2. As the HTTP POST request body with a content-type of application/x-www-form-urlencoded.
+    % 3. Added to the URLs in the query part (as defined by [RFC3986] section 3).
+    AuthHeader = case MochiReq:get_header_value("authorization") of
+        undefined ->
+            "";
+        Else ->
+            [Head | Tail] = re:split(Else, "\\s", [{parts, 2}, {return, list}]),
+            case [string:to_lower(Head) | Tail] of
+                ["oauth", Rest] -> Rest;
+                _ -> ""
+            end
+    end,
+    HeaderParams = oauth_uri:params_from_header_string(AuthHeader),
+    %Realm = proplists:get_value("realm", HeaderParams),
+    Params = proplists:delete("realm", HeaderParams) ++ MochiReq:parse_qs(),
+    ?LOG_DEBUG("OAuth Params: ~p", [Params]),
+    case proplists:get_value("oauth_version", Params, "1.0") of
+        "1.0" ->
+            case proplists:get_value("oauth_consumer_key", Params, undefined) of
+                undefined ->
+                    case FailSilently of
+                        true -> Req;
+                        false -> couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer.">>)
+                    end;
+                ConsumerKey ->
+                    SigMethod = proplists:get_value("oauth_signature_method", Params),
+                    case consumer_lookup(ConsumerKey, SigMethod) of
+                        none ->
+                            couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer (key or signature method).">>);
+                        Consumer ->
+                            Signature = proplists:get_value("oauth_signature", Params),
+                            URL = couch_httpd:absolute_uri(Req, MochiReq:get(path)),
+                            Fun(URL, proplists:delete("oauth_signature", Params),
+                                Consumer, Signature)
+                    end
+            end;
+        _ ->
+            couch_httpd:send_error(Req, 400, <<"invalid_oauth_version">>, <<"Invalid OAuth version.">>)
+    end.
+
+consumer_lookup(Key, MethodStr) ->
+    SignatureMethod = case MethodStr of
+        "PLAINTEXT" -> plaintext;
+        "HMAC-SHA1" -> hmac_sha1;
+        %"RSA-SHA1" -> rsa_sha1;
+        _Else -> undefined
+    end,
+    case SignatureMethod of
+        undefined -> none;
+        _SupportedMethod ->
+            case couch_config:get("oauth_consumer_secrets", Key, undefined) of
+                undefined -> none;
+                Secret -> {Key, Secret, SignatureMethod}
+            end
+    end.
+
+ok(#httpd{mochi_req=MochiReq}, Body) ->
+    {ok, MochiReq:respond({200, [], Body})}.

Modified: couchdb/trunk/src/couchdb/couch_httpd_show.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_show.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_show.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_show.erl Tue Aug  4 19:50:46 2009
@@ -30,7 +30,6 @@
     handle_doc_show(Req, DesignName, ShowName, DocId, Db);
 
 handle_doc_show_req(#httpd{
-        method='GET',
         path_parts=[_DbName, _Design, DesignName, _Show, ShowName]
     }=Req, Db) ->
     handle_doc_show(Req, DesignName, ShowName, nil, Db);
@@ -39,7 +38,7 @@
     send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>);
 
 handle_doc_show_req(Req, _Db) ->
-    send_method_not_allowed(Req, "GET,HEAD").
+    send_method_not_allowed(Req, "GET,POST,HEAD").
 
 handle_doc_show(Req, DesignName, ShowName, DocId, Db) ->
     DesignId = <<"_design/", DesignName/binary>>,
@@ -112,7 +111,7 @@
     end.
 
 
-output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
+output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
     #view_query_args{
         limit = Limit,
         direction = Dir,
@@ -125,7 +124,7 @@
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         % get the os process here
         % pass it into the view fold with closures
@@ -145,7 +144,7 @@
         finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount)
     end);
 
-output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
+output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
     #view_query_args{
         limit = Limit,
         direction = Dir,
@@ -156,7 +155,7 @@
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         % get the os process here
         % pass it into the view fold with closures
@@ -239,7 +238,7 @@
         _ -> send_chunk(Resp, Chunk)
     end.
 
-output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
+output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
     #view_query_args{
         limit = Limit,
         direction = Dir,
@@ -256,7 +255,7 @@
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag),
         SendListRowFun = make_reduce_send_row_fun(QueryServer, Db),
@@ -274,7 +273,7 @@
         finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null)
     end);
 
-output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
+output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
     #view_query_args{
         limit = Limit,
         direction = Dir,
@@ -289,7 +288,7 @@
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, Keys}),
+    CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx, Keys}),
 
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag),
@@ -332,17 +331,18 @@
     end,
     send_chunk(Resp, []).
 
+
 render_head_for_empty_list(StartListRespFun, Req, Etag, null) ->
     StartListRespFun(Req, Etag, []); % for reduce
 render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) ->
     StartListRespFun(Req, Etag, TotalRows, null, []).
 
-send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq}=Req, Db) ->
+send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) ->
     % compute etag with no doc
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept}),
+    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept, UserCtx}),
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc,
             DocId, nil, Req, Db),
@@ -350,12 +350,12 @@
         couch_httpd_external:send_external_response(Req, JsonResp)
     end);
 
-send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
+send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) ->
     % calculate the etag
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept}),
+    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept, UserCtx}),
     % We know our etag now
     couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
         [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc,

Modified: couchdb/trunk/src/couchdb/couch_httpd_view.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_view.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_view.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_view.erl Tue Aug  4 19:50:46 2009
@@ -18,7 +18,7 @@
 -export([get_stale_type/1, get_reduce_type/1, parse_view_params/3]).
 -export([make_view_fold_fun/6, finish_view_fold/3, view_row_obj/3]).
 -export([view_group_etag/1, view_group_etag/2, make_reduce_fold_funs/5]).
--export([design_doc_view/5]).
+-export([design_doc_view/5, parse_bool_param/1]).
 
 -import(couch_httpd,
     [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,send_chunk/2,

Modified: couchdb/trunk/src/couchdb/couch_rep.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_rep.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_rep.erl (original)
+++ couchdb/trunk/src/couchdb/couch_rep.erl Tue Aug  4 19:50:46 2009
@@ -84,7 +84,8 @@
 
 -record(http_db, {
     uri,
-    headers
+    headers,
+    oauth
 }).
 
 
@@ -496,8 +497,8 @@
     end.
 
 
-open_db({remote, Url, Headers})->
-    {ok, #http_db{uri=?b2l(Url), headers=Headers}, Url};
+open_db({remote, Url, Headers, Auth})->
+    {ok, #http_db{uri=?b2l(Url), headers=Headers, oauth=Auth}, Url};
 open_db({local, DbName, UserCtx})->
     case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
     {ok, Db} -> {ok, Db, DbName};
@@ -600,19 +601,38 @@
 
     end.
 
-do_http_request(Url, Action, Headers) ->
-    do_http_request(Url, Action, Headers, []).
+do_http_request(Url, Action, Headers, Auth) ->
+    do_http_request(Url, Action, Headers, Auth, []).
 
-do_http_request(Url, Action, Headers, JsonBody) ->
-    do_http_request(Url, Action, Headers, JsonBody, 10, 1000).
+do_http_request(Url, Action, Headers, Auth, JsonBody) ->
+    Headers0 = case Auth of
+        {Props} ->
+            % Add OAuth header
+            {OAuth} = proplists:get_value(<<"oauth">>, Props),
+            ConsumerKey = ?b2l(proplists:get_value(<<"consumer_key">>, OAuth)),
+            Token = ?b2l(proplists:get_value(<<"token">>, OAuth)),
+            TokenSecret = ?b2l(proplists:get_value(<<"token_secret">>, OAuth)),
+            ConsumerSecret = ?b2l(proplists:get_value(<<"consumer_secret">>, OAuth)),
+            Consumer = {ConsumerKey, ConsumerSecret, hmac_sha1},
+            Method = case Action of
+                get -> "GET";
+                post -> "POST";
+                put -> "PUT"
+            end,
+            Params = oauth:signed_params(Method, Url, [], Consumer, Token, TokenSecret),
+            [{"Authorization", "OAuth " ++ oauth_uri:params_to_header_string(Params)} | Headers];
+        _Else ->
+            Headers
+    end,
+    do_http_request0(Url, Action, Headers0, JsonBody, 10, 1000).
 
-do_http_request(Url, Action, Headers, Body, Retries, Pause) when is_binary(Url) ->
-    do_http_request(?b2l(Url), Action, Headers, Body, Retries, Pause);
-do_http_request(Url, Action, _Headers, _JsonBody, 0, _Pause) ->
+do_http_request0(Url, Action, Headers, Body, Retries, Pause) when is_binary(Url) ->
+    do_http_request0(?b2l(Url), Action, Headers, Body, Retries, Pause);
+do_http_request0(Url, Action, _Headers, _JsonBody, 0, _Pause) ->
     ?LOG_ERROR("couch_rep HTTP ~p request failed after 10 retries: ~s",
         [Action, Url]),
     exit({http_request_failed, ?l2b(["failed to replicate ", Url])});
-do_http_request(Url, Action, Headers, JsonBody, Retries, Pause) ->
+do_http_request0(Url, Action, Headers, JsonBody, Retries, Pause) ->
     ?LOG_DEBUG("couch_rep HTTP ~p request: ~s", [Action, Url]),
     Body =
     case JsonBody of
@@ -638,7 +658,7 @@
         ResponseCode >= 300, ResponseCode < 400 ->
             RedirectUrl = mochiweb_headers:get_value("Location",
                 mochiweb_headers:make(ResponseHeaders)),
-            do_http_request(RedirectUrl, Action, Headers, JsonBody, Retries-1,
+            do_http_request0(RedirectUrl, Action, Headers, JsonBody, Retries-1,
                 Pause);
         ResponseCode >= 400, ResponseCode < 500 ->
             ?JSON_DECODE(ResponseBody);
@@ -646,18 +666,18 @@
             ?LOG_INFO("retrying couch_rep HTTP ~p request in ~p seconds " ++
                 "due to 500 error: ~s", [Action, Pause/1000, Url]),
             timer:sleep(Pause),
-            do_http_request(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
+            do_http_request0(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
         end;
     {error, Reason} ->
         ?LOG_INFO("retrying couch_rep HTTP ~p request in ~p seconds due to " ++
             "{error, ~p}: ~s", [Action, Pause/1000, Reason, Url]),
         timer:sleep(Pause),
-        do_http_request(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
+        do_http_request0(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
     end.
 
-ensure_full_commit(#http_db{uri=DbUrl, headers=Headers}) ->
+ensure_full_commit(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}) ->
     {ResultProps} = do_http_request(DbUrl ++ "_ensure_full_commit", post,
-        Headers, true),
+        Headers, OAuth, true),
     true = proplists:get_value(<<"ok">>, ResultProps),
     {ok, proplists:get_value(<<"instance_start_time">>, ResultProps)};
 ensure_full_commit(Db) ->
@@ -687,16 +707,16 @@
 
 
 
-get_db_info(#http_db{uri=DbUrl, headers=Headers}) ->
-    {DbProps} = do_http_request(DbUrl, get, Headers),
+get_db_info(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}) ->
+    {DbProps} = do_http_request(DbUrl, get, Headers, OAuth),
     {ok, [{list_to_atom(?b2l(K)), V} || {K,V} <- DbProps]};
 get_db_info(Db) ->
     couch_db:get_db_info(Db).
 
-get_doc_info_list(#http_db{uri=DbUrl, headers=Headers}, StartSeq) ->
+get_doc_info_list(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, StartSeq) ->
     Url = DbUrl ++ "_all_docs_by_seq?limit=100&startkey="
         ++ integer_to_list(StartSeq),
-    {Results} = do_http_request(Url, get, Headers),
+    {Results} = do_http_request(Url, get, Headers, OAuth),
     lists:map(fun({RowInfoList}) ->
         {RowValueProps} = proplists:get_value(<<"value">>, RowInfoList),
         Seq = proplists:get_value(<<"key">>, RowInfoList),
@@ -719,9 +739,9 @@
     end, {0, []}),
     lists:reverse(DocInfoList).
 
-get_missing_revs(#http_db{uri=DbUrl, headers=Headers}, DocIdRevsList) ->
+get_missing_revs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, DocIdRevsList) ->
     DocIdRevsList2 = [{Id, couch_doc:rev_to_strs(Revs)} || {Id, Revs} <- DocIdRevsList],
-    {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, Headers,
+    {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, Headers, OAuth,
             {DocIdRevsList2}),
     {DocMissingRevsList} = proplists:get_value(<<"missing_revs">>, ResponseMembers),
     DocMissingRevsList2 = [{Id, couch_doc:parse_revs(MissingRevStrs)} || {Id, MissingRevStrs} <- DocMissingRevsList],
@@ -730,9 +750,9 @@
     couch_db:get_missing_revs(Db, DocId).
 
 
-open_doc(#http_db{uri=DbUrl, headers=Headers}, DocId, Options) ->
+open_doc(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, DocId, Options) ->
     [] = Options,
-    case do_http_request(DbUrl ++ couch_util:url_encode(DocId), get, Headers) of
+    case do_http_request(DbUrl ++ couch_util:url_encode(DocId), get, Headers, OAuth) of
     {[{<<"error">>, ErrId}, {<<"reason">>, Reason}]} ->
         {couch_util:to_existing_atom(ErrId), Reason};
     Doc  ->
@@ -741,7 +761,7 @@
 open_doc(Db, DocId, Options) ->
     couch_db:open_doc(Db, DocId, Options).
 
-open_doc_revs(#http_db{uri=DbUrl, headers=Headers} = DbS, DocId, Revs0,
+open_doc_revs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth} = DbS, DocId, Revs0,
         [latest]) ->
     Revs = couch_doc:rev_to_strs(Revs0),
     BaseUrl = DbUrl ++ couch_util:url_encode(DocId) ++ "?revs=true&latest=true",
@@ -752,18 +772,18 @@
     JsonResults = case length(Revs) > MaxN of
     false ->
         Url = ?l2b(BaseUrl ++ "&open_revs=" ++ ?JSON_ENCODE(Revs)),
-        do_http_request(Url, get, Headers);
+        do_http_request(Url, get, Headers, OAuth);
     true ->
         {_, Rest, Acc} = lists:foldl(
         fun(Rev, {Count, RevsAcc, AccResults}) when Count =:= MaxN ->
             QSRevs = ?JSON_ENCODE(lists:reverse(RevsAcc)),
             Url = ?l2b(BaseUrl ++ "&open_revs=" ++ QSRevs),
-            {1, [Rev], AccResults++do_http_request(Url, get, Headers)};
+            {1, [Rev], AccResults++do_http_request(Url, get, Headers, OAuth)};
         (Rev, {Count, RevsAcc, AccResults}) ->
             {Count+1, [Rev|RevsAcc], AccResults}
         end, {0, [], []}, Revs),
         Acc ++ do_http_request(?l2b(BaseUrl ++ "&open_revs=" ++
-            ?JSON_ENCODE(lists:reverse(Rest))), get, Headers)
+            ?JSON_ENCODE(lists:reverse(Rest))), get, Headers, OAuth)
     end,
 
     Results =
@@ -825,10 +845,10 @@
     lists:foldl(fun({_Id, Size, _NRefs}, Acc) -> Size+Acc end,
         0, element(2,process_info(Pid, binary))).
 
-update_doc(#http_db{uri=DbUrl, headers=Headers}, #doc{id=DocId}=Doc, Options) ->
+update_doc(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, #doc{id=DocId}=Doc, Options) ->
     [] = Options,
     Url = DbUrl ++ couch_util:url_encode(DocId),
-    {ResponseMembers} = do_http_request(Url, put, Headers,
+    {ResponseMembers} = do_http_request(Url, put, Headers, OAuth,
             couch_doc:to_json_obj(Doc, [attachments])),
     Rev = proplists:get_value(<<"rev">>, ResponseMembers),
     {ok, couch_doc:parse_rev(Rev)};
@@ -837,10 +857,10 @@
 
 update_docs(_, [], _, _) ->
     {ok, []};
-update_docs(#http_db{uri=DbUrl, headers=Headers}, Docs, [], replicated_changes) ->
+update_docs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, Docs, [], replicated_changes) ->
     JsonDocs = [couch_doc:to_json_obj(Doc, [revs,attachments]) || Doc <- Docs],
     ErrorsJson =
-        do_http_request(DbUrl ++ "_bulk_docs", post, Headers,
+        do_http_request(DbUrl ++ "_bulk_docs", post, Headers, OAuth,
                 {[{new_edits, false}, {docs, JsonDocs}]}),
     ErrorsList =
     lists:map(

Modified: couchdb/trunk/src/couchdb/couch_util.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_util.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_util.erl (original)
+++ couchdb/trunk/src/couchdb/couch_util.erl Tue Aug  4 19:50:46 2009
@@ -16,7 +16,8 @@
 -export([should_flush/0, should_flush/1, to_existing_atom/1]).
 -export([new_uuid/0, rand32/0, implode/2, collate/2, collate/3]).
 -export([abs_pathname/1,abs_pathname/2, trim/1, ascii_lower/1]).
--export([encodeBase64/1, decodeBase64/1, to_hex/1,parse_term/1,dict_find/3]).
+-export([encodeBase64/1, decodeBase64/1, encodeBase64Url/1, decodeBase64Url/1,
+    to_hex/1,parse_term/1, dict_find/3]).
 -export([file_read_size/1, get_nested_json_value/2, json_user_ctx/1]).
 -export([to_binary/1, to_list/1, url_encode/1]).
 
@@ -259,6 +260,23 @@
 encodeBase64(<<>>, Acc) ->
     Acc.
 
+encodeBase64Url(Bs) when list(Bs) ->
+    encodeBase64Url(list_to_binary(Bs), <<>>);
+encodeBase64Url(Bs) ->
+    encodeBase64Url(Bs, <<>>).
+    
+encodeBase64Url(<<B:3/binary, Bs/binary>>, Acc) ->
+    <<C1:6, C2:6, C3:6, C4:6>> = B,
+    encodeBase64Url(Bs, <<Acc/binary, (encUrl(C1)), (encUrl(C2)), (encUrl(C3)), (encUrl(C4))>>);
+encodeBase64Url(<<B:2/binary>>, Acc) ->
+    <<C1:6, C2:6, C3:6, _:6>> = <<B/binary, 0>>,
+    <<Acc/binary, (encUrl(C1)), (encUrl(C2)), (encUrl(C3))>>;
+encodeBase64Url(<<B:1/binary>>, Acc) ->
+    <<C1:6, C2:6, _:12>> = <<B/binary, 0, 0>>,
+    <<Acc/binary, (encUrl(C1)), (encUrl(C2))>>;
+encodeBase64Url(<<>>, Acc) ->
+    Acc.
+
 %%
 %% decodeBase64(BinaryChars) -> Binary
 %%
@@ -279,6 +297,23 @@
 decode1(<<>>, Acc) ->
     Acc.
 
+decodeBase64Url(Cs) when is_list(Cs)->
+    decodeBase64Url(list_to_binary(Cs));
+decodeBase64Url(Cs) ->
+    decode1Url(Cs, <<>>).
+
+decode1Url(<<C1, C2>>, Acc) ->
+    <<B1, _:16>> = <<(decUrl(C1)):6, (decUrl(C2)):6, 0:12>>,
+    <<Acc/binary, B1>>;
+decode1Url(<<C1, C2, C3>>, Acc) ->
+    <<B1, B2, _:8>> = <<(decUrl(C1)):6, (decUrl(C2)):6, (decUrl(C3)):6, (decUrl(0)):6>>,
+    <<Acc/binary, B1, B2>>;
+decode1Url(<<C1, C2, C3, C4, Cs/binary>>, Acc) ->
+    Bin = <<Acc/binary, (decUrl(C1)):6, (decUrl(C2)):6, (decUrl(C3)):6, (decUrl(C4)):6>>,
+    decode1Url(Cs, Bin);
+decode1Url(<<>>, Acc) ->
+    Acc.
+
 %% enc/1 and dec/1
 %%
 %% Mapping: 0-25 -> A-Z, 26-51 -> a-z, 52-61 -> 0-9, 62 -> +, 63 -> /
@@ -289,6 +324,15 @@
 dec(C) ->
     62*?st(C,43) + ?st(C,47) + (C-59)*?st(C,48) - 69*?st(C,65) - 6*?st(C,97).
 
+%% encUrl/1 and decUrl/1
+%%
+%% Mapping: 0-25 -> A-Z, 26-51 -> a-z, 52-61 -> 0-9, 62 -> -, 63 -> _
+%%
+encUrl(C) ->
+    65 + C + 6*?st(C,26) - 75*?st(C,52) -13*?st(C,62) + 49*?st(C,63).
+
+decUrl(C) ->
+    62*?st(C,45) + (C-58)*?st(C,48) - 69*?st(C,65) + 33*?st(C,95) - 39*?st(C,97).
 
 dict_find(Key, Dict, DefaultValue) ->
     case dict:find(Key, Dict) of

Added: couchdb/trunk/src/erlang-oauth/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/Makefile.am?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/Makefile.am (added)
+++ couchdb/trunk/src/erlang-oauth/Makefile.am Tue Aug  4 19:50:46 2009
@@ -0,0 +1,47 @@
+## Licensed under the Apache License, Version 2.0 (the "License"); you may not
+## use this file except in compliance with the License.  You may obtain a copy
+## of the License at
+##
+##   http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+## WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+## License for the specific language governing permissions and limitations under
+## the License.
+
+oauthebindir = $(localerlanglibdir)/erlang-oauth/ebin
+
+oauth_file_collection = \
+    oauth.erl \
+    oauth_hmac_sha1.erl \
+    oauth_http.erl \
+    oauth_plaintext.erl \
+    oauth_rsa_sha1.erl \
+    oauth_unix.erl \
+    oauth_uri.erl
+
+oauthebin_static_file = oauth.app
+
+oauthebin_make_generated_file_list = \
+    oauth.beam \
+    oauth_hmac_sha1.beam \
+    oauth_http.beam \
+    oauth_plaintext.beam \
+    oauth_rsa_sha1.beam \
+    oauth_unix.beam \
+    oauth_uri.beam
+
+oauthebin_DATA = \
+    $(oauthebin_static_file) \
+    $(oauthebin_make_generated_file_list)
+
+EXTRA_DIST =  \
+    $(oauth_file_collection) \
+    $(oauthebin_static_file)
+
+CLEANFILES = \
+    $(oauthebin_make_generated_file_list)
+
+%.beam: %.erl
+	$(ERLC) $(ERLC_FLAGS) $<

Added: couchdb/trunk/src/erlang-oauth/oauth.app
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth.app?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth.app (added)
+++ couchdb/trunk/src/erlang-oauth/oauth.app Tue Aug  4 19:50:46 2009
@@ -0,0 +1,20 @@
+{application, oauth, [
+  {description, "Erlang OAuth implementation"},
+  {vsn, "dev"},
+  {modules, [
+    oauth,
+    oauth_hmac_sha1,
+    oauth_http,
+    oauth_plaintext,
+    oauth_rsa_sha1,
+    oauth_unix,
+    oauth_uri
+  ]},
+  {registered, []},
+  {applications, [
+    kernel,
+    stdlib,
+    crypto,
+    inets
+  ]}
+]}.

Added: couchdb/trunk/src/erlang-oauth/oauth.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,107 @@
+-module(oauth).
+
+-export(
+  [ get/5
+  , header/1
+  , post/5
+  , signature/5
+  , signature_base_string/3
+  , signed_params/6
+  , token/1
+  , token_secret/1
+  , uri/2
+  , verify/6
+  ]).
+
+
+get(URL, ExtraParams, Consumer, Token, TokenSecret) ->
+  SignedParams = signed_params("GET", URL, ExtraParams, Consumer, Token, TokenSecret),
+  oauth_http:get(uri(URL, SignedParams)).
+
+post(URL, ExtraParams, Consumer, Token, TokenSecret) ->
+  SignedParams = signed_params("POST", URL, ExtraParams, Consumer, Token, TokenSecret),
+  oauth_http:post(URL, oauth_uri:params_to_string(SignedParams)).
+
+uri(Base, []) ->
+  Base;
+uri(Base, Params) ->
+  lists:concat([Base, "?", oauth_uri:params_to_string(Params)]).
+
+header(Params) ->
+  {"Authorization", "OAuth " ++ oauth_uri:params_to_header_string(Params)}.
+
+token(Params) ->
+  proplists:get_value("oauth_token", Params).
+
+token_secret(Params) ->
+  proplists:get_value("oauth_token_secret", Params).
+
+verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) ->
+  case signature_method(Consumer) of
+    plaintext ->
+      oauth_plaintext:verify(Signature, consumer_secret(Consumer), TokenSecret);
+    hmac_sha1 ->
+      BaseString = signature_base_string(HttpMethod, URL, Params),
+      oauth_hmac_sha1:verify(Signature, BaseString, consumer_secret(Consumer), TokenSecret);
+    rsa_sha1 ->
+      BaseString = signature_base_string(HttpMethod, URL, Params),
+      oauth_rsa_sha1:verify(Signature, BaseString, consumer_secret(Consumer))
+  end.
+
+signed_params(HttpMethod, URL, ExtraParams, Consumer, Token, TokenSecret) ->
+  Params = token_param(Token, params(Consumer, ExtraParams)),
+  [{"oauth_signature", signature(HttpMethod, URL, Params, Consumer, TokenSecret)}|Params].
+
+signature(HttpMethod, URL, Params, Consumer, TokenSecret) ->
+  case signature_method(Consumer) of
+    plaintext ->
+      oauth_plaintext:signature(consumer_secret(Consumer), TokenSecret);
+    hmac_sha1 ->
+      BaseString = signature_base_string(HttpMethod, URL, Params),
+      oauth_hmac_sha1:signature(BaseString, consumer_secret(Consumer), TokenSecret);
+    rsa_sha1 ->
+      BaseString = signature_base_string(HttpMethod, URL, Params),
+      oauth_rsa_sha1:signature(BaseString, consumer_secret(Consumer))
+  end.
+
+signature_base_string(HttpMethod, URL, Params) ->
+  NormalizedURL = oauth_uri:normalize(URL),
+  NormalizedParams = oauth_uri:params_to_string(lists:sort(Params)),
+  oauth_uri:calate("&", [HttpMethod, NormalizedURL, NormalizedParams]).
+
+token_param("", Params) ->
+  Params;
+token_param(Token, Params) ->
+  [{"oauth_token", Token}|Params].
+
+params(Consumer, Params) ->
+  Nonce = base64:encode_to_string(crypto:rand_bytes(32)), % cf. ruby-oauth
+  params(Consumer, oauth_unix:timestamp(), Nonce, Params).
+
+params(Consumer, Timestamp, Nonce, Params) ->
+  [ {"oauth_version", "1.0"}
+  , {"oauth_nonce", Nonce}
+  , {"oauth_timestamp", integer_to_list(Timestamp)}
+  , {"oauth_signature_method", signature_method_string(Consumer)}
+  , {"oauth_consumer_key", consumer_key(Consumer)}
+  | Params
+  ].
+
+signature_method_string(Consumer) ->
+  case signature_method(Consumer) of
+    plaintext ->
+      "PLAINTEXT";
+    hmac_sha1 ->
+      "HMAC-SHA1";
+    rsa_sha1 ->
+      "RSA-SHA1"
+  end.
+
+signature_method(_Consumer={_, _, Method}) ->
+  Method.
+
+consumer_secret(_Consumer={_, Secret, _}) ->
+  Secret.
+
+consumer_key(_Consumer={Key, _, _}) ->
+  Key.

Added: couchdb/trunk/src/erlang-oauth/oauth_hmac_sha1.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_hmac_sha1.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_hmac_sha1.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_hmac_sha1.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,11 @@
+-module(oauth_hmac_sha1).
+
+-export([signature/3, verify/4]).
+
+
+signature(BaseString, CS, TS) ->
+  Key = oauth_uri:calate("&", [CS, TS]),
+  base64:encode_to_string(crypto:sha_mac(Key, BaseString)).
+
+verify(Signature, BaseString, CS, TS) ->
+  Signature =:= signature(BaseString, CS, TS).

Added: couchdb/trunk/src/erlang-oauth/oauth_http.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_http.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_http.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_http.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,22 @@
+-module(oauth_http).
+
+-export([get/1, post/2, response_params/1, response_body/1, response_code/1]).
+
+
+get(URL) ->
+  request(get, {URL, []}).
+
+post(URL, Data) ->
+  request(post, {URL, [], "application/x-www-form-urlencoded", Data}).
+
+request(Method, Request) ->
+  http:request(Method, Request, [{autoredirect, false}], []).
+
+response_params(Response) ->
+  oauth_uri:params_from_string(response_body(Response)).
+
+response_body({{_, _, _}, _, Body}) ->
+  Body.
+
+response_code({{_, Code, _}, _, _}) ->
+  Code.

Added: couchdb/trunk/src/erlang-oauth/oauth_plaintext.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_plaintext.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_plaintext.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_plaintext.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,10 @@
+-module(oauth_plaintext).
+
+-export([signature/2, verify/3]).
+
+
+signature(CS, TS) ->
+  oauth_uri:calate("&", [CS, TS]).
+
+verify(Signature, CS, TS) ->
+  Signature =:= signature(CS, TS).

Added: couchdb/trunk/src/erlang-oauth/oauth_rsa_sha1.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_rsa_sha1.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_rsa_sha1.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_rsa_sha1.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,30 @@
+-module(oauth_rsa_sha1).
+
+-export([signature/2, verify/3]).
+
+-include_lib("public_key/include/public_key.hrl").
+
+
+signature(BaseString, PrivateKeyPath) ->
+  {ok, [Info]} = public_key:pem_to_der(PrivateKeyPath),
+  {ok, PrivateKey} = public_key:decode_private_key(Info),
+  base64:encode_to_string(public_key:sign(list_to_binary(BaseString), PrivateKey)).
+
+verify(Signature, BaseString, PublicKey) ->
+  public_key:verify_signature(to_binary(BaseString), sha, base64:decode(Signature), public_key(PublicKey)).
+
+to_binary(Term) when is_list(Term) ->
+  list_to_binary(Term);
+to_binary(Term) when is_binary(Term) ->
+  Term.
+
+public_key(Path) when is_list(Path) ->
+  {ok, [{cert, DerCert, not_encrypted}]} = public_key:pem_to_der(Path),
+  {ok, Cert} = public_key:pkix_decode_cert(DerCert, otp),
+  public_key(Cert);
+public_key(#'OTPCertificate'{tbsCertificate=Cert}) ->
+  public_key(Cert);
+public_key(#'OTPTBSCertificate'{subjectPublicKeyInfo=Info}) ->
+  public_key(Info);
+public_key(#'OTPSubjectPublicKeyInfo'{subjectPublicKey=Key}) ->
+  Key.

Added: couchdb/trunk/src/erlang-oauth/oauth_unix.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_unix.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_unix.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_unix.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,16 @@
+-module(oauth_unix).
+
+-export([timestamp/0]).
+
+
+timestamp() ->
+  timestamp(calendar:universal_time()).
+
+timestamp(DateTime) ->
+  seconds(DateTime) - epoch().
+
+epoch() ->
+  seconds({{1970,1,1},{00,00,00}}).
+
+seconds(DateTime) ->
+  calendar:datetime_to_gregorian_seconds(DateTime).

Added: couchdb/trunk/src/erlang-oauth/oauth_uri.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/erlang-oauth/oauth_uri.erl?rev=800938&view=auto
==============================================================================
--- couchdb/trunk/src/erlang-oauth/oauth_uri.erl (added)
+++ couchdb/trunk/src/erlang-oauth/oauth_uri.erl Tue Aug  4 19:50:46 2009
@@ -0,0 +1,88 @@
+-module(oauth_uri).
+
+-export([normalize/1, calate/2, encode/1]).
+-export([params_from_string/1, params_to_string/1,
+  params_from_header_string/1, params_to_header_string/1]).
+
+-import(lists, [concat/1]).
+
+-define(is_uppercase_alpha(C), C >= $A, C =< $Z).
+-define(is_lowercase_alpha(C), C >= $a, C =< $z).
+-define(is_alpha(C), ?is_uppercase_alpha(C); ?is_lowercase_alpha(C)).
+-define(is_digit(C), C >= $0, C =< $9).
+-define(is_alphanumeric(C), ?is_alpha(C); ?is_digit(C)).
+-define(is_unreserved(C), ?is_alphanumeric(C); C =:= $-; C =:= $_; C =:= $.; C =:= $~).
+-define(is_hex(C), ?is_digit(C); C >= $A, C =< $F).
+
+
+normalize(URI) ->
+  case http_uri:parse(URI) of
+    {Scheme, UserInfo, Host, Port, Path, _Query} ->
+      normalize(Scheme, UserInfo, string:to_lower(Host), Port, [Path]);
+    Else ->
+      Else
+  end.
+
+normalize(http, UserInfo, Host, 80, Acc) ->
+  normalize(http, UserInfo, [Host|Acc]);
+normalize(https, UserInfo, Host, 443, Acc) ->
+  normalize(https, UserInfo, [Host|Acc]);
+normalize(Scheme, UserInfo, Host, Port, Acc) ->
+  normalize(Scheme, UserInfo, [Host, ":", Port|Acc]).
+
+normalize(Scheme, [], Acc) ->
+  concat([Scheme, "://"|Acc]);
+normalize(Scheme, UserInfo, Acc) ->
+  concat([Scheme, "://", UserInfo, "@"|Acc]).
+
+params_to_header_string(Params) ->
+  intercalate(", ", [concat([encode(K), "=\"", encode(V), "\""]) || {K, V} <- Params]).
+
+params_from_header_string(String) ->
+  [param_from_header_string(Param) || Param <- re:split(String, ",\\s*", [{return, list}]), Param =/= ""].
+
+param_from_header_string(Param) ->
+  [Key, QuotedValue] = string:tokens(Param, "="),
+  Value = string:substr(QuotedValue, 2, length(QuotedValue) - 2),
+  {decode(Key), decode(Value)}.
+
+params_from_string(Params) ->
+  [param_from_string(Param) || Param <- string:tokens(Params, "&")].
+
+param_from_string(Param) ->
+  list_to_tuple([decode(Value) || Value <- string:tokens(Param, "=")]).
+
+params_to_string(Params) ->
+  intercalate("&", [calate("=", [K, V]) || {K, V} <- Params]).
+
+calate(Sep, Xs) ->
+  intercalate(Sep, [encode(X) || X <- Xs]).
+
+intercalate(Sep, Xs) ->
+  concat(intersperse(Sep, Xs)).
+
+intersperse(_, []) -> [];
+intersperse(_, [X]) -> [X];
+intersperse(Sep, [X|Xs]) ->
+  [X, Sep|intersperse(Sep, Xs)].
+
+decode(Chars) ->
+  decode(Chars, []).
+
+decode([], Decoded) ->
+  lists:reverse(Decoded);
+decode([$%,A,B|Etc], Decoded) when ?is_hex(A), ?is_hex(B) ->
+  decode(Etc, [erlang:list_to_integer([A,B], 16)|Decoded]);
+decode([C|Etc], Decoded) when ?is_unreserved(C) ->
+  decode(Etc, [C|Decoded]).
+
+encode(Chars) ->
+  encode(Chars, []).
+
+encode([], Encoded) ->
+  lists:flatten(lists:reverse(Encoded));
+encode([C|Etc], Encoded) when ?is_unreserved(C) ->
+  encode(Etc, [C|Encoded]);
+encode([C|Etc], Encoded) ->
+  Value = io_lib:format("%~2.1.0s", [erlang:integer_to_list(C, 16)]),
+  encode(Etc, [Value|Encoded]).

Modified: couchdb/trunk/src/mochiweb/mochiweb_cookies.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_cookies.erl?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_cookies.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_cookies.erl Tue Aug  4 19:50:46 2009
@@ -32,7 +32,7 @@
 %% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
 %% where Option = {max_age, integer()} | {local_time, {date(), time()}}
 %%                | {domain, string()} | {path, string()}
-%%                | {secure, true | false}
+%%                | {secure, true | false} | {http_only, true | false}
 %%
 %% @doc Generate a Set-Cookie header field tuple.
 cookie(Key, Value, Options) ->
@@ -83,7 +83,14 @@
             Path ->
                 ["; Path=", quote(Path)]
         end,
-    CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart],
+    HttpOnlyPart =
+        case proplists:get_value(http_only, Options) of
+            true ->
+                "; HttpOnly";
+            _ ->
+                ""
+        end,
+    CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart, HttpOnlyPart],
     {"Set-Cookie", lists:flatten(CookieParts)}.
 
 

Modified: couchdb/trunk/test/runner.sh
URL: http://svn.apache.org/viewvc/couchdb/trunk/test/runner.sh?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/test/runner.sh (original)
+++ couchdb/trunk/test/runner.sh Tue Aug  4 19:50:46 2009
@@ -19,5 +19,5 @@
 cat ../share/www/script/couch.js \
     ../share/www/script/couch_test_runner.js \
     ../share/www/script/couch_tests.js \
-    ../share/www/script/test/* test.js \
+    ../share/www/script/test/*.js test.js \
     | ../src/couchdb/couchjs -

Modified: couchdb/trunk/utils/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/utils/Makefile.am?rev=800938&r1=800937&r2=800938&view=diff
==============================================================================
--- couchdb/trunk/utils/Makefile.am (original)
+++ couchdb/trunk/utils/Makefile.am Tue Aug  4 19:50:46 2009
@@ -25,6 +25,7 @@
 	    -e "s|%mochiwebebindir%|mochiweb|g" \
 	    -e "s|%couchdbebindir%|couchdb|g" \
 	    -e "s|%ibrowseebindir%|ibrowse|g" \
+	    -e "s|%oauthebindir%|erlang-oauth|g" \
 	    -e "s|%defaultini%|default_dev.ini|g" \
 	    -e "s|%localini%|local_dev.ini|g" \
 	    -e "s|%localerlanglibdir%|$(abs_top_srcdir)/src|g" \



Mime
View raw message