diff options
author | Benoit Chesneau <benoitc@apache.org> | 2010-01-31 23:00:37 +0000 |
---|---|---|
committer | Benoit Chesneau <benoitc@apache.org> | 2010-01-31 23:00:37 +0000 |
commit | a874df4ab163a251df931ae3551ebfb94361d73d (patch) | |
tree | 9fa069b481130d22db029295891178e4077e27bf | |
parent | 70544a1352d627503b788d45e4f6d3e978c46560 (diff) |
add url rewriting support.
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@905134 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r-- | etc/couchdb/default.ini.tpl.in | 1 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 1 | ||||
-rw-r--r-- | share/www/script/test/rewrite.js | 241 | ||||
-rw-r--r-- | src/couchdb/Makefile.am | 2 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_rewrite.erl | 389 |
5 files changed, 634 insertions, 0 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index dcf4d0f3..af971f1d 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -87,6 +87,7 @@ _view = {couch_httpd_view, handle_view_req} _show = {couch_httpd_show, handle_doc_show_req} _list = {couch_httpd_show, handle_view_list_req} _info = {couch_httpd_db, handle_design_info_req} +_rewrite = {couch_httpd_rewrite, handle_rewrite_req} _update = {couch_httpd_show, handle_doc_update_req} [uuids] diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index fe9f361e..6f1acf83 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -70,6 +70,7 @@ loadTest("reduce_false.js"); loadTest("reduce_false_temp.js"); loadTest("replication.js"); loadTest("rev_stemming.js"); +loadTest("rewrite.js"); loadTest("security_validation.js"); loadTest("show_documents.js"); loadTest("stats.js"); diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js new file mode 100644 index 00000000..48e5edb4 --- /dev/null +++ b/share/www/script/test/rewrite.js @@ -0,0 +1,241 @@ +// 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. + + + +couchTests.rewrite = function(debug) { + // this test _rewrite handler + + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + + + if (debug) debugger; + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], + + function(){ + var designDoc = { + _id:"_design/test", + language: "javascript", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: [ + { + "from": "foo", + "to": "foo.txt" + }, + { + "from": "foo2", + "to": "foo.txt", + "method": "GET" + }, + { + "from": "hello/:id", + "to": "_update/hello/:id", + "method": "PUT" + }, + { + "from": "/welcome", + "to": "_show/welcome" + }, + { + "from": "/welcome/:name", + "to": "_show/welcome", + "query": { + "name": ":name" + } + }, + { + "from": "/welcome2/:name", + "to": "_update/welcome2/:name", + "method": "PUT" + }, + { + "from": "/welcome2/:name", + "to": "_show/welcome2/:name", + "method": "GET" + }, + { + "from": "simpleForm/basicView", + "to": "_list/simpleForm/basicView", + }, + { + "from": "simpleForm/basicViewFixed", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": 3, + "endkey": 8 + } + }, + { + "from": "simpleForm/basicViewPath/:start/:end", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": ":start", + "endkey": ":end" + } + } + + ], + lists: { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('<ul>'); + var row, row_number = 0, prevKey, firstKey = null; + while (row = getRow()) { + row_number += 1; + if (!firstKey) firstKey = row.key; + prevKey = row.key; + send('\n<li>Key: '+row.key + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); + } + return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; + }) + }, + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "welcome2": stringFun(function(doc, req) { + return "Welcome " + doc.name; + }), + }, + updates: { + "hello" : stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id : req.id + }, "New World"] + } + return [null, "Empty World"]; + } + doc.world = "hello"; + doc.edited_by = req.userCtx; + return [doc, "hello doc"]; + }), + "welcome2": stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id: req.id, + name: req.id + }, "New World"] + } + return [null, "Empty World"]; + } + return [doc, "hello doc"]; + }) + }, + views : { + basicView : { + map : stringFun(function(doc) { + emit(doc.integer, doc.string); + }) + } + } + } + + db.save(designDoc); + + var docs = makeDocs(0, 10); + db.bulkSave(docs); + + // test simple rewriting + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo2"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + + // test POST + // hello update world + + var doc = {"word":"plankton", "name":"Rusty"} + var resp = db.save(doc); + T(resp.ok); + var docid = resp.id; + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/hello/"+docid); + T(xhr.status == 201); + T(xhr.responseText == "hello doc"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + + doc = db.open(docid); + T(doc.world == "hello"); + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/welcome2/test"); + T(xhr.status == 201); + T(xhr.responseText == "New World"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome2/test"); + T(xhr.responseText == "Welcome test"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome?name=user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome/user"); + T(req.responseText == "Welcome user"); + + + + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewPath/3/8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + + + }); + +}
\ No newline at end of file diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am index 9e909964..9c7e9d4d 100644 --- a/src/couchdb/Makefile.am +++ b/src/couchdb/Makefile.am @@ -48,6 +48,7 @@ source_files = \ couch_httpd_show.erl \ couch_httpd_view.erl \ couch_httpd_misc_handlers.erl \ + couch_httpd_rewrite.erl \ couch_httpd_stats_handlers.erl \ couch_key_tree.erl \ couch_log.erl \ @@ -103,6 +104,7 @@ compiled_files = \ couch_httpd_show.beam \ couch_httpd_view.beam \ couch_httpd_misc_handlers.beam \ + couch_httpd_rewrite.beam \ couch_httpd_stats_handlers.beam \ couch_key_tree.beam \ couch_log.beam \ diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl new file mode 100644 index 00000000..5dac5387 --- /dev/null +++ b/src/couchdb/couch_httpd_rewrite.erl @@ -0,0 +1,389 @@ +% 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. +% +% bind_path is based on bind method from Webmachine + + +%% @doc Module for URL rewriting by pattern matching. + +-module(couch_httpd_rewrite). +-export([handle_rewrite_req/3]). +-include("couch_db.hrl"). + +-define(SEPARATOR, $\/). +-define(MATCH_ALL, '*'). + + +%% 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 : +%% +%% { +%% .... +%% "rewrite": [ +%% { +%% "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 are 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 = couch_httpd:qs(Req), + + #doc{body={Props}} = DDoc, + + % get rules from ddoc + case proplists:get_value(<<"rewrites">>, Props) of + undefined -> + couch_httpd:send_error(Req, 404, <<"rewrite_error">>, + <<"Invalid path.">>); + Rules -> + % create dispatch list from rules + DispatchList = [make_rule(Rule) || {Rule} <- Rules], + + %% get raw path by matching url to a rule. + RawPath = case try_bind_path(DispatchList, Method, PathParts, + QueryList) of + no_dispatch_path -> + throw(not_found); + {NewPathParts, Bindings} -> + Parts = [mochiweb_util:quote_plus(X) || X <- NewPathParts], + + % build new path, reencode query args, eventually convert + % them to json + Path = lists:append( + string:join(Parts, [?SEPARATOR]), + case Bindings of + [] -> []; + _ -> [$?, encode_query(Bindings)] + end), + + % 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 + + end, + + % normalize final path (fix levels "." and "..") + RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))), + + %% get path parts, needed for CouchDB dispatching + {"/" ++ NewPath2, _, _} = mochiweb_util:urlsplit_path(RawPath1), + NewPathParts1 = [?l2b(couch_httpd:unquote(Part)) + || Part <- string:tokens(NewPath2, "/")], + + ?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(), + + % send to couchdb the rewritten request + couch_httpd_db:handle_request(Req#httpd{ + path_parts=NewPathParts1, + mochi_req=MochiReq1}) + end. + + + +%% @doc Try to find a rule matching current url. If none is found +%% 404 error not_found is raised +try_bind_path([], _Method, _PathParts, _QueryList) -> + no_dispatch_path; +try_bind_path([Dispatch|Rest], Method, PathParts, QueryList) -> + [{PathParts1, Method1}, RedirectPath, QueryArgs] = Dispatch, + case bind_method(Method1, Method) of + true -> + case bind_path(PathParts1, PathParts, []) of + {ok, Remaining, Bindings} -> + Bindings1 = Bindings ++ QueryList, + + % we parse query args from the rule and fill + % it eventually with bindings vars + QueryArgs1 = make_query_list(QueryArgs, Bindings1, []), + + % remove params in QueryLists1 that are already in + % QueryArgs1 + Bindings2 = lists:foldl(fun({K, V}, Acc) -> + K1 = to_atom(K), + KV = case proplists:get_value(K1, QueryArgs1) of + undefined -> [{K1, V}]; + _V1 -> [] + end, + Acc ++ KV + end, [], Bindings1), + + FinalBindings = Bindings2 ++ QueryArgs1, + NewPathParts = make_new_path(RedirectPath, FinalBindings, + Remaining, []), + {NewPathParts, FinalBindings}; + fail -> + try_bind_path(Rest, Method, PathParts, QueryList) + end; + false -> + try_bind_path(Rest, Method, PathParts, QueryList) + end. + +%% rewriting dynamically the quey list given as query member in +%% rewrites. Each value is replaced by one binding or an argument +%% passed in url. +make_query_list([], _Bindings, Acc) -> + Acc; +make_query_list([{Key, {Value}}|Rest], Bindings, Acc) -> + Value1 = make_query_list(Value, Bindings, []), + make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]); +make_query_list([{Key, Value}|Rest], Bindings, Acc) when is_binary(Value) -> + Value1 = replace_var(Value, Bindings), + make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]); +make_query_list([{Key, Value}|Rest], Bindings, Acc) when is_list(Value) -> + + Value1 = replace_vars(Value, Bindings, []), + make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]); +make_query_list([{Key, Value}|Rest], Bindings, Acc) -> + make_query_list(Rest, Bindings, [{to_atom(Key), Value}|Acc]). + +replace_vars([], _Bindings, Acc) -> + lists:reverse(Acc); +replace_vars([V|R], Bindings, Acc) -> + V1 = replace_var(V, Bindings), + replace_vars(R, Bindings, [V1|Acc]). + +replace_var(Value, Bindings) -> + case Value of + <<":", Var/binary>> -> + Var1 = list_to_atom(binary_to_list(Var)), + proplists:get_value(Var1, Bindings, Value); + _ -> Value + end. + + +%% doc: build new patch from bindings. bindings are query args +%% (+ dynamic query rewritten if needed) and bindings found in +%% bind_path step. +make_new_path([], _Bindings, _Remaining, Acc) -> + lists:reverse(Acc); +make_new_path([?MATCH_ALL], _Bindings, Remaining, Acc) -> + Acc1 = lists:reverse(Acc) ++ Remaining, + Acc1; +make_new_path([?MATCH_ALL|_Rest], _Bindings, Remaining, Acc) -> + Acc1 = lists:reverse(Acc) ++ Remaining, + Acc1; +make_new_path([P|Rest], Bindings, Remaining, Acc) when is_atom(P) -> + P2 = case proplists:get_value(P, Bindings) of + undefined -> << "undefined">>; + P1 -> P1 + end, + make_new_path(Rest, Bindings, Remaining, [P2|Acc]); +make_new_path([P|Rest], Bindings, Remaining, Acc) -> + make_new_path(Rest, Bindings, Remaining, [P|Acc]). + + +%% @doc If method of the query fith the rule method. If the +%% method rule is '*', which is the default, all +%% request method will bind. It allows us to make rules +%% depending on HTTP method. +bind_method(?MATCH_ALL, _Method) -> + true; +bind_method(Method, Method) -> + true; +bind_method(_, _) -> + false. + + +%% @doc bind path. Using the rule from we try to bind variables given +%% to the current url by pattern matching +bind_path([], [], Bindings) -> + {ok, [], Bindings}; +bind_path([?MATCH_ALL], Rest, Bindings) when is_list(Rest) -> + {ok, Rest, Bindings}; +bind_path(_, [], _) -> + fail; +bind_path([Token|RestToken],[Match|RestMatch],Bindings) when is_atom(Token) -> + bind_path(RestToken, RestMatch, [{Token, Match}|Bindings]); +bind_path([Token|RestToken], [Token|RestMatch], Bindings) -> + bind_path(RestToken, RestMatch, Bindings); +bind_path(_, _, _) -> + fail. + + +%% normalize path. +normalize_path(Path) -> + "/" ++ string:join(normalize_path1(string:tokens(Path, + "/"), []), [?SEPARATOR]). + + +normalize_path1([], Acc) -> + lists:reverse(Acc); +normalize_path1([".."|Rest], Acc) -> + Acc1 = case Acc of + [] -> [".."|Acc]; + [T|_] when T =:= ".." -> [".."|Acc]; + [_|R] -> R + end, + normalize_path1(Rest, Acc1); +normalize_path1(["."|Rest], Acc) -> + normalize_path1(Rest, Acc); +normalize_path1([Path|Rest], Acc) -> + normalize_path1(Rest, [Path|Acc]). + + +%% @doc transform json rule in erlang for pattern matching +make_rule(Rule) -> + Method = case proplists:get_value(<<"method">>, Rule) of + undefined -> '*'; + M -> list_to_atom(?b2l(M)) + end, + QueryArgs = case proplists:get_value(<<"query">>, Rule) of + undefined -> []; + {Args} -> Args + end, + FromParts = case proplists:get_value(<<"from">>, Rule) of + undefined -> ['*']; + From -> + parse_path(From) + end, + ToParts = case proplists:get_value(<<"to">>, Rule) of + undefined -> + throw({error, invalid_rewrite_target}); + To -> + parse_path(To) + end, + [{FromParts, Method}, ToParts, QueryArgs]. + +parse_path(Path) -> + {ok, SlashRE} = re:compile(<<"\\/">>), + path_to_list(re:split(Path, SlashRE), []). + +%% @doc convert a path rule (from or to) to an erlang list +%% * and path variable starting by ":" are converted +%% in erlang atom. +path_to_list([], Acc) -> + lists:reverse(Acc); +path_to_list([<<>>|R], Acc) -> + path_to_list(R, Acc); +path_to_list([<<"*">>|R], Acc) -> + path_to_list(R, [?MATCH_ALL|Acc]); +path_to_list([P|R], Acc) -> + P1 = case P of + <<":", Var/binary>> -> + list_to_atom(binary_to_list(Var)); + _ -> P + end, + path_to_list(R, [P1|Acc]). + +encode_query(Props) -> + RevPairs = lists:foldl(fun ({K, V}, Acc) -> + V1 = case is_list(V) of + true -> V; + false -> + mochiweb_util:quote_plus(V) + end, + [{K, V1} | Acc] + end, [], Props), + lists:flatten(mochiweb_util:urlencode(RevPairs)). + +to_atom(V) when is_atom(V) -> + V; +to_atom(V) when is_binary(V) -> + to_atom(?b2l(V)); +to_atom(V) -> + list_to_atom(V). |