Permalink
Browse files

WebDAV compliancy rework (Tjeerd van der Laan)

The WebDAV support is reworked and adds class 1, 2 and 3 compliancy
which includes:

* XML request body parsing and  multistatus responses

* PROPFIND and PROPPATCH methods returning properties asked for

* all RFC4918 properties, the Apache executable property plus some
  Microsoft extensions

* locking mechanism (class 2 compliancy) on all destructive methods

* If header parsing
  • Loading branch information...
1 parent 00aa12d commit efa9effca98b8fe0c3f91e855b087b1dcc33e54e @vinoski vinoski committed Jul 12, 2012
View
4 .gitignore
@@ -26,8 +26,8 @@ test/ibrowse.tar.gz
test/support/include.mk
test/support/include.sh
test/t1/localhost:8000/
-test/t[1234567]/logs/
-test/t[1234567]/yaws.conf
+test/t[12345678]/logs/
+test/t[12345678]/yaws.conf
test/t4/www2/8388608.bin
www/yaws.pdf
www/yaws.ps
View
23 include/yaws_dav.hrl
@@ -1,11 +1,26 @@
-ifndef(_YAWS_DAV).
-define(_YAWS_DAV, true).
--record(propfind, {
- prop = [],
- uri = ""
- }).
+-define(LOCK_LIFETIME, 900). % lock lifetime in seconds: 15 minutes
+-define(CLEANUP_INTERVAL, 60). % cleanup interval in seconds: 1 minute
+-define(elog(X,Y), error_logger:info_msg("*elog ~p:~p: " X,
+ [?MODULE, ?LINE | Y])).
+-record(resource,{
+ name, % normalized name of resource
+ info % file_info record of mapped file
+ }).
+
+-record(davlock,{
+ path=undefined, % resource path
+ id=undefined, % uid
+ owner=anonymous, % lock owner if submitted
+ depth=infinity, % 0|infinity
+ scope=exclusive, % exclusive|shared
+ type=write, % write
+ timeout=0, % ?LOCK_LIFETIME or shorter
+ timestamp=0 % erlang:now()
+ }).
-endif.
View
1 src/Makefile
@@ -42,6 +42,7 @@ MODULES=yaws \
yaws_log_file_h \
yaws_rss \
yaws_dav \
+ yaws_davlock \
yaws_pam \
json json2 jsonrpc yaws_jsonrpc yaws_xmlrpc\
haxe yaws_rpc \
View
12 src/yaws.erl
@@ -101,7 +101,8 @@
-export([parse_ipmask/1, match_ipmask/2]).
%% Internal
--export([local_time_as_gmt_string/1, universal_time_as_string/1]).
+-export([local_time_as_gmt_string/1, universal_time_as_string/1,
+ stringdate_to_datetime/1]).
start() ->
application:start(yaws, permanent).
@@ -1168,8 +1169,11 @@ make_allow_header(Options) ->
[] ->
HasDav = ?sc_has_dav(get(sc)),
["Allow: GET, POST, OPTIONS, HEAD",
- if HasDav == true -> ", PUT, DELETE, PROPFIND, MKCOL, MOVE, COPY";
- true -> ""
+ case HasDav of
+ true ->
+ ", PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, MOVE, COPY";
+ false ->
+ ""
end, "\r\n"];
_ ->
["Allow: ",
@@ -1185,7 +1189,7 @@ make_server_header() ->
undefined -> (get(gc))#gconf.yaws;
S -> S
end,
- ["Server: ", Signature, "\r\n" | if HasDav == true -> ["DAV: 1\r\n"];
+ ["Server: ", Signature, "\r\n" | if HasDav == true -> ["DAV: 1, 2, 3\r\n"];
true -> []
end].
View
4 src/yaws_api.erl
@@ -534,7 +534,7 @@ code_to_phrase(203) -> "Non-Authoritative Information";
code_to_phrase(204) -> "No Content";
code_to_phrase(205) -> "Reset Content";
code_to_phrase(206) -> "Partial Content";
-code_to_phrase(207) -> "Multi Status";
+code_to_phrase(207) -> "Multi-Status";
code_to_phrase(208) -> "Already Reported";
code_to_phrase(226) -> "IM Used";
code_to_phrase(300) -> "Multiple Choices";
@@ -568,7 +568,7 @@ code_to_phrase(418) -> "I'm a teapot";
code_to_phrase(420) -> "Enhance Your Calm";
code_to_phrase(422) -> "Unprocessable Entity";
code_to_phrase(423) -> "Locked";
-code_to_phrase(424) -> "Method Failure";
+code_to_phrase(424) -> "Failed Dependency";
code_to_phrase(425) -> "Unordered Collection";
code_to_phrase(426) -> "Upgrade Required";
code_to_phrase(428) -> "Precondition Required";
View
1,233 src/yaws_dav.erl
@@ -2,12 +2,14 @@
%%%-------------------------------------------------------------------
%%% Created : 15 May 2005 by Tobbet <tobbe@tornkvist.org>
%%% Modified: 21 Nov 2005 by <mbj@tail-f.com>
+%%% Modified: 28 Jun 2012 by <tjeerd@yolt.nl>
%%% Desc. : WebDav specifics.
-%%% To use, add a line dav = true in the <server>.
-%%% TODO: fix more fine-grained permissions
+%%% RFC4918 class 1, 2, 3 compliant
+%%% To use, add a line dav = true in the <server>.
+%%%
%%%-------------------------------------------------------------------
--export([propfind/1, delete/1, put/2, mkcol/1, move/1, copy/1]).
--export([parse_xml/1, xml_expand/1, xml_expand/2]).
+-export([lock/1, unlock/1, propfind/1, proppatch/1, delete/1, put/2,
+ mkcol/1, move/1, copy/1]).
-include("../include/yaws_dav.hrl").
-include("../include/yaws_api.hrl").
@@ -16,45 +18,123 @@
-include_lib("xmerl/include/xmerl.hrl").
-include_lib("kernel/include/file.hrl").
+%%------------------------------------------------------
+%% methods
+%%
+
+lock(A) ->
+ try
+ Name = davname(A),
+ Path = davpath(A),
+ Locks = yaws_davlock:discover(Path),
+ If = h_if(Name,A,Locks),
+ verify_protected(Locks,If),
+ R = case file:read_file_info(Path) of
+ {ok, F} when (F#file_info.type == directory) or (F#file_info.type == regular) ->
+ #resource{ name = Name, info = F};
+ {error,enoent} ->
+ case string:right(A#arg.server_path,1) of
+ "/" ->
+ ok = file:make_dir(Path);
+ _ ->
+ ok = file:write_file(Path,"")
+ end,
+ {ok, F} = file:read_file_info(Path),
+ #resource{ name = Name, info = F};
+ {error,_} -> throw(409)
+ end,
+ Req = binary_to_list(A#arg.clidata),
+ L = parse_lockinfo(Req),
+ Id = h_locktoken(A),
+ Timeout = h_timeout(A),
+ Depth = h_depth(A),
+ case yaws_davlock:lock(R#resource.name,L#davlock{id=Id,timeout=Timeout,depth=Depth}) of
+ {ok,Id1} ->
+ {200,Result} = prop_get({'DAV:',lockdiscovery},A,R),
+ Response = [{'D:prop', [{'xmlns:D',"DAV:"}], [Result]}],
+ status(200,[{"Lock-Token","<opaquelocktoken:"++Id1++">"}],Response);
+ {error,locked} ->
+ status(423);
+ _ ->
+ throw(501)
+ end
+ catch
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
+ end.
+
+unlock(A) ->
+ try
+ R = davresource0(A),
+ Id = h_locktoken(A),
+ yaws_davlock:unlock(R#resource.name,Id),
+ status(204)
+ catch
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
+ end.
--define(elog(X,Y), error_logger:info_msg("*elog ~p:~p: " X,
- [?MODULE, ?LINE | Y])).
delete(A) ->
- Path = davpath(A),
- ?elog("DELETE Path=~p~n", [Path]),
- case rmrf(Path) of
- ok -> out200();
- _ -> out403()
+ try
+ R = davresource0(A),
+ case yaws_davlock:lock(R#resource.name,#davlock{depth=infinity,scope=exclusive}) of
+ {ok,Id} ->
+ rmrf(A#arg.docroot++R#resource.name),
+ yaws_davlock:unlock(R#resource.name,Id);
+ _ -> throw(423)
+ end
+ catch
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
end.
put(SC, ARG) ->
- H = ARG#arg.headers,
- PPS = SC#sconf.partial_post_size,
- CT =
- case yaws:to_lower(H#headers.content_type) of
- "multipart/form-data"++_ -> multipart;
- _ -> urlencoded
+ try
+ Name = davname(ARG),
+ FName = davpath(ARG),
+ Locks = yaws_davlock:discover(Name),
+ If = h_if(FName,ARG,Locks),
+ verify_protected(Locks,If),
+ IsDir = filelib:is_dir(FName),
+ H = ARG#arg.headers,
+ PPS = SC#sconf.partial_post_size,
+ CT = case yaws:to_lower(H#headers.content_type) of
+ "multipart/form-data"++_ -> multipart;
+ _ -> urlencoded
+ end,
+ if
+ IsDir-> throw(405);
+ true -> ok
end,
- SSL = yaws:is_ssl(SC),
- FName = davpath(ARG),
- CliSock = case yaws_api:get_sslsocket(ARG#arg.clisock) of
- {ok, SslSock} -> SslSock;
- undefined -> ARG#arg.clisock
- end,
- TmpName = FName ++ ".tmp",
- %% FIXME: first check if we can write to original file??
- case file:open(TmpName, [raw,write]) of
- {ok, Fd} ->
- try
+ SSL = yaws:is_ssl(SC),
+ CliSock = case yaws_api:get_sslsocket(ARG#arg.clisock) of
+ {ok, SslSock} -> SslSock;
+ undefined -> ARG#arg.clisock
+ end,
+ TmpName = temp_name(FName),
+ case file:open(TmpName, [raw,write]) of
+ {ok, Fd} ->
case H#headers.content_length of
undefined ->
Chunked = H#headers.transfer_encoding == "chunked",
case H#headers.connection of
"close" when Chunked == false->
store_client_data(Fd, CliSock, all, SSL);
_ when Chunked == true ->
- store_chunked_client_data(Fd, CliSock, SSL)
+ store_chunked_client_data(Fd, CliSock, SSL);
+ _ ->
+ %store_client_data(Fd, CliSock, all, SSL)
+ ok
end;
Len when is_integer(PPS) ->
Int_len = list_to_integer(Len),
@@ -64,7 +144,7 @@ put(SC, ARG) ->
PPS < Int_len, CT == multipart ->
%% FIXME: handle this
%% {partial,
- store_client_data(Fd,CliSock, PPS, SSL); % };
+ store_client_data(Fd,CliSock, PPS, SSL); % };
true ->
store_client_data(Fd, CliSock, Int_len, SSL)
end;
@@ -80,30 +160,39 @@ put(SC, ARG) ->
file:close(Fd),
case file:rename(TmpName, FName) of
ok ->
- out200();
- Error ->
- throw(Error)
- end
- catch
- _:_Err ->
- ?Debug("PUT error ~p\n", [_Err, TmpName]),
- file:close(Fd),
- file:delete(TmpName),
- out409()
- end;
- _Error ->
- ?Debug("PUT error ~p ~p\n", [_Error, TmpName]),
- out409()
+ status(200);
+ _ ->
+ status(409)
+ end;
+ {error,eexist} -> throw(405);
+ {error,enoent} -> throw(409);
+ {error,eisdir} -> throw(409);
+ {error,enospace} -> throw(507);
+ _ -> status(500)
+ end
+ catch
+ exit:normal -> exit(normal);
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
end.
mkcol(A) ->
Path = davpath(A),
- case file:make_dir(Path) of
- ok ->
- out201();
- {error, Reason} ->
- ?elog("failed to create dir: ~p , reason: ~p~n", [Path, Reason]),
- out403()
+ Name = davname(A),
+ try
+ Locks = yaws_davlock:discover(Name),
+ If = h_if(Path,A,Locks),
+ verify_protected(Locks,If),
+ file_do(make_dir,[Path]),
+ status(201)
+ catch
+ Status -> status(Status);
+ Error:Reason ->
+ ?elog("create directory ~p failed: ~p with reason ~p~n", [Path,Error,Reason]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
end.
copy(A) ->
@@ -113,392 +202,811 @@ move(A) ->
copy_move(A, fun do_move/2).
copy_move(A, OpF) ->
- case lists:keysearch("Destination", 3, (A#arg.headers)#headers.other) of
- {value, {http_header, _, _, _, Url}} ->
- %% FIXME: check for weird paths
- {Url1, _} = yaws_api:url_decode_q_split(Url),
- Path = Url1 -- davroot(A),
- From = davpath(A),
- To = A#arg.docroot ++ "/" ++ Path,
- ?elog("move from ~p to ~p (~p)\n", [From, To, Url]),
- DoOverwrite = get_overwrite(A),
- IsSame = is_same(From, To),
- ToExsist = exists(To),
- if IsSame == true ->
- [{status, 403}];
- DoOverwrite == false,
- ToExsist == true ->
- [{status, 412}];
- true ->
- if DoOverwrite == true ->
- rmrf(To);
- true ->
- ok
- end,
- OpF(From, To)
- end;
- _ ->
- [{status, 501}]
+ From = davpath(A),
+ try
+ To = h_destination(A),
+ Locks = yaws_davlock:discover(To),
+ If = h_if(To,A,Locks),
+ verify_protected(Locks,If),
+ DoOverwrite = h_overwrite(A),
+ ToExists = exists(To),
+ if
+ DoOverwrite == false, ToExists == true ->
+ status(412);
+ true ->
+ if ToExists == true ->
+ rmrf(To);
+ true ->
+ ok
+ end,
+ OpF(From, To)
+ end
+ catch
+ Status -> status(Status);
+ Error:Reason ->
+ ?elog("copy/move ~p failed: ~p with reason ~p~n", [From,Error,Reason]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
end.
do_move(From, To) ->
case file:rename(From, To) of
ok ->
- out201();
+ status(201);
_ ->
case file:copy(From, To) of
{ok,_} ->
- ok = file:delete(From),
- out201();
+ file:delete(From),
+ status(201);
{error, Reason} ->
?elog("move from ~p to ~p failed: ~p\n",
[From, To, Reason]),
- out409()
+ status(409,[{'D:error', [{'xmlns:D',"DAV:"}],[Reason]}])
end
end.
do_copy(From, To) ->
case file:copy(From, To) of
{ok, _} ->
- out201();
- Error ->
+ status(201);
+ {error, Reason} ->
?elog("move from ~p to ~p failed: ~p\n",
- [From, To, Error]),
- out409()
+ [From, To, Reason]),
+ status(409)
end.
-propfind(A) ->
- %% Depth:
- %% If '0', then no members should be returned.
- %% If '1', then members one level down should be included in the reply.
- %% If 'infinity', then all members, recursively, should be included.
- case depth(A) of
- 0 ->
- ?elog("propfind: Depth=0~n", []),
- Response = depth_zero(A),
- MultiStatus = [{multistatus, [{'xmlns',"DAV:"}], Response}],
- B = yaws_dav:xml_expand(MultiStatus),
- out207(B);
- 1 ->
- Entries = get_entries(A),
- ?elog("propfind: Depth=1 , length(Entries)=~p~n",
- [length(Entries)]),
- Url = davurl(A),
- F = fun(Finfo) -> response_entry(Finfo, Url) end,
- Responses = lists:map(F, Entries),
- MultiStatus = [{multistatus, [{'xmlns',"DAV:"}], Responses}],
- B = yaws_dav:xml_expand(MultiStatus),
- out207(B)
+exists(Path) ->
+ case file:read_file_info(Path) of
+ {ok, _} -> true;
+ _ -> false
end.
+temp_name(F) ->
+ {A,B,C} = erlang:now(),
+ Path = filename:dirname(F),
+ File = filename:basename(F),
+ T0 = io_lib:format("~s/.~s.~p-~p-~p",[Path,File,A,B,C]),
+ lists:flatten(T0).
-date_string({{Y,M,D}, {Hr,Min,Sec}}) ->
- lists:concat([D, " ", month(M), " ", Y, " ", Hr, ":", Min, ":", Sec]).
+propfind(A) ->
+ try
+ Req = binary_to_list(A#arg.clidata),
+ Props = parse_propfind(Req),
+ R = davresource0(A),
+ case h_depth(A) of
+ 0 ->
+ %?elog("PROPFIND ~p (Depth=0)~n", [R#resource.name]),
+ Response = {'D:response', [], propfind_response(Props,A,R)},
+ MultiStatus = [{'D:multistatus', [{'xmlns:D',"DAV:"}], [Response]}],
+ status(207,MultiStatus);
+ 1 ->
+ R1 = davresource1(A),
+ %?elog("PROPFIND ~p (Depth=1, entries=~p)~n", [R#resource.name,length(R1)]),
+ Response = {'D:response', [], propfind_response(Props,A,R)},
+ Responses = [{'D:response', [], propfind_response(Props,A,Rx)} || Rx <- R1],
+ MultiStatus = [{'D:multistatus', [{'xmlns:D',"DAV:"}], [Response|Responses]}],
+ status(207,MultiStatus);
+ infinity ->
+ %?elog("PROPFIND ~p (Depth=infinity)~n", [R#resource.name]),
+ Response = [{'D:error', [{'xmlns:D',"DAV:"}],[{'propfind-finite-depth',[],[]}]}],
+ status(403,Response)
+ end
+ catch
+ {Status,Precondition} ->
+ Response1 = [{'D:error', [{'xmlns:D',"DAV:"}],[{Precondition,[],[]}]}],
+ status(Status,Response1);
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
+ end.
-get_entries(A) ->
- Path = davpath(A),
- case file:read_file_info(Path) of
- {ok, Dir} when Dir#file_info.type == directory ->
- {ok, L} = file:list_dir(Path),
- [{Name, element(2, file:read_file_info(Path ++ "/" ++ Name))} ||
- Name <- L];
- {ok, Else} ->
- [{get_name(Path),Else}]
+propfind_response(Props,A,R) ->
+ Url = yaws_api:url_encode(R#resource.name),
+ %Url = R#resource.name,
+ case Props of
+ [allprop] ->
+ AllProp = [ prop_get(N,A,R) || N <- allprops(R) ],
+ AllSorted = prop_sort(AllProp),
+ {200, Results} = lists:keyfind(200,1,AllSorted),
+ [{'D:href', [], [Url]},
+ {'D:propstat', [], [
+ {'D:prop', [], Results},{status, [],["HTTP/1.1 200 OK"]}
+ ]}];
+ [propname] ->
+ Results = [ case NS of
+ 'DAV:' -> {list_to_atom("D:"++atom_to_list(P)),[],[]};
+ _ -> {P,[{'xmlns',NS}],[]}
+ end
+ || {NS,P} <-allprops(R) ],
+ [{'D:href', [], [Url]},
+ {'D:propstat', [], [
+ {'D:prop', [], Results},{status, [],["HTTP/1.1 200 OK"]}
+ ]}];
+ PropsRequested ->
+ Results = [ prop_get(N,A,R) || {N,_} <- PropsRequested ],
+ ResultsSorted = prop_sort(Results),
+ [{'D:href', [], [Url]}|
+ [{'D:propstat', [], [
+ {'D:prop', [], PropsFound},prop_status(Status)
+ ]} || {Status,PropsFound} <- ResultsSorted ]
+ ]
end.
-%%% FIXME should get a proper file_info entry here
-%%
-response_entry({Name, F}, Url) when F#file_info.type == directory -> % Dir
- {response, [],
- [{href, [], [Url ++ Name]},
- {propstat, [],
- [{prop, [],
- [{name, [], [Name]},
- {creationdate, [], [date_string(F#file_info.ctime)]},
- {getlastmodified, [], [date_string(F#file_info.mtime)]},
- {getcontentlength, [], [integer_to_list(F#file_info.size)]},
- {resourcetype, [],
- [{collection, [], []}]}
- %%{ishidden, [], [bool2lnum(F#file.is_hidden)]}]},
- ]},
- {status, [], % Status 1
- ["HTTP/1.1 200 OK"]}]}]};
-%%
-response_entry({Name, F}, Url) when F#file_info.type == regular -> % File
- {response, [],
- [{href, [], [Url ++ Name]},
- {propstat, [],
- [{prop, [],
- [{name, [], [Name]},
- {creationdate, [], [date_string(F#file_info.ctime)]},
- {getlastmodified, [], [date_string(F#file_info.mtime)]},
- {getcontentlength, [], [integer_to_list(F#file_info.size)]},
- {resourcetype, [], []}
- %%{ishidden, [], [bool2lnum(F#file.is_hidden)]}]},
- ]},
- {status, [], % Status 1
- ["HTTP/1.1 200 OK"]}]}]};
-%%
-response_entry(F, _Url) ->
- ?elog("ignoring file: ~p~n", [F]),
- [].
-
-
-get_name("/") -> "/";
-get_name("") -> "/";
-get_name(L) ->
- [Rname|_] = string:tokens(lists:reverse(L), "/"),
- lists:reverse(Rname).
-
-file_name("/") -> ".";
-file_name("") -> ".";
-file_name(L) ->
- [Rname|_] = string:tokens(lists:reverse(L), "/"),
- lists:reverse(Rname).
-
-get_overwrite(A) ->
- case lists:keysearch("Overwrite", 3, (A#arg.headers)#headers.other) of
- {value, {http_header, _, _, _, "T"}} -> true;
- _ -> false
+proppatch(A) ->
+ try
+ Req = binary_to_list(A#arg.clidata),
+ R = davresource0(A),
+ Update = parse_proppatch(Req),
+ Response = proppatch_response(Update,A,R),
+ MultiStatus = [{'D:multistatus', [{'xmlns:D',"DAV:"}], Response}],
+ status(207,MultiStatus)
+ catch
+ {Status,Precondition} ->
+ Response1 = [{'D:error', [{'xmlns:D',"DAV:"}],[{Precondition,[],[]}]}],
+ status(Status,Response1);
+ Status ->
+ status(Status);
+ _Error:Reason ->
+ ?elog("unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ status(500,[{'D:error',[{'xmlns:D',"DAV:"}],[Reason]}])
end.
-exists(Path) ->
+proppatch_response(Update,A,R) ->
+ Url = yaws_api:url_encode(R#resource.name),
+ %Url = R#resource.name,
+ Results = proppatch_response(Update,A,R,[]),
+ ResultsSorted = prop_sort(lists:flatten(Results)),
+ [{'D:href', [], [Url]}|
+ [{'D:propstat', [], [
+ {'D:prop', [], PropsFound},prop_status(Status)
+ ]} || {Status,PropsFound} <- ResultsSorted ]
+ ].
+proppatch_response([H|T],A,R,Results) ->
+ Result = case H of
+ {set,Props} -> [ prop_set(P,A,R,V) || {P,V} <- Props];
+ {remove,Props} -> [ prop_remove(P,A,R) || {P,_V} <- Props]
+ end,
+ proppatch_response(T,A,R,[Result|Results]);
+proppatch_response([],_A,_R,Results) ->
+ Results.
+
+prop_sort(L) -> prop_sort(L,[]).
+prop_sort([H|T],R) ->
+ {Status,Prop} = H,
+ R1 = case lists:keyfind(Status,1,R) of
+ {Status, Props} -> lists:keystore(Status,1,R,{Status,[Prop|Props]});
+ false -> lists:keystore(Status,1,R,{Status,[Prop]})
+ end,
+ prop_sort(T,R1);
+prop_sort([],R) -> R.
+
+
+prop_status(Status) ->
+ {'D:status',[],["HTTP/1.1 " ++ integer_to_list(Status) ++ " " ++
+ yaws_api:code_to_phrase(Status)]}.
+
+%----------------------------------------------------
+% Available props include namespace
+% Available props can differ per resource
+% For proposed Microsoft extensions see: draft-hopmann-collection-props-00.txt
+%
+allprops(R) ->
+ F = R#resource.info,
+ P1 = case F#file_info.type of
+ directory -> [
+ {'DAV:',childcount} % Microsoft extension
+ ];
+ _ -> [
+ {'http://apache.org/dav/props/',executable} % Apache extension
+ ]
+ end,
+ P2 = [
+ %{'http://yaws.hyber.org/',access}, % sample Yaws extension
+ {'DAV:',creationdate},
+ {'DAV:',displayname},
+ %{'DAV:',getcontentlanguage}, % not supported in GET
+ % so omitted here as well
+ {'DAV:',getcontentlength},
+ {'DAV:',getcontenttype},
+ {'DAV:',getetag},
+ {'DAV:',getlastmodified},
+ {'DAV:',isfolder}, % Microsoft extension
+ {'DAV:',ishidden}, % Microsoft extension
+ {'DAV:',lockdiscovery}, % class 2 compliancy
+ %{'DAV:','quota-avialable-bytes'} % RFC4331
+ %{'DAV:','quota-used-bytes'} % RFC4331
+ {'DAV:',resourcetype},
+ {'DAV:',supportedlock} % class 2 compliancy
+ ],
+ P1 ++ P2.
+
+prop_get({'http://yaws.hyber.org/',access},_A,R) ->
+ F = R#resource.info,
+ A = F#file_info.access,
+ P = {access, [{xmlns,'http://yaws.hyber.org/'}], [atom_to_list(A)]},
+ {200, P};
+prop_get({'DAV:',childcount},A,_R) ->
+ Path=davpath(A),
+ L = case file:list_dir(Path) of
+ {ok, Files} -> length(Files);
+ _ -> 0
+ end,
+ P = {'D:childcount', [], [integer_to_list(L)]},
+ {200, P};
+prop_get({'DAV:',creationdate},_A,R) ->
+ F = R#resource.info,
+ D = F#file_info.ctime,
+ T = yaws:local_time_as_gmt_string(D),
+ P = {'D:creationdate', [], [lists:flatten(T)]},
+ {200, P};
+prop_get({'DAV:',displayname},_A,R) ->
+ Name = filename:basename(R#resource.name),
+ P = {'D:displayname', [], [Name]},
+ {200, P};
+prop_get({'http://apache.org/dav/props/',executable},_A,R) ->
+ F = R#resource.info,
+ case F#file_info.type of
+ directory -> {404,{executable, [{'xmlns',"http://apache.org/dav/props/"}], []}};
+ _ -> {200, {executable, [{'xmlns',"http://apache.org/dav/props/"}], ["F"]}}
+ end;
+prop_get({'DAV:',getcontentlanguage},_A,_R) ->
+ P = {'D:getcontentlanguage', [], []},
+ {200, P};
+prop_get({'DAV:',getcontentlength},_A,R) ->
+ F = R#resource.info,
+ P = {'D:getcontentlength', [], [integer_to_list(F#file_info.size)]},
+ {200, P};
+prop_get({'DAV:',getcontenttype},_A,R) ->
+ F = R#resource.info,
+ Mediatype = case F#file_info.type of
+ directory ->
+ "text/html"; % this represents the mediatype of a GET on a collection
+ _ ->
+ Name = R#resource.name,
+ Ext = filename:extension(Name),
+ Ext1 = case Ext of
+ [] -> "";
+ _ -> tl(Ext)
+ end,
+ {_Kind,Mimetype} = mime_types:t(Ext1),
+ Mimetype
+ end,
+ P = {'D:getcontenttype', [], [Mediatype]},
+ {200, P};
+prop_get({'DAV:',getetag},_A,R) ->
+ F = R#resource.info,
+ E = yaws:make_etag(F),
+ P = {'D:getetag', [], [E]},
+ {200, P};
+prop_get({'DAV:',getlastmodified},_A,R) ->
+ F = R#resource.info,
+ D = F#file_info.mtime,
+ T = yaws:local_time_as_gmt_string(D),
+ P = {'D:getlastmodified', [], [lists:flatten(T)]},
+ {200, P};
+prop_get({'DAV:',isfolder},_A,R) ->
+ F = R#resource.info,
+ D = case F#file_info.type of
+ directory -> "1";
+ _ -> "0"
+ end,
+ P = {'D:isfolder', [], [D]},
+ {200, P};
+prop_get({'DAV:',ishidden},_A,R) ->
+ N = filename:basename(R#resource.name),
+ H = case N of
+ "."++_Rest -> "1"; % dotted file
+ _ -> "0"
+ end,
+ P = {'D:ishidden', [], [H]},
+ {200, P};
+prop_get({'DAV:',resourcetype},_A,R) ->
+ F = R#resource.info,
+ P = case F#file_info.type of
+ directory -> {'D:resourcetype', [], [{'D:collection',[],[]}]};
+ _ -> {'D:resourcetype', [], []}
+ end,
+ {200, P};
+prop_get({'DAV:',lockdiscovery},_A,R) ->
+ Name = R#resource.name,
+ Locks = yaws_davlock:discover(Name),
+ case Locks of
+ [] ->
+ {404,{'D:lockdiscovery',[],[]}};
+ _ ->
+ ActiveLocks = [
+ {'D:activelock',[],[
+ {'D:lockscope',[],[prop_get_format(scope,Lock#davlock.scope)]},
+ {'D:locktype',[],[prop_get_format(type,Lock#davlock.type)]},
+ {'D:depth',[],[prop_get_format(depth,Lock#davlock.depth)]},
+ %{'D:owner',[],[prop_get_format(owner,Lock#davlock.owner)]}, % kept secret
+ {'D:timeout',[],[prop_get_format(timeout,Lock#davlock.timeout)]},
+ {'D:locktoken',[],[prop_get_format(locktoken,Lock#davlock.id)]},
+ {'D:lockroot',[],[prop_get_format(lockroot,Lock#davlock.path)]}
+ ]}
+ || Lock <- Locks ],
+ {200, {'D:lockdiscovery',[],ActiveLocks}}
+ end;
+prop_get({'DAV:',supportedlock},_A,_R) ->
+ P = {'D:supportedlock',[],[
+ {'D:lockentry',[],[
+ {'D:lockscope',[],[{'D:exclusive',[],[]}]},
+ {'D:locktype',[],[{'D:write',[],[]}]}
+ ]},
+ {'D:lockentry',[],[
+ {'D:lockscope',[],[{'D:shared',[],[]}]},
+ {'D:locktype',[],[{'D:write',[],[]}]}
+ ]}
+ ]},
+ {200, P};
+prop_get({NS,P},_A,_R) ->
+ {404,{P,[{'xmlns',NS}],[]}}.
+
+
+prop_set({'DAV:',creationdate},A,_R,V) ->
+ Path=davpath(A),
+ P = {'D:creationdate', [], []},
case file:read_file_info(Path) of
- {ok, _} -> true;
- _ -> false
- end.
+ {ok,F0} ->
+ T = yaws:stringdate_to_datetime(V),
+ F1 = F0#file_info{ctime=T},
+ case file:write_file_info(Path,F1) of
+ ok ->
+ {200, P};
+ {error,_} ->
+ {409, P}
+ end;
+ {error,_} ->
+ {409, P}
+ end;
+prop_set({'DAV:',getlastmodified},A,_R,V) ->
+ Path=davpath(A),
+ P = {'D:creationdate', [], []},
+ case file:read_file_info(Path) of
+ {ok,F0} ->
+ T = yaws:stringdate_to_datetime(V),
+ F1 = F0#file_info{mtime=T},
+ case file:write_file_info(Path,F1) of
+ ok ->
+ {200, P};
+ {error,_} ->
+ {409, P}
+ end;
+ {error,_} ->
+ {409, P}
+ end;
+prop_set({'DAV:',getetag},_A,_R,_V) ->
+ {403,{'D:getetag',[],[{'cannot-modify-protected-property',[],[]}]}};
+prop_set({'DAV:',lockdiscovery},_A,_R,_V) ->
+ {403,{'D:lockdiscovery',[],[{'cannot-modify-protected-property',[],[]}]}};
+prop_set({'DAV:',resourcetype},_A,_R,_V) ->
+ {403,{'D:resourcetype',[],[{'cannot-modify-protected-property',[],[]}]}};
+prop_set({NS,P},_A,_R,_V) ->
+ {404,{P,[{'xmlns',NS}],[]}}.
-%% FIXME: how to do this in a portable way? on unix we could check inode...
-is_same(A, B) ->
- A == B.
+prop_remove({P,NS},_A,_R) ->
+ {403,{P,[{'xmlns',NS}],[]}}.
-depth_zero(A) ->
- Path = davpath(A),
- Url = davurl(A),
- Name = file_name(Path),
- {ok, F} = file:read_file_info(Path), % FIXME
- ?elog("server_path=~p~n", [A#arg.server_path]),
- [{response, [],
- [{href, [], [Url]},
- {propstat, [],
- [{prop, [],
- [{name, [], [Name]},
- {creationdate, [], [date_string(F#file_info.ctime)]},
- {getlastmodified, [], [date_string(F#file_info.mtime)]},
- {getcontentlength, [], [integer_to_list(F#file_info.size)]},
- {resourcetype, [],
- is_collection(F)}
- %%{ishidden, [], [bool2lnum(F#file.is_hidden)]}]},
- ]},
- {status, [],
- ["HTTP/1.1 200 OK"]}]}]}].
-
-is_collection(F) when F#file_info.type == directory ->
- [{collection, [], []}];
-is_collection(_) ->
- [].
+prop_get_format(type,write) ->
+ {'D:write',[],[]};
+prop_get_format(scope,exclusive) ->
+ {'D:exclusive',[],[]};
+prop_get_format(scope,_) ->
+ {'D:shared',[],[]};
+prop_get_format(depth,infinity) ->
+ "infinity";
+prop_get_format(depth,Depth) ->
+ integer_to_list(Depth);
+prop_get_format(timeout,Timeout) ->
+ lists:flatten(io_lib:format("Second-~p",[Timeout]));
+prop_get_format(locktoken,Id) ->
+ {'D:href',[],["opaquelocktoken:"++Id]};
+prop_get_format(lockroot,Ref) ->
+ {'D:href',[],[Ref]};
+prop_get_format(owner,Owner) ->
+ Owner;
+prop_get_format(_,_) ->
+ throw(500).
+
+%% --------------------------------------------------------
+%% Resource mapping
+%%
+
+davname(A) ->
+ A#arg.server_path.
davpath(A) ->
A#arg.docroot ++ A#arg.server_path.
-davurl(A) ->
- davroot(A) ++ A#arg.server_path ++ "/".
-
davroot(A) ->
- Method = case A#arg.clisock of
- {ssl ,_} -> "https";
- _ -> "http"
+ Method = case yaws_api:get_sslsocket(A#arg.clisock) of
+ {ok, _SslSock} -> "https";
+ undefined -> "http"
end,
Host = (A#arg.headers)#headers.host,
Method ++ "://" ++ Host.
-depth(A) ->
- %%
- %% Look for: {http_header, _Num, 'Depth', _, Depth}
- %%
+%% davresource0/1 - get resources with depth 0
+davresource0(A) ->
+ Name = davname(A),
+ Path = davpath(A),
+ case file:read_file_info(Path) of
+ {ok, F} when (F#file_info.type == directory) or (F#file_info.type == regular) ->
+ #resource{ name = Name, info = F};
+ {error,_} -> throw(404)
+ end.
+%% davresource1/1 - get additional resources for depth 1
+davresource1(A) ->
+ Coll = davname(A),
+ Path = davpath(A),
+ case file:read_file_info(Path) of
+ {ok, Dir} when Dir#file_info.type == directory ->
+ {ok, L} = file:list_dir(Path),
+ davresource1(A,Path,Coll,L,[]);
+ {ok, _Else} ->
+ []
+ end.
+davresource1(_A,_Path,_Coll,[],Result) ->
+ Result;
+davresource1(_A,Path,Coll,[Name|Rest],Result) ->
+ File = filename:join(Path,Name),
+ Ref = filename:join(Coll,Name),
+ {ok, Info} = file:read_file_info(File),
+ if
+ (Info#file_info.type == regular) or (Info#file_info.type == directory) ->
+ Resource = #resource {name = Ref, info = Info},
+ davresource1(_A,Path,Coll,Rest,[Resource|Result]);
+ true ->
+ davresource1(_A,Path,Coll,Rest,Result)
+ end.
+
+%% --------------------------------------------------------
+%% Check if resource is protected by locks
+%% is_protected(Locks,If) -> true|false
+
+verify_protected(Locks,false) when length(Locks)>0 -> throw(412);
+verify_protected(Locks,undefined) when length(Locks)>0 -> throw(423);
+verify_protected(_Lock,_If) -> ok.
+
+%% --------------------------------------------------------
+%% Parse additional HTTP headers
+%%
+
+h_depth(A) ->
Hs = (A#arg.headers)#headers.other,
case lists:keysearch("Depth", 3, Hs) of
{value, {_,_,"Depth",_,Depth}} ->
- to_depth(Depth);
+ h_depth_interpret(Depth);
_ ->
- 0
+ infinity
end.
+h_depth_interpret("infinity") -> infinity;
+h_depth_interpret("1") -> 1;
+h_depth_interpret(_) -> 0.
-to_depth("infinity") -> infinity;
-to_depth(L) ->
- case catch list_to_integer(L) of
- I when is_integer(I) -> I;
- _ -> 0
+h_destination(A) ->
+ Hs = (A#arg.headers)#headers.other,
+ case lists:keysearch("Destination", 3, Hs) of
+ {value, {http_header,_,_,_,Dest}} ->
+ Url = yaws_api:parse_url(Dest),
+ {Path,_} = yaws_api:url_decode_q_split(Url#url.path),
+ A#arg.docroot ++ "/" ++ Path;
+ _ ->
+ throw(501)
end.
-xml_expand(L) ->
- xml_expand(L, "utf-8").
+h_overwrite(A) ->
+ Hs = (A#arg.headers)#headers.other,
+ case lists:keysearch("Overwrite", 3, Hs) of
+ {value, {http_header, _ , _, _, "T"}} ->
+ true;
+ _ ->
+ false
+ end.
-xml_expand(L, Cset) ->
- Prolog = ["<?xml version=\"1.0\" encoding=\""++Cset++"\" ?>"],
- xmerl:export_simple(L,xmerl_xml,[{prolog,Prolog}]).
+h_timeout(A) ->
+ Hs = (A#arg.headers)#headers.other,
+ case lists:keysearch("Timeout", 3, Hs) of
+ {value, {_,_,"Timeout",_,T}} ->
+ case T of
+ "Second-"++TimeoutVal ->
+ Val = case catch list_to_integer(TimeoutVal) of
+ I when is_integer(I) -> I;
+ _ -> ?LOCK_LIFETIME
+ end,
+ min(Val,?LOCK_LIFETIME);
+ _ -> ?LOCK_LIFETIME
+ end;
+ _ ->
+ ?LOCK_LIFETIME
+ end.
+h_locktoken(A) ->
+ Hs = (A#arg.headers)#headers.other,
+ case lists:keysearch("Lock-Token", 3, Hs) of
+ {value, {_,_,"Lock-Token",_,URL}} ->
+ case URL of
+ "<opaquelocktoken:"++Token -> string:left(Token,36);
+ _ -> URL
+ end;
+ _ ->
+ undefined
+ end.
-parse_xml([]) -> [];
-parse_xml(L) when is_list(L) ->
- case catch xmerl_scan:string(L, [{namespace_conformant, true}]) of
- {X,_} when is_record(X, xmlElement) ->
- parse_dav(X);
- _Z ->
- ?elog("to_xml: error ~p~n", [_Z]),
- {error, "xml scanner failed"}
+h_if(_Path,A,Locks) ->
+ Hs = (A#arg.headers)#headers.other,
+ case lists:keysearch("If", 3, Hs) of
+ {value, {_,_,"If",_,If}} ->
+ List = if_parse(If,untagged),
+ Q = if_eval(A,Locks,List),
+ %?elog("If-header ~p evaluated to ~p~n",[List,Q]),
+ Q;
+ _ ->
+ undefined
end.
--define(CONTENT(X), X#xmlElement.content).
+if_parse([],_Resource) ->
+ [];
+if_parse(Line,Resource) when hd(Line)==32 ->
+ if_parse(tl(Line),Resource);
+if_parse(Line,untagged) when hd(Line)==60 -> % <
+ {Url,Rest} = if_parse_token(tl(Line),""),
+ if_parse(Rest,Url);
+if_parse(Line,Resource) when hd(Line)==40 -> % (
+ {Condition,Rest} = if_parse_condition(tl(Line),[],true),
+ [{Resource,Condition}|if_parse(Rest,untagged)].
--define(IS_PROPFIND(X), #xmlElement{expanded_name = {'DAV:',propfind}} = X).
--define(IS_PROP(X), #xmlElement{expanded_name = {'DAV:',prop}} = X).
--define(IS_NAME(X), #xmlElement{expanded_name = {'DAV:',name}} = X).
--define(IS_PARENTNAME(X), #xmlElement{expanded_name = {'DAV:',parentname}} = X).
+if_parse_condition(Line,List,_Bool) when hd(Line)==41 -> % )
+ {List,tl(Line)};
+if_parse_condition(Line,List,Bool) when hd(Line)==32 -> % whitespace
+ if_parse_condition(tl(Line),List,Bool);
+if_parse_condition("Not"++Line,List,_Bool) -> % negate
+ if_parse_condition(tl(Line),List,false);
+if_parse_condition(Line,List,Bool) when hd(Line)==60 -> % <
+ {Token,Rest} = if_parse_token(tl(Line),""),
+ if_parse_condition(Rest,[{Bool,state,Token}|List],true);
+if_parse_condition(Line,List,Bool) when hd(Line)==91 -> % [
+ {Etag,Rest} = if_parse_etag(tl(Line),""),
+ if_parse_condition(Rest,[{Bool,etag,Etag}|List],true).
+
+if_parse_token(Line,Buffer) when hd(Line)==62 -> % >
+ Uri = lists:reverse(Buffer),
+ Token1 = case Uri of
+ "opaquelocktoken:"++Token -> Token;
+ _ -> Uri
+ end,
+ {Token1,tl(Line)};
+if_parse_token([H|T],Buffer) ->
+ if_parse_token(T,[H|Buffer]).
+
+if_parse_etag(Line,Buffer) when hd(Line)==93 -> % ]
+ {lists:reverse(Buffer),tl(Line)};
+if_parse_etag([H|T],Buffer) ->
+ if_parse_etag(T,[H|Buffer]).
+
+%% if_eval(A,RequestPath,Conditions)
+if_eval(_A,_Locks,[]) ->
+ false;
+if_eval(A,Locks,[{Resource,AndList}|More]) ->
+ Target = case Resource of
+ untagged -> davname(A);
+ _ -> Resource -- davroot(A)
+ end,
+ if_eval_condition(AndList,A,Target,Locks) orelse if_eval(A,Locks,More).
+
+if_eval_condition(AndList,A,Target,Locks) ->
+ if_eval_condition(AndList,true,false,A,Target,Locks).
+
+if_eval_condition([],Result,Valid,_A,_Target,_Locks) ->
+ Result and Valid;
+if_eval_condition([{false,Kind,Ref}|T],Result,Valid,A,Target,Locks) ->
+ not if_eval_condition([{true,Kind,Ref}|T],Result,Valid,A,Target,Locks);
+if_eval_condition([{true,state,Ref}|T],_Result,_Valid,A,Target,Locks) ->
+ Result1 = if_eval_locktoken(Target,Ref,Locks),
+ Valid1 = true,
+ Result1 andalso if_eval_condition(T,Result1,Valid1,A,Target,Locks);
+if_eval_condition([{true,etag,Ref}|T],_Result,Valid,A,Target,Locks) ->
+ F = file:read_info(A#arg.docroot++Target),
+ E = yaws:make_etag(F),
+ Result1 = (E==Ref),
+ Valid1 = Valid,
+ Result1 andalso if_eval_condition(T,Result1,Valid1,A,Target,Locks).
+
+%% if_eval_locktoken(Target,Token,Locktokens) -> true|false
+if_eval_locktoken(_Target,_Token,[]) ->
+ false;
+if_eval_locktoken(Target,Token,[H|T]) ->
+ ((H#davlock.path == Target) and (H#davlock.id == Token)) orelse if_eval_locktoken(Target,Token,T).
+
+
+%% --------------------------------------------------------
+%% XML elements of RFC4918
+%%
+%% activelock
+-define(IS_ALLPROP(X), #xmlElement{expanded_name = {'DAV:',allprop}} = X).
+%% collection
+%% depth
+%% error
+-define(IS_EXCLUSIVE(X), #xmlElement{expanded_name = {'DAV:',exclusive}} = X).
-define(IS_HREF(X), #xmlElement{expanded_name = {'DAV:',href}} = X).
--define(IS_ISHIDDEN(X), #xmlElement{expanded_name = {'DAV:',ishidden}} = X).
--define(IS_ISCOLLECTION(X), #xmlElement{expanded_name = {'DAV:',iscollection}} = X).
--define(IS_ISREADONLY(X), #xmlElement{expanded_name = {'DAV:',isreadonly}} = X).
--define(IS_GETCONTENTTYPE(X), #xmlElement{expanded_name = {'DAV:',getcontenttype}} = X).
--define(IS_CONTENTCLASS(X), #xmlElement{expanded_name = {'DAV:',contentclass}} = X).
--define(IS_GETCONTENTLANGUAGE(X), #xmlElement{expanded_name = {'DAV:',getcontentlanguage}} = X).
--define(IS_CREATIONDATE(X), #xmlElement{expanded_name = {'DAV:',creationdate}} = X).
--define(IS_LASTACCESSED(X), #xmlElement{expanded_name = {'DAV:',lastaccessed}} = X).
--define(IS_GETLASTMODIFIED(X), #xmlElement{expanded_name = {'DAV:',getlastmodified}} = X).
--define(IS_GETCONTENTLENGTH(X), #xmlElement{expanded_name = {'DAV:',getcontentlength}} = X).
--define(IS_RESOURCETYPE(X), #xmlElement{expanded_name = {'DAV:',resourcetype}} = X).
--define(IS_ISSTRUCTUREDDOCUMENT(X), #xmlElement{expanded_name = {'DAV:',isstructureddocument}} = X).
--define(IS_DEFAULTDOCUMENT(X), #xmlElement{expanded_name = {'DAV:',defaultdocument}} = X).
--define(IS_DISPLAYNAME(X), #xmlElement{expanded_name = {'DAV:',displayname}} = X).
--define(IS_ISROOT(X), #xmlElement{expanded_name = {'DAV:',isroot}} = X).
-
-
-parse_dav(?IS_PROPFIND(X)) ->
- parse_propfind(?CONTENT(X), #propfind{});
-parse_dav(_X) ->
- %%?elog("parse_dav: GOT ~p~n", [_X]),
- {error, "parse_dav"}. % FIXME , webdav (tobbe)
-
-
-parse_propfind([?IS_PROP(H)|T], R) ->
- Prop = parse_prop(?CONTENT(H)),
- parse_propfind(T, R#propfind{prop = Prop});
+%% include % TODO: add this tag
+%% location
+%% lockentry
+-define(IS_LOCKINFO(X), #xmlElement{expanded_name = {'DAV:',lockinfo}} = X).
+%% lockroot
+-define(IS_LOCKSCOPE(X), #xmlElement{expanded_name = {'DAV:',lockscope}} = X).
+%% locktoken
+-define(IS_LOCKTYPE(X), #xmlElement{expanded_name = {'DAV:',locktype}} = X).
+%% multistatus
+-define(IS_OWNER(X), #xmlElement{expanded_name = {'DAV:',owner}} = X).
+-define(IS_PROP(X), #xmlElement{expanded_name = {'DAV:',prop}} = X).
+-define(IS_PROPERTYUPDATE(X), #xmlElement{expanded_name = {'DAV:',propertyupdate}} = X).
+-define(IS_PROPFIND(X), #xmlElement{expanded_name = {'DAV:',propfind}} = X).
+-define(IS_PROPNAME(X), #xmlElement{expanded_name = {'DAV:',propname}} = X).
+%% propstat
+-define(IS_REMOVE(X), #xmlElement{expanded_name = {'DAV:',remove}} = X).
+%% response
+%% responsedescription
+-define(IS_SET(X), #xmlElement{expanded_name = {'DAV:',set}} = X).
+-define(IS_SHARED(X), #xmlElement{expanded_name = {'DAV:',shared}} = X).
+%% status
+%% timeout
+-define(IS_WRITE(X), #xmlElement{expanded_name = {'DAV:',write}} = X).
+
+-define(CONTENT(X), X#xmlElement.content).
+
+%% Parameter is always list
+parse_propfind([]) -> [allprop]; % RFC4918: no body then allprop, is [] no body?
+parse_propfind(L) ->
+ case catch xmerl_scan:string(L, [{namespace_conformant, true}]) of
+ {?IS_PROPFIND(X),_} ->
+ parse_propfind(?CONTENT(X),[]);
+ _Z ->
+ throw(400)
+ end.
+parse_propfind([?IS_PROPNAME(_H)|_T], _R) ->
+ [propname];
+parse_propfind([?IS_ALLPROP(_H)|_T], _R) ->
+ [allprop]; %% TODO add include tag
+parse_propfind([?IS_PROP(H)|T], _R) ->
+ Props = parse_prop(?CONTENT(H)),
+ parse_propfind(T, Props);
parse_propfind([_H|T], R) ->
- %%?elog("parse_propfind: ~p~n",[_H]),
+ %% skip #xmlText, #xmlComment, etc.
parse_propfind(T, R);
parse_propfind([], R) ->
R.
+parse_proppatch(L) ->
+ case catch xmerl_scan:string(L, [{namespace_conformant, true}]) of
+ {?IS_PROPERTYUPDATE(X),_} ->
+ parse_proppatch(?CONTENT(X),[]);
+ _Z ->
+ throw(400)
+ end.
+parse_proppatch([?IS_SET(H)|T],R) ->
+ Props = parse_setremove(?CONTENT(H)),
+ parse_proppatch(T,[{set,Props}|R]);
+parse_proppatch([?IS_REMOVE(H)|T],R) ->
+ Props = parse_setremove(?CONTENT(H)),
+ parse_proppatch(T,[{remove,Props}|R]);
+parse_proppatch([_H|T], R) ->
+ %% skip #xmlText, #xmlComment, etc.
+ parse_proppatch(T, R);
+parse_proppatch([],R) ->
+ lists:reverse(R). % MUST proces in document order
+
+parse_setremove([?IS_PROP(X)]) ->
+ parse_prop(?CONTENT(X)).
+
parse_prop(L) ->
parse_prop(L, []).
-parse_prop([?IS_NAME(_H)|T], L) ->
- parse_prop(T, [name | L]);
-parse_prop([?IS_PARENTNAME(_H)|T], L) ->
- parse_prop(T, [parentname | L]);
-parse_prop([?IS_HREF(_H)|T], L) ->
- parse_prop(T, [href | L]);
-parse_prop([?IS_ISHIDDEN(_H)|T], L) ->
- parse_prop(T, [ishidden | L]);
-parse_prop([?IS_ISCOLLECTION(_H)|T], L) ->
- parse_prop(T, [iscollection | L]);
-parse_prop([?IS_ISREADONLY(_H)|T], L) ->
- parse_prop(T, [isreadonly | L]);
-parse_prop([?IS_GETCONTENTTYPE(_H)|T], L) ->
- parse_prop(T, [getcontenttype | L]);
-parse_prop([?IS_CONTENTCLASS(_H)|T], L) ->
- parse_prop(T, [contentclass | L]);
-parse_prop([?IS_GETCONTENTLANGUAGE(_H)|T], L) ->
- parse_prop(T, [getcontentlanguage | L]);
-parse_prop([?IS_CREATIONDATE(_H)|T], L) ->
- parse_prop(T, [creationdate | L]);
-parse_prop([?IS_LASTACCESSED(_H)|T], L) ->
- parse_prop(T, [lastaccessed | L]);
-parse_prop([?IS_GETLASTMODIFIED(_H)|T], L) ->
- parse_prop(T, [getlastmodified | L]);
-parse_prop([?IS_GETCONTENTLENGTH(_H)|T], L) ->
- parse_prop(T, [getcontentlength | L]);
-parse_prop([?IS_RESOURCETYPE(_H)|T], L) ->
- parse_prop(T, [resourcetype | L]);
-parse_prop([?IS_ISSTRUCTUREDDOCUMENT(_H)|T], L) ->
- parse_prop(T, [isstructureddocument | L]);
-parse_prop([?IS_DEFAULTDOCUMENT(_H)|T], L) ->
- parse_prop(T, [defaultdocument | L]);
-parse_prop([?IS_DISPLAYNAME(_H)|T], L) ->
- parse_prop(T, [displayname | L]);
-parse_prop([?IS_ISROOT(_H)|T], L) ->
- parse_prop(T, [isroot | L]);
-parse_prop([H|T], L) ->
- ?elog("parse_propfind: NYI ~p~n",[H]), % FIXME , webdav
- parse_prop(T, L);
+parse_prop([H|T],L) ->
+ case H of
+ H when is_record(H,xmlElement) ->
+ Value = case H#xmlElement.content of
+ [C] when is_record(C,xmlText) -> C#xmlText.value;
+ _ -> ""
+ end,
+ parse_prop(T,[{H#xmlElement.expanded_name,Value}|L]);
+ _ ->
+ parse_prop(T,L)
+ end;
parse_prop([], L) ->
- lists:reverse(L). % preserve order!
+ lists:reverse(L). % preserve order for PROPPATCH
+parse_lockinfo(L) ->
+ case catch xmerl_scan:string(L, [{namespace_conformant, true}]) of
+ {?IS_LOCKINFO(X),_} ->
+ parse_lockinfo(?CONTENT(X),#davlock{});
+ _Z ->
+ throw(400)
+ end.
+parse_lockinfo([?IS_LOCKSCOPE(H)|T], D) ->
+ X = parse_lockscope(?CONTENT(H)),
+ parse_lockinfo(T,D#davlock{scope=X});
+parse_lockinfo([?IS_LOCKTYPE(H)|T], D) ->
+ X = parse_locktype(?CONTENT(H)),
+ parse_lockinfo(T,D#davlock{type=X});
+parse_lockinfo([?IS_OWNER(H)|T], D) ->
+ X = parse_owner(?CONTENT(H)),
+ parse_lockinfo(T,D#davlock{owner=X});
+parse_lockinfo([_H|T],D) ->
+ parse_lockinfo(T,D); % skip spaces and comments, etc.
+parse_lockinfo([], D) ->
+ D.
-month(1) -> "Jan";
-month(2) -> "Feb";
-month(3) -> "Mar";
-month(4) -> "Apr";
-month(5) -> "May";
-month(6) -> "Jun";
-month(7) -> "Jul";
-month(8) -> "Aug";
-month(9) -> "Sep";
-month(10) -> "Oct";
-month(11) -> "Nov";
-month(12) -> "Dec".
+parse_lockscope([?IS_EXCLUSIVE(_H)|_T]) ->
+ exclusive;
+parse_lockscope([?IS_SHARED(_H)|_T]) ->
+ shared;
+parse_lockscope(_X) ->
+ throw(400).
-out207(L) ->
- outXXX(207, L).
+parse_locktype([?IS_WRITE(_H)|_T]) ->
+ write;
+parse_locktype(_) ->
+ throw(400).
-outXXX(XXX, L) ->
- [{status, XXX},
- {header, {content_type, "text/xml; charset=\"utf-8\""}},
- {html, L}].
+parse_owner(X) ->
+ Xml = xmerl:export_simple_content(X,xmerl_xml),
+ lists:flatten(Xml).
-out200() ->
- [{status, 200}].
+%% --------------------------------------------------------
+%% Status output
+%%
-out201() ->
- [{status, 201}].
+status(Status) ->
+ [{status, Status}].
+status(Status,Response) ->
+ Xml = xml_expand(Response),
+ [{status, Status},
+ {content, "application/xml; charset=\"utf-8\"", Xml}
+ ].
+status(Status,Headers,Response) ->
+ Xml = xml_expand(Response),
+ Hdrs = [ {header,H} || H <- Headers],
+ [{status, Status} | Hdrs] ++ [{content, "application/xml; charset=\"utf-8\"", Xml}].
-out403() ->
- [{status, 403}].
+xml_expand(L) ->
+ xml_expand(L, "utf-8").
-out409() ->
- [{status, 409}].
+xml_expand(L, Cset) ->
+ Prolog = ["<?xml version=\"1.0\" encoding=\""++Cset++"\" ?>"],
+ Xml = xmerl:export_simple(L,xmerl_xml,[{prolog,Prolog}]),
+ % MS requires \r\n at end of every XML response
+ [Xml|"\r\n"].
+%% --------------------------------------------------------
+%% File functions
+%%
-rmrf(Path) ->
- case file:read_file_info(Path) of
- {ok, F} when F#file_info.type == directory ->
- case file:list_dir(Path) of
- {ok, Fs} ->
- case rmrf(Path, Fs) of
- ok ->
- file:del_dir(Path);
- _Err ->
- ok
- end;
- Err ->
- Err
- end;
- {ok, _} ->
- file:delete(Path);
- Err ->
- Err
+file_do(Func,Params) ->
+ Result = erlang:apply(file,Func,Params),
+ case Result of
+ ok -> ok;
+ {ok,X} -> {ok,X};
+ {ok,X1,X2} -> {ok,X1,X2};
+ eof -> eof;
+ {error,eexist} -> throw(405);
+ {error,enoent} -> throw(409);
+ {error,eisdir} -> throw(409);
+ {error,enospace} -> throw(507);
+ _Error -> ?elog("file function returned ~p~n",[_Error]),throw(500)
end.
-rmrf(_Dir, []) ->
- ok;
-rmrf(Dir, [H|T]) ->
- F = filename:join(Dir, H),
- case rmrf(F) of
- ok ->
- rmrf(Dir, T);
- Err ->
- Err
+rmrf(Path) ->
+ {ok, F} = file_do(read_file_info,[Path]),
+ case F#file_info.type of
+ directory ->
+ {ok, Dir} = file_do(list_dir,[Path]),
+ [ rmrf(filename:join(Path,File)) || File <- Dir ],
+ file_do(del_dir,[Path]);
+ _ ->
+ file:delete(Path)
end.
@@ -527,15 +1035,17 @@ store_chunked_client_data(Fd, CliSock, SSL) ->
store_chunked_client_data(Fd, CliSock, SSL)
end.
+-define(MAX_PART, 65536).
+
store_client_data_len(_Fd, _CliSock, 0, _SSlBool) ->
ok;
store_client_data_len(Fd, CliSock, Len, SSlBool) ->
- case yaws:cli_recv(CliSock, Len, SSlBool) of
+ L = if Len<?MAX_PART -> Len; true -> ?MAX_PART end,
+ case yaws:cli_recv(CliSock, L, SSlBool) of
{ok, B} ->
ok = file:write(Fd, B),
store_client_data_len(Fd, CliSock, Len-size(B), SSlBool);
_Other ->
- ?Debug("store_client_data_len: ~p~n", [_Other]),
exit(normal)
end.
@@ -550,4 +1060,3 @@ store_client_data_all(Fd, CliSock, SSlBool) ->
?Debug("store_client_data_all: ~p~n", [_Other]),
exit(normal)
end.
-
View
355 src/yaws_davlock.erl
@@ -0,0 +1,355 @@
+%%----------------------------------------------------------------------
+%%% File : yaws_davlock.erl
+%%% Author : tjeerd <tjeerd@yolt.nl>
+%%% Purpose :
+%%% Created : 25 Jul 2012 by tjeerd <tjeerd@yolt.nl>
+%%%----------------------------------------------------------------------
+
+-module(yaws_davlock).
+-author('tjeerd@yolt.nl').
+-include("../include/yaws_dav.hrl").
+-include("../include/yaws_api.hrl").
+-include("../include/yaws.hrl").
+-include("yaws_debug.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-behaviour(gen_server).
+
+%% start/stop manually
+-export([start_link/0,stop/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
+ code_change/3]).
+
+%% API
+%
+% lock(Path,Lock) -> {ok,Id}|{error,Reason}
+% unlock(Path,Id) -> ok|{error,Reason}
+% locked(Path) -> true|false
+% check(Path,Id) -> ok|{error,Reason}
+% discover(Path) -> [Lock]
+% report() -> Report
+% dump() -> Dump
+%
+-export([lock/2, unlock/2, locked/1, check/2, discover/1, report/0, dump/0, clear/0]).
+-export([cleanup_manual/0]).
+
+start_link() -> gen_server:start_link({local,?MODULE},?MODULE,[],[]).
+stop() ->
+ gen_server:call(?MODULE,stop).
+
+lock(Path,Lock) ->
+ gen_server:call(?MODULE,{lock,Path,Lock}).
+unlock(Path,Id) ->
+ gen_server:call(?MODULE,{unlock,Path,Id}).
+locked(Path) ->
+ gen_server:call(?MODULE,{locked,Path}).
+check(Path,Id) ->
+ gen_server:call(?MODULE,{check,Path,Id}).
+discover(Path) ->
+ gen_server:call(?MODULE,{discover,Path}).
+report() ->
+ gen_server:call(?MODULE,report).
+dump() ->
+ gen_server:call(?MODULE,dump).
+clear() ->
+ gen_server:call(?MODULE,clear).
+cleanup_manual() ->
+ erlang:send(?MODULE,cleanup).
+
+%% init/1
+init([]) ->
+ %Table is a tree consisting of {Name, Locks, Children} tuples
+ Table = [],
+ erlang:send_after(?CLEANUP_INTERVAL*1000, self(), cleanup),
+ {ok, Table}.
+
+%% handle_call/3
+handle_call({lock,Path,Lock}, _From, Table) ->
+ try
+ T0 = erlang:now(),
+ Id = locktoken(),
+ %?elog("create lock ~p for ~p~n",[Id,Path]),
+ Lock1 = Lock#davlock{path=Path,id=Id,timestamp=T0},
+ Path1 = filename:split(Path),
+ Table1 = do_lock(Path1,Lock1,Table),
+ {reply, {ok,Id}, Table1}
+ catch
+ Status -> {reply, {error, Status}, Table};
+ _Error:Reason ->
+ ?elog("Unexpected error: ~p~n~p~n",[Reason,erlang:get_stacktrace()]),
+ {reply, {error, Reason}, Table}
+ end;
+handle_call({unlock,Path,Id}, _From, Table) ->
+ % even if the lock is not found, its removal is succesfull
+ %?elog("remove lock ~p for ~p~n",[Id,Path]),
+ Path1 = filename:split(Path),
+ Table1 = do_unlock(Path1,Id,Table),
+ {reply, ok, Table1};
+handle_call({locked,Path}, _From, Table) ->
+ L = filename:split(Path),
+ Lock = do_locked(L,Table),
+ {reply, Lock, Table};
+handle_call({check,Path,Id}, _From, Table) ->
+ L = filename:split(Path),
+ Lock = do_check(L,Id,Table),
+ {reply, Lock, Table};
+handle_call({discover,Path}, _From, Table) ->
+ L = filename:split(Path),
+ Lock = do_discover(L,Table),
+ {reply, Lock, Table};
+handle_call(report, _From, Table) ->
+ R = do_report(Table),
+ Report = [ {Lock,format(Name)} || {Lock,Name} <- R],
+ {reply, Report, Table};
+handle_call(dump, _From, Table) ->
+ {reply, Table, Table};
+handle_call(stop, _From, Table) ->
+ {stop, normal, stopped, Table};
+handle_call(clear, _From, _Table) ->
+ {reply, ok, []}.
+
+%% handle_cast/2
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%% handle_info/2
+handle_info(cleanup, Table) ->
+ erlang:send_after(?CLEANUP_INTERVAL*1000, self(), cleanup),
+ Table1 = do_cleanup(Table),
+ {noreply, Table1}.
+
+%% terminate/2
+terminate(_Reason, _State) ->
+ ok.
+
+%% code_change/3
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%----------------------------------------------------------------------
+%% do_lock(Lock,Path,Table) -> Table
+%%
+do_lock([],_Lock,Table) ->
+ % fileaname:split/1 can return empty list
+ Table;
+do_lock([H],Lock,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,Children}} ->
+ case do_lock_check(Locks) of
+ {shared,_} when Lock#davlock.scope == shared ->
+ lists:keyreplace(H,1,Table,{H,[Lock|Locks],Children});
+ unlocked ->
+ lists:keyreplace(H,1,Table,{H,[Lock],Children});
+ _ ->
+ throw(locked)
+ end;
+ false ->
+ lists:keystore(H,1,Table,{H,[Lock],[]})
+ end;
+do_lock([H|T],Lock,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,Children}} ->
+ case do_lock_check(Locks) of
+ {_,infinity} when Lock#davlock.scope == exclusive ->
+ throw(locked);
+ _ ->
+ lists:keyreplace(H,1,Table,{H,Locks,do_lock(T,Lock,Children)})
+ end;
+ false ->
+ lists:keystore(H,1,Table,{H,[],do_lock(T,Lock,[])})
+ end.
+
+do_lock_check(Locks) ->
+ do_lock_check(Locks,unlocked).
+do_lock_check([],Result) ->
+ Result;
+do_lock_check([H|T],_Result) ->
+ case H#davlock.depth of
+ infinity -> {H#davlock.scope,infinity};
+ 0 -> do_lock_check(T,{H#davlock.scope,0})
+ end.
+
+%%----------------------------------------------------------------------
+%% do_unlock(Path,Id,Table) -> Table
+%%
+do_unlock([],_Id,Table) ->
+ Table;
+do_unlock([_H|_T],_Id,[]) ->
+ [];
+do_unlock([H],Id,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,Children}} ->
+ Locks1 = do_unlock_id(Locks,Id),
+ case {Locks1,Children} of
+ {[],[]} ->
+ {_,_,Return} = lists:keytake(H,1,Table),
+ Return;
+ _ ->
+ lists:keyreplace(H,1,Table,{H,Locks1,Children})
+ end;
+ false ->
+ Table
+ end;
+do_unlock([H|T],Id,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,Children}} ->
+ Children1 = do_unlock(T,Id,Children),
+ case {Locks,Children1} of
+ {[],[]} ->
+ {_,_,Return} = lists:keytake(H,1,Table),
+ Return;
+ _ ->
+ lists:keyreplace(H,1,Table,{H,Locks,Children1})
+ end;
+ false ->
+ Table
+ end.
+
+do_unlock_id([],_Id) ->
+ [];
+do_unlock_id([H|T],Id) ->
+ case H#davlock.id of
+ Id -> T;
+ _ -> [H|do_unlock_id(T,Id)]
+ end.
+
+%%----------------------------------------------------------------------
+%% do_locked(Path,Table) -> Table
+%%
+do_locked([],_Table) ->
+ false;
+do_locked([H],Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,_}} when length(Locks)>0 -> true;
+ _ -> false
+ end;
+do_locked([H|T],Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,_Locks,Children}} -> do_locked(T,Children);
+ _ -> false
+ end.
+
+%%----------------------------------------------------------------------
+%% do_check(Path,Id,Table) -> Table
+%%
+do_check([],_Id,_Table) ->
+ {error,not_found};
+do_check([H],Id,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,_}} -> do_check_locks(Locks,Id);
+ false -> {error,not_found}
+ end;
+do_check([H|T],Id,Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,_Lock,Children}} -> do_check(T,Id,Children);
+ false -> {error,not_found}
+ end.
+
+do_check_locks([],_Id) ->
+ {error,not_found};
+do_check_locks([H|T],Id) ->
+ case H#davlock.id of
+ Id -> ok;
+ _ -> do_check_locks(T,Id)
+ end.
+
+%%----------------------------------------------------------------------
+%% do_discover(Path,Table) -> Locks
+%%
+do_discover([],_Table) ->
+ [];
+do_discover([H],Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,_}} -> Locks;
+ false -> []
+ end;
+do_discover([H|T],Table) ->
+ case lists:keysearch(H,1,Table) of
+ {value,{H,Locks,Children}} ->
+ do_discover_depth_infinity(Locks)++do_discover(T,Children);
+ false -> []
+ end.
+
+do_discover_depth_infinity([]) ->
+ [];
+do_discover_depth_infinity([H|T]) ->
+ Take = case H#davlock.depth of
+ infinity -> [H];
+ _ -> []
+ end,
+ Take ++ do_discover_depth_infinity(T).
+
+%%----------------------------------------------------------------------
+%% do_report(Path) -> Report
+%%
+do_report([]) ->
+ [];
+do_report([{Name,Lock,Children}|T]) ->
+ Rest = do_report(T),
+ case Lock of
+ unlocked ->
+ [ {locked,[Name|N]} || {locked,N} <- do_report(Children)]++Rest;
+ _ ->
+ _Owner = Lock#davlock.owner,
+ _Scope = Lock#davlock.scope,
+ _Type = Lock#davlock.type,
+ [{locked,[Name]}]++Rest++[ {locked,[Name|N]} || {locked,N} <- do_report(Children)]
+ end.
+
+%%----------------------------------------------------------------------
+%% do_cleanup(Table) -> Table
+%%
+do_cleanup([]) ->
+ [];
+do_cleanup([{Name,Locks,Children}|T]) ->
+ Locks1 = do_cleanup_locks(Locks),
+ Children1 = do_cleanup(Children),
+ if
+ (length(Locks1)==0) and (length(Children1)==0) ->
+ do_cleanup(T);
+ true ->
+ [{Name,Locks1,Children1}|do_cleanup(T)]
+ end.
+
+do_cleanup_locks([]) ->
+ [];
+do_cleanup_locks([H|T]) ->
+ T0 = H#davlock.timestamp,
+ T1 = erlang:now(),
+ Delta = timer:now_diff(T1,T0),
+ if
+ Delta > (H#davlock.timeout*1000000) ->
+ ?elog("discarded lock ~p~n",[H#davlock.id]),
+ do_cleanup_locks(T);
+ true ->
+ [H|do_cleanup_locks(T)]
+ end.
+
+%%----------------------------------------------------------------------
+locktoken() ->
+ % RFC4122 section 3 based UUID
+ Version = 1,
+ Variant = 2#10,
+ Now = {_, _, Micro} = now(),
+ Nowish = calendar:now_to_universal_time(Now),
+ Timestamp = calendar:datetime_to_gregorian_seconds(Nowish) * 1000000000,
+ <<TimeHi:12, TimeMid:16, TimeLow:32>> = <<Timestamp:60>>,
+ Clocksequence = <<Micro:14>>,
+ <<ClockseqHi:6, ClockseqLow:8>> = Clocksequence,
+ {ok,Ifs} = inet:getifaddrs(),
+ Addrs = [ lists:keysearch(hwaddr,1,Attr) || {_If,Attr} <- Ifs ],
+ Addr = lists:max([ A || {value,{hwaddr,A}} <- Addrs ]),
+ Node = list_to_binary(Addr),
+ UUID = <<TimeLow:32, TimeMid:16, Version:4, TimeHi:12,
+ Variant:2, ClockseqLow:8, ClockseqHi:6, Node/binary>>,
+ <<U0:32, U1:16, U2:16, U3:16, U4:48>> = UUID,
+ lists:flatten(io_lib:format("~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",[U0,U1,U2,U3,U4])).
+
+format([]) ->
+ [];
+format(["/"|T]) ->
+ format(T);
+format([H|T]) ->
+ lists:flatten(["/",H|format(T)]).
View
33 src/yaws_server.erl
@@ -1500,8 +1500,16 @@ not_implemented(CliSock, _IPPort, Req, Head) ->
'PUT'(CliSock, IPPort, Req, Head) ->
?Debug("PUT Req=~p~n H=~p~n", [?format_record(Req, http_request),
?format_record(Head, headers)]),
- body_method(CliSock, IPPort, Req, Head).
-
+ SC=get(sc),
+ case ?sc_has_dav(SC) of
+ true ->
+ %% body is handled by yaws_dav:put/1
+ ok = yaws:setopts(CliSock, [{packet, raw}, binary], yaws:is_ssl(SC)),
+ ARG = make_arg(CliSock, IPPort, Head, Req, undefined),
+ handle_request(CliSock, ARG, 0);
+ false ->
+ body_method(CliSock, IPPort, Req, Head)
+ end.
'DELETE'(CliSock, IPPort, Req, Head) ->
no_body_method(CliSock, IPPort, Req, Head).
@@ -1520,6 +1528,15 @@ not_implemented(CliSock, _IPPort, Req, Head) ->
%% ?format_record(Head, headers)]),
body_method(CliSock, IPPort, Req, Head).
+'PROPPATCH'(CliSock, IPPort, Req, Head) ->
+ body_method(CliSock, IPPort, Req, Head).
+
+'LOCK'(CliSock, IPPort, Req, Head) ->
+ body_method(CliSock, IPPort, Req, Head).
+
+'UNLOCK'(CliSock, IPPort, Req, Head) ->
+ body_method(CliSock, IPPort, Req, Head).
+
'MOVE'(CliSock, IPPort, Req, Head) ->
no_body_method(CliSock, IPPort, Req, Head).
@@ -1621,6 +1638,12 @@ handle_extension_method("PATCH", CliSock, IPPort, Req, Head) ->
'PATCH'(CliSock, IPPort, Req#http_request{method = 'PATCH'}, Head);
handle_extension_method("PROPFIND", CliSock, IPPort, Req, Head) ->
'PROPFIND'(CliSock, IPPort, Req, Head);
+handle_extension_method("PROPPATCH", CliSock, IPPort, Req, Head) ->
+ 'PROPPATCH'(CliSock, IPPort, Req, Head);
+handle_extension_method("LOCK", CliSock, IPPort, Req, Head) ->
+ 'LOCK'(CliSock, IPPort, Req, Head);
+handle_extension_method("UNLOCK", CliSock, IPPort, Req, Head) ->
+ 'UNLOCK'(CliSock, IPPort, Req, Head);
handle_extension_method("MKCOL", CliSock, IPPort, Req, Head) ->
'MKCOL'(CliSock, IPPort, Req, Head);
handle_extension_method("MOVE", CliSock, IPPort, Req, Head) ->
@@ -2322,6 +2345,12 @@ handle_ut(CliSock, ARG, UT = #urltype{type = dav}, N) ->
fun(A) -> yaws_dav:delete(A) end;
Req#http_request.method == "PROPFIND" ->
fun(A)-> yaws_dav:propfind(A) end;
+ Req#http_request.method == "PROPPATCH" ->
+ fun(A)-> yaws_dav:proppatch(A) end;
+ Req#http_request.method == "LOCK" ->
+ fun(A)-> yaws_dav:lock(A) end;
+ Req#http_request.method == "UNLOCK" ->
+ fun(A)-> yaws_dav:unlock(A) end;
Req#http_request.method == "MOVE" ->
fun(A)-> yaws_dav:move(A) end;
Req#http_request.method == "COPY" ->
View
5 src/yaws_sup.erl
@@ -57,12 +57,15 @@ child_specs() ->
YawsServ = {yaws_server, {yaws_server, start_link, YawsServArgs},
permanent, 120000, worker, [yaws_server]},
+ YawsDavLock = {yaws_davlock, {yaws_davlock, start_link, []},
+ permanent, 5000, worker, [yaws_davlock]},
+
%% and this guy will restart auxiliary procs that can fail
Sup = {yaws_sup_restarts,
{yaws_sup_restarts, start_link, []},
transient, infinity, supervisor, [yaws_sup_restarts]},
- [YawsLog, YawsTrace, YawsServ, Sup].
+ [YawsLog, YawsTrace, YawsServ, YawsDavLock, Sup].
%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
View
2 test/Makefile
@@ -1,6 +1,6 @@
include support/include.mk
-SUBDIRS = t1 t2 t3 t4 t5 t6 t7 eunit
+SUBDIRS = t1 t2 t3 t4 t5 t6 t7 t8 eunit
all: ibrowse
@cd src; $(MAKE) all
View
109 test/conf/davconf.conf
@@ -0,0 +1,109 @@
+
+
+logdir = ./logs
+
+# This the path to a directory where additional
+# beam code can be placed. The daemon will add this
+# directory to its search path
+
+ebin_dir = %YTOP%/test/ibrowse/ebin
+include_dir = %YTOP%/test/include
+
+
+
+# This is a debug variable, possible values are http | traffic | false
+# It is also possible to set the trace (possibly to the tty) while
+# invoking yaws from the shell as in
+# yaws -i -T -x (see man yaws)
+
+trace = false
+
+
+
+# it is possible to have yaws start additional
+# application specific code at startup
+#
+# runmod = mymodule
+
+
+# By default yaws will copy the erlang error_log and
+# end write it to a wrap log called report.log (in the logdir)
+# this feature can be turned off. This would typically
+# be the case when yaws runs within another larger app
+
+copy_error_log = true
+
+
+# Logs are wrap logs
+
+log_wrap_size = 1000000
+
+
+# Possibly resolve all hostnames in logfiles so webalizer
+# can produce the nice geography piechart
+
+log_resolve_hostname = false
+
+
+
+# fail completely or not if yaws fails
+# to bind a listen socket
+fail_on_bind_err = true
+
+
+
+# If yaws is started as root, it can, once it has opened
+# all relevant sockets for listening, change the uid to a
+# user with lower accessrights than root
+
+# username = nobody
+
+
+# If HTTP auth is used, it is possible to have a specific
+# auth log.
+# Deprecated and ignored. Now, this target must be set in server part
+#auth_log = true
+
+
+# When we're running multiple yaws systems on the same
+# host, we need to give each yaws system an individual
+# name. Yaws will write a number of runtime files under
+# /tmp/yaws/${id}
+# The default value is "default"
+
+
+# id = myname
+
+
+# earlier versions of Yaws picked the first virtual host
+# in a list of hosts with the same IP/PORT when the Host:
+# header doesn't match any name on any Host
+# This is often nice in testing environments but not
+# acceptable in real live hosting scenarios
+
+pick_first_virthost_on_nomatch = true
+
+
+# All unices are broken since it's not possible to bind to
+# a privileged port (< 1024) unless uid==0
+# There is a contrib in jungerl which makes it possible by means
+# of an external setuid root programm called fdsrv to listen to
+# to privileged port.
+# If we use this feature, it requires fdsrv to be properly installed.
+# Doesn't yet work with SSL.
+
+use_fdsrv = false
+
+
+
+
+# end then a set of virtual servers
+
+<server localhost>
+ port = 8000
+ listen = 0.0.0.0
+ docroot = %YTOP%/test/t8/www
+ dav = true
+</server>
+
+
View
0 test/t7/.placeholder
No changes.
View
28 test/t8/Makefile
@@ -0,0 +1,28 @@
+include ../support/include.mk
+
+.PHONY: all test conf newconf t1 json
+
+all: conf setup
+ @echo "all ok"
+
+## to run test, do
+# make all test
+
+test: all
+ set -e ; \
+ for t in t1; do \
+ $(MAKE) $$t ; \
+ done
+
+newconf:
+ @rm -f yaws.conf
+ $(MAKE) conf
+
+t1: newconf start
+ $(SHELL