Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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...
commit efa9effca98b8fe0c3f91e855b087b1dcc33e54e 1 parent 00aa12d
Steve Vinoski vinoski authored
4 .gitignore
View
@@ -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
23 include/yaws_dav.hrl
View
@@ -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.
1  src/Makefile
View
@@ -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 \
12 src/yaws.erl
View
@@ -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].
4 src/yaws_api.erl
View
@@ -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";
1,233 src/yaws_dav.erl
View
@@ -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,37 +18,112 @@
-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",
@@ -54,7 +131,10 @@ put(SC, ARG) ->
"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.
-
355 src/yaws_davlock.erl
View
@@ -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)]).
33 src/yaws_server.erl
View
@@ -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" ->
5 src/yaws_sup.erl
View
@@ -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].
%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
2  test/Makefile
View
@@ -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
109 test/conf/davconf.conf
View
@@ -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>
+
+
0  test/t7/.placeholder
View
No changes.
28 test/t8/Makefile
View
@@ -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) ./davtest
+ $(MAKE) stop
+
+conf: davconf
+
+clean: tclean
+ -rm -rf $(TMP_DATAFILE) yaws.conf
33 test/t8/davtest
View
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+#
+#
+
+printf "\n === WebDAV tests ===\n\n"
+
+printf "check environment\n"
+
+cadaver=cadaver
+output=logs/cadaver.out
+command -v $cadaver >/dev/null 2>&1 || { echo >&2 "- test requires cadaver but it is not installed"; exit 1; }
+
+rm -f erl_crash.dump
+rm -f davtest.out
+
+printf "run the tests\n"
+
+$cadaver http://localhost:8000 <davtest.in > $output
+
+rm -rf www/locked
+if [ -f erl_crash.dump ]; then
+ echo "- yaws crashed, see logs/report.log and erl_crash.dump for details"
+ exit 1
+fi
+success=`grep -c succeeded. $output`
+if [ $success -ne 9 ]; then
+ echo "- not all cadaver actions succeeded, see $output for details"
+ exit 1
+fi
+printf "\n** All tests completed successfully.\n\n"
+exit 0
+
15 test/t8/davtest.in
View
@@ -0,0 +1,15 @@
+unset tolerant
+set namespace DAV:
+ls
+propnames test
+put davtest.in
+rm davtest.in
+mkcol locked
+lock locked
+cd locked
+put davtest.in
+cd ..
+unlock locked
+rmcol locked
+
+
1  test/t8/www/test
View
@@ -0,0 +1 @@
+test
1  www/contributors.txt
View
@@ -98,6 +98,7 @@ Brian Templeton
Nicolas Thauvin
Fredrik Thulin
Torbjörn Törnqvist
+Tjeerd van der Laan
Steve Vinoski
Lev Walkin
wde
Please sign in to comment.
Something went wrong with that request. Please try again.