You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by kx...@apache.org on 2015/12/21 22:38:57 UTC

[2/2] chttpd commit: updated refs/heads/master to 417679a

Rewrite via query server

COUCHDB-2874


Project: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/commit/9f267e1e
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/tree/9f267e1e
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/diff/9f267e1e

Branch: refs/heads/master
Commit: 9f267e1edd70e7dbd043e5a76988a2436c4465e5
Parents: d31b2a4
Author: ermouth <er...@gmail.com>
Authored: Fri Oct 2 14:00:53 2015 +0300
Committer: Alexander Shorin <kx...@apache.org>
Committed: Tue Dec 22 00:23:00 2015 +0300

----------------------------------------------------------------------
 src/chttpd_external.erl |  89 +++++++-----
 src/chttpd_rewrite.erl  | 313 ++++++++++++++++++++++---------------------
 2 files changed, 217 insertions(+), 185 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/9f267e1e/src/chttpd_external.erl
----------------------------------------------------------------------
diff --git a/src/chttpd_external.erl b/src/chttpd_external.erl
index 9df120f..3802c97 100644
--- a/src/chttpd_external.erl
+++ b/src/chttpd_external.erl
@@ -13,7 +13,8 @@
 -module(chttpd_external).
 
 -export([handle_external_req/2, handle_external_req/3]).
--export([send_external_response/2, json_req_obj/2, json_req_obj/3]).
+-export([send_external_response/2]).
+-export([json_req_obj_fields/0, json_req_obj/2, json_req_obj/3, json_req_obj/4]).
 -export([default_or_content_type/2, parse_external_response/1]).
 
 -import(chttpd,[send_error/4]).
@@ -54,46 +55,66 @@ process_external_req(HttpReq, Db, Name) ->
         send_external_response(HttpReq, Response)
     end.
 
-json_req_obj(Req, Db) -> json_req_obj(Req, Db, null).
-json_req_obj(#httpd{mochi_req=Req,
-               method=Method,
-               requested_path_parts=RequestedPath,
-               path_parts=Path,
-               req_body=ReqBody
-            }, Db, DocId) ->
-    Body = case ReqBody of
-        undefined ->
-            MaxSize = list_to_integer(
-                config:get("couchdb", "max_document_size", "4294967296")),
-            Req:recv_body(MaxSize);
-        Else -> Else
-    end,
+
+json_req_obj(Req, Db) ->
+    json_req_obj(Req, Db, null).
+json_req_obj(Req, Db, DocId) ->
+    json_req_obj(Req, Db, DocId, all).
+json_req_obj(Req, Db, DocId, all) ->
+    Fields = json_req_obj_fields(),
+    json_req_obj(Req, Db, DocId, Fields);
+json_req_obj(Req, Db, DocId, Fields) when is_list(Fields) ->
+    {[{Field, json_req_obj_field(Field, Req, Db, DocId)} || Field <- Fields]}.
+
+json_req_obj_fields() ->
+    [<<"info">>, <<"uuid">>, <<"id">>, <<"method">>, <<"requested_path">>,
+     <<"path">>, <<"raw_path">>, <<"query">>, <<"headers">>, <<"body">>,
+     <<"peer">>, <<"form">>, <<"cookie">>, <<"userCtx">>, <<"secObj">>].
+
+json_req_obj_field(<<"info">>, #httpd{}, Db, _DocId) ->
+    {ok, Info} = get_db_info(Db),
+    {Info};
+json_req_obj_field(<<"uuid">>, #httpd{}, _Db, _DocId) ->
+    couch_uuids:new();
+json_req_obj_field(<<"id">>, #httpd{}, _Db, DocId) ->
+    DocId;
+json_req_obj_field(<<"method">>, #httpd{method=Method}, _Db, _DocId) ->
+    Method;
+json_req_obj_field(<<"requested_path">>, #httpd{requested_path_parts=Path}, _Db, _DocId) ->
+    Path;
+json_req_obj_field(<<"path">>, #httpd{path_parts=Path}, _Db, _DocId) ->
+    Path;
+json_req_obj_field(<<"raw_path">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+    ?l2b(Req:get(raw_path));
+json_req_obj_field(<<"query">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+    json_query_keys(to_json_terms(Req:parse_qs()));
+json_req_obj_field(<<"headers">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+    Headers = Req:get(headers),
+    Hlist = mochiweb_headers:to_list(Headers),
+    to_json_terms(Hlist);
+json_req_obj_field(<<"body">>, #httpd{req_body=undefined, mochi_req=Req}, _Db, _DocId) ->
+    MaxSize = config:get_integer("couchdb", "max_document_size", 4294967296),
+    Req:recv_body(MaxSize);
+json_req_obj_field(<<"body">>, #httpd{req_body=Body}, _Db, _DocId) ->
+    Body;
+json_req_obj_field(<<"peer">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+    ?l2b(Req:get(peer));
+json_req_obj_field(<<"form">>, #httpd{mochi_req=Req, method=Method}=HttpReq, Db, DocId) ->
+    Body = json_req_obj_field(<<"body">>, HttpReq, Db, DocId),
     ParsedForm = case Req:get_primary_header_value("content-type") of
         "application/x-www-form-urlencoded" ++ _ when Method =:= 'POST' orelse Method =:= 'PUT' ->
             mochiweb_util:parse_qs(Body);
         _ ->
             []
     end,
-    Headers = Req:get(headers),
-    Hlist = mochiweb_headers:to_list(Headers),
-    {ok, Info} = get_db_info(Db),
+    to_json_terms(ParsedForm);
+json_req_obj_field(<<"cookie">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+    to_json_terms(Req:parse_cookie());
+json_req_obj_field(<<"userCtx">>, #httpd{}, Db, _DocId) ->
+    couch_util:json_user_ctx(Db);
+json_req_obj_field(<<"secObj">>, #httpd{}, Db, _DocId) ->
+    couch_db:get_security(Db).
 
-    % add headers...
-    {[{<<"info">>, {Info}},
-        {<<"uuid">>, couch_uuids:new()},
-        {<<"id">>, DocId},
-        {<<"method">>, Method},
-        {<<"requested_path">>, RequestedPath},
-        {<<"path">>, Path},
-        {<<"raw_path">>, ?l2b(Req:get(raw_path))},
-        {<<"query">>, json_query_keys(to_json_terms(Req:parse_qs()))},
-        {<<"headers">>, to_json_terms(Hlist)},
-        {<<"body">>, Body},
-        {<<"peer">>, ?l2b(Req:get(peer))},
-        {<<"form">>, to_json_terms(ParsedForm)},
-        {<<"cookie">>, to_json_terms(Req:parse_cookie())},
-        {<<"userCtx">>, couch_util:json_user_ctx(Db)},
-        {<<"secObj">>, couch_db:get_security(Db)}]}.
 
 get_db_info(#db{main_pid = nil} = Db) ->
     fabric:get_db_info(Db);

http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/9f267e1e/src/chttpd_rewrite.erl
----------------------------------------------------------------------
diff --git a/src/chttpd_rewrite.erl b/src/chttpd_rewrite.erl
index a1718d4..7b72884 100644
--- a/src/chttpd_rewrite.erl
+++ b/src/chttpd_rewrite.erl
@@ -23,102 +23,7 @@
 -define(MATCH_ALL, {bind, <<"*">>}).
 
 
-%% doc The http rewrite handler. All rewriting is done from
-%% /dbname/_design/ddocname/_rewrite by default.
-%%
-%% each rules should be in rewrites member of the design doc.
-%% Ex of a complete rule :
-%%
-%%  {
-%%      ....
-%%      "rewrites": [
-%%      {
-%%          "from": "",
-%%          "to": "index.html",
-%%          "method": "GET",
-%%          "query": {}
-%%      }
-%%      ]
-%%  }
-%%
-%%  from: is the path rule used to bind current uri to the rule. It
-%% use pattern matching for that.
-%%
-%%  to: rule to rewrite an url. It can contain variables depending on binding
-%% variables discovered during pattern matching and query args (url args and from
-%% the query member.)
-%%
-%%  method: method to bind the request method to the rule. by default "*"
-%%  query: query args you want to define they can contain dynamic variable
-%% by binding the key to the bindings
-%%
-%%
-%% to and from are path with  patterns. pattern can be string starting with ":" or
-%% "*". ex:
-%% /somepath/:var/*
-%%
-%% This path is converted in erlang list by splitting "/". Each var are
-%% converted in atom. "*" is converted to '*' atom. The pattern matching is done
-%% by splitting "/" in request url in a list of token. A string pattern will
-%% match equal token. The star atom ('*' in single quotes) will match any number
-%% of tokens, but may only be present as the last pathtern in a pathspec. If all
-%% tokens are matched and all pathterms are used, then the pathspec matches. It works
-%% like webmachine. Each identified token will be reused in to rule and in query
-%%
-%% The pattern matching is done by first matching the request method to a rule. by
-%% default all methods match a rule. (method is equal to "*" by default). Then
-%% It will try to match the path to one rule. If no rule match, then a 404 error
-%% is displayed.
-%%
-%% Once a rule is found we rewrite the request url using the "to" and
-%% "query" members. The identified token are matched to the rule and
-%% will replace var. if '*' is found in the rule it will contain the remaining
-%% part if it exists.
-%%
-%% Examples:
-%%
-%% Dispatch rule            URL             TO                  Tokens
-%%
-%% {"from": "/a/b",         /a/b?k=v        /some/b?k=v         var =:= b
-%% "to": "/some/"}                                              k = v
-%%
-%% {"from": "/a/b",         /a/b            /some/b?var=b       var =:= b
-%% "to": "/some/:var"}
-%%
-%% {"from": "/a",           /a              /some
-%% "to": "/some/*"}
-%%
-%% {"from": "/a/*",         /a/b/c          /some/b/c
-%% "to": "/some/*"}
-%%
-%% {"from": "/a",           /a              /some
-%% "to": "/some/*"}
-%%
-%% {"from": "/a/:foo/*",    /a/b/c          /some/b/c?foo=b     foo =:= b
-%% "to": "/some/:foo/*"}
-%%
-%% {"from": "/a/:foo",     /a/b             /some/?k=b&foo=b    foo =:= b
-%% "to": "/some",
-%%  "query": {
-%%      "k": ":foo"
-%%  }}
-%%
-%% {"from": "/a",           /a?foo=b        /some/b             foo =:= b
-%% "to": "/some/:foo",
-%%  }}
-
-
-
-handle_rewrite_req(#httpd{
-        path_parts=[DbName, <<"_design">>, DesignName, _Rewrite|PathParts],
-        method=Method,
-        mochi_req=MochiReq}=Req, _Db, DDoc) ->
-
-    % we are in a design handler
-    DesignId = <<"_design/", DesignName/binary>>,
-    Prefix = <<"/", DbName/binary, "/", DesignId/binary>>,
-    QueryList = lists:map(fun decode_query_value/1, chttpd:qs(Req)),
-
+handle_rewrite_req(#httpd{}=Req, Db, DDoc) ->
     RewritesSoFar = erlang:get(?REWRITE_COUNT),
     MaxRewrites = config:get_integer("httpd", "rewrite_limit", 100),
     case RewritesSoFar >= MaxRewrites of
@@ -127,67 +32,170 @@ handle_rewrite_req(#httpd{
         false ->
             erlang:put(?REWRITE_COUNT, RewritesSoFar + 1)
     end,
+    case get_rules(DDoc) of
+        Rules when is_list(Rules) ->
+            do_rewrite(Req, Rules);
+        Rules when is_binary(Rules) ->
+            case couch_query_servers:rewrite(Req, Db, DDoc) of
+                undefined ->
+                    chttpd:send_error(Req, 404, <<"rewrite_error">>,
+                        <<"Invalid path.">>);
+                Rewrite ->
+                    do_rewrite(Req, Rewrite)
+            end;
+        undefined ->
+            chttpd:send_error(Req, 404, <<"rewrite_error">>,
+                <<"Invalid path.">>)
+    end.
 
-    #doc{body={Props}} = DDoc,
 
-    % get rules from ddoc
-    case couch_util:get_value(<<"rewrites">>, Props) of
-        undefined ->
-            couch_httpd:send_error(Req, 404, <<"rewrite_error">>,
-                <<"Invalid path.">>);
-        Bin when is_binary(Bin) ->
-            couch_httpd:send_error(Req, 400, <<"rewrite_error">>,
-                <<"Rewrite rules are a String. They must be a JSON Array.">>);
-        Rules ->
-            % create dispatch list from rules
-            DispatchList =  [make_rule(Rule) || {Rule} <- Rules],
-            Method1 = couch_util:to_binary(Method),
-
-            %% get raw path by matching url to a rule.
-            RawPath = case try_bind_path(DispatchList, Method1,
-                    PathParts, QueryList) of
-                no_dispatch_path ->
-                    throw(not_found);
-                {NewPathParts, Bindings} ->
-                    Parts = [quote_plus(X) || X <- NewPathParts],
-
-                    % build new path, reencode query args, eventually convert
-                    % them to json
-                    Bindings1 = maybe_encode_bindings(Bindings),
-                    Path = binary_to_list(
-                        iolist_to_binary([
-                                string:join(Parts, [?SEPARATOR]),
-                                [["?", mochiweb_util:urlencode(Bindings1)]
-                                    || Bindings1 =/= [] ]
-                            ])),
-
-                    % if path is relative detect it and rewrite path
-                    case mochiweb_util:safe_relative_path(Path) of
-                        undefined ->
-                            ?b2l(Prefix) ++ "/" ++ Path;
-                        P1 ->
-                            ?b2l(Prefix) ++ "/" ++ P1
-                    end
+get_rules(#doc{body={Props}}) ->
+    couch_util:get_value(<<"rewrites">>, Props).
+
 
+do_rewrite(#httpd{mochi_req=MochiReq}=Req, {Props}=Rewrite) when is_list(Props) ->
+    case couch_util:get_value(<<"code">>, Props) of
+        undefined ->
+            Method = rewrite_method(Req, Rewrite),
+            Headers = rewrite_headers(Req, Rewrite),
+            Path = ?b2l(rewrite_path(Req, Rewrite)),
+            NewMochiReq = mochiweb_request:new(MochiReq:get(socket),
+                                               Method,
+                                               Path,
+                                               MochiReq:get(version),
+                                               Headers),
+            NewMochiReq:cleanup(),
+            couch_log:debug("rewrite to ~p", [Path]),
+            chttpd:handle_request_int(NewMochiReq);
+        Code ->
+            chttpd:send_response(
+                Req,
+                Code,
+                case couch_util:get_value(<<"headers">>, Props) of
+                    undefined -> [];
+                    {H1} -> H1
                 end,
+                rewrite_body(Rewrite))
+    end;
+do_rewrite(#httpd{method=Method,
+                  path_parts=[_DbName, <<"_design">>, _DesignName, _Rewrite|PathParts],
+                  mochi_req=MochiReq}=Req,
+           Rules) when is_list(Rules) ->
+    % create dispatch list from rules
+    Prefix = path_prefix(Req),
+    QueryList = lists:map(fun decode_query_value/1, chttpd:qs(Req)),
+
+    DispatchList =  [make_rule(Rule) || {Rule} <- Rules],
+    Method1 = couch_util:to_binary(Method),
+
+    %% get raw path by matching url to a rule.
+    RawPath = case try_bind_path(DispatchList, Method1,
+            PathParts, QueryList) of
+        no_dispatch_path ->
+            throw(not_found);
+        {NewPathParts, Bindings} ->
+            Parts = [quote_plus(X) || X <- NewPathParts],
+
+            % build new path, reencode query args, eventually convert
+            % them to json
+            Bindings1 = maybe_encode_bindings(Bindings),
+            Path = iolist_to_binary([
+                string:join(Parts, [?SEPARATOR]),
+                [["?", mochiweb_util:urlencode(Bindings1)] || Bindings1 =/= []]
+            ]),
+
+            % if path is relative detect it and rewrite path
+            safe_relative_path(Prefix, Path)
+        end,
+
+    % normalize final path (fix levels "." and "..")
+    RawPath1 = ?b2l(normalize_path(RawPath)),
+
+    couch_log:debug("rewrite to ~p ~n", [RawPath1]),
+
+    % build a new mochiweb request
+    MochiReq1 = mochiweb_request:new(MochiReq:get(socket),
+                                     MochiReq:get(method),
+                                     RawPath1,
+                                     MochiReq:get(version),
+                                     MochiReq:get(headers)),
+
+    % cleanup, It force mochiweb to reparse raw uri.
+    MochiReq1:cleanup(),
+
+    chttpd:handle_request_int(MochiReq1).
 
-            % normalize final path (fix levels "." and "..")
-            RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))),
 
-            couch_log:debug("rewrite to ~p ~n", [RawPath1]),
+rewrite_method(#httpd{method=Method}, {Props}) ->
+    DefaultMethod = couch_util:to_binary(Method),
+    couch_util:get_value(<<"method">>, Props, DefaultMethod).
 
-            % build a new mochiweb request
-            MochiReq1 = mochiweb_request:new(MochiReq:get(socket),
-                                             MochiReq:get(method),
-                                             RawPath1,
-                                             MochiReq:get(version),
-                                             MochiReq:get(headers)),
+rewrite_path(#httpd{}=Req, {Props}=Rewrite) ->
+    Prefix = path_prefix(Req),
+    RewritePath = case couch_util:get_value(<<"path">>, Props) of
+        undefined ->
+            throw({<<"rewrite_error">>,
+                   <<"Rewrite result must produce a new path.">>});
+        P -> P
+    end,
+    SafeRelativePath = safe_relative_path(Prefix, RewritePath),
+    NormalizedPath = normalize_path(SafeRelativePath),
+    QueryParams = rewrite_query_params(Req, Rewrite),
+    case QueryParams of
+        <<"">> ->
+            NormalizedPath;
+        QueryParams ->
+            <<NormalizedPath/binary, "?", QueryParams/binary>>
+    end.
+
+rewrite_query_params(#httpd{}=Req, {Props}) ->
+    RequestQS = chttpd:qs(Req),
+    RewriteQS = case couch_util:get_value(<<"query">>, Props) of
+        undefined -> RequestQS;
+        {V} -> V
+    end,
+    RewriteQSEsc = [{chttpd:quote(K), chttpd:quote(V)} || {K, V} <- RewriteQS],
+    iolist_to_binary([[K, "=", V] || {K, V} <- RewriteQSEsc]).
 
-            % cleanup, It force mochiweb to reparse raw uri.
-            MochiReq1:cleanup(),
+rewrite_headers(#httpd{mochi_req=MochiReq}, {Props}) ->
+    case couch_util:get_value(<<"headers">>, Props) of
+        undefined ->
+            MochiReq:get(headers);
+        {H} ->
+            mochiweb_headers:enter_from_list(
+                lists:map(fun({Key, Val}) -> {?b2l(Key), ?b2l(Val)} end, H),
+                MochiReq:get(headers))
+    end.
+
+rewrite_body({Props}) ->
+    Body = case couch_util:get_value(<<"body">>, Props) of
+        undefined -> erlang:get(mochiweb_request_body);
+        B -> B
+    end,
+    case Body of
+        undefined ->
+            [];
+        _ ->
+            erlang:put(mochiweb_request_body, Body),
+            Body
+    end.
+
+
+path_prefix(#httpd{path_parts=[DbName, <<"_design">>, DesignName | _]}) ->
+    EscapedDesignName = ?l2b(couch_util:url_encode(DesignName)),
+    EscapedDbName = ?l2b(couch_util:url_encode(DbName)),
+    DesignId = <<"_design/", EscapedDesignName/binary>>,
+    <<"/", EscapedDbName/binary, "/", DesignId/binary>>.
+
+safe_relative_path(Prefix, Path) ->
+    case mochiweb_util:safe_relative_path(?b2l(Path)) of
+        undefined ->
+            <<Prefix/binary, "/", Path/binary>>;
+        V0 ->
+            V1 = ?l2b(V0),
+            <<Prefix/binary, "/", V1/binary>>
+    end.
 
-            chttpd:handle_request_int(MochiReq1)
-        end.
 
 quote_plus({bind, X}) ->
     mochiweb_util:quote_plus(X);
@@ -359,9 +367,12 @@ bind_path(_, _, _) ->
 
 
 %% normalize path.
-normalize_path(Path)  ->
-    "/" ++ string:join(normalize_path1(string:tokens(Path,
-                "/"), []), [?SEPARATOR]).
+normalize_path(Path) when is_binary(Path)->
+    normalize_path(?b2l(Path));
+normalize_path(Path) when is_list(Path)->
+    Segments = normalize_path1(string:tokens(Path, "/"), []),
+    NormalizedPath = string:join(Segments, [?SEPARATOR]),
+    iolist_to_binary(["/", NormalizedPath]).
 
 
 normalize_path1([], Acc) ->