Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

551 lines (489 sloc) 17.732 kb
-module(yaws_dav).
%%%-------------------------------------------------------------------
%%% Created : 15 May 2005 by Tobbet <tobbe@tornkvist.org>
%%% Modified: 21 Nov 2005 by <mbj@tail-f.com>
%%% Desc. : WebDav specifics.
%%% To use, add a line dav = true in the <server>.
%%% TODO: fix more fine-grained permissions
%%%-------------------------------------------------------------------
-export([propfind/1, delete/1, put/2, mkcol/1, move/1, copy/1]).
-export([parse_xml/1, xml_expand/1, xml_expand/2]).
-include("../include/yaws_dav.hrl").
-include("../include/yaws_api.hrl").
-include("../include/yaws.hrl").
-include("yaws_debug.hrl").
-include_lib("xmerl/include/xmerl.hrl").
-include_lib("kernel/include/file.hrl").
-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()
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
end,
SSL = yaws:is_ssl(SC),
FName = davpath(ARG),
CliSock = ARG#arg.clisock,
TmpName = FName ++ ".tmp",
%% FIXME: first check if we can write to original file??
case file:open(TmpName, [raw,write]) of
{ok, Fd} ->
try
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)
end;
Len when is_integer(PPS) ->
Int_len = list_to_integer(Len),
if
Int_len == 0 ->
ok;
PPS < Int_len, CT == multipart ->
%% FIXME: handle this
%% {partial,
store_client_data(Fd,CliSock, PPS, SSL); % };
true ->
store_client_data(Fd, CliSock, Int_len, SSL)
end;
Len when PPS == nolimit ->
Int_len = list_to_integer(Len),
if
Int_len == 0 ->
ok;
true ->
store_client_data(Fd, CliSock, Int_len, SSL)
end
end,
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()
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()
end.
copy(A) ->
copy_move(A, fun do_copy/2).
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}]
end.
do_move(From, To) ->
case file:rename(From, To) of
ok ->
out201();
_ ->
case file:copy(From, To) of
{ok,_} ->
ok = file:delete(From),
out201();
{error, Reason} ->
?elog("move from ~p to ~p failed: ~p\n",
[From, To, Reason]),
out409()
end
end.
do_copy(From, To) ->
case file:copy(From, To) of
{ok, _} ->
out201();
Error ->
?elog("move from ~p to ~p failed: ~p\n",
[From, To, Error]),
out409()
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)
end.
date_string({{Y,M,D}, {Hr,Min,Sec}}) ->
lists:concat([D, " ", month(M), " ", Y, " ", Hr, ":", Min, ":", Sec]).
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}]
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
end.
exists(Path) ->
case file:read_file_info(Path) of
{ok, _} -> true;
_ -> false
end.
%% FIXME: how to do this in a portable way? on unix we could check inode...
is_same(A, B) ->
A == B.
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(_) ->
[].
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"
end,
Host = (A#arg.headers)#headers.host,
Method ++ "://" ++ Host.
depth(A) ->
%%
%% Look for: {http_header, _Num, 'Depth', _, Depth}
%%
Hs = (A#arg.headers)#headers.other,
case lists:keysearch("Depth", 3, Hs) of
{value, {_,_,"Depth",_,Depth}} ->
to_depth(Depth);
_ ->
0
end.
to_depth("infinity") -> infinity;
to_depth(L) ->
case catch list_to_integer(L) of
I when is_integer(I) -> I;
_ -> 0
end.
xml_expand(L) ->
xml_expand(L, "utf-8").
xml_expand(L, Cset) ->
Prolog = ["<?xml version=\"1.0\" encoding=\""++Cset++"\" ?>"],
xmerl:export_simple(L,xmerl_xml,[{prolog,Prolog}]).
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"}
end.
-define(CONTENT(X), X#xmlElement.content).
-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).
-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});
parse_propfind([_H|T], R) ->
%%?elog("parse_propfind: ~p~n",[_H]),
parse_propfind(T, R);
parse_propfind([], R) ->
R.
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([], L) ->
lists:reverse(L). % preserve order!
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".
out207(L) ->
outXXX(207, L).
outXXX(XXX, L) ->
[{status, XXX},
{header, {content_type, "text/xml; charset=\"utf-8\""}},
{html, L}].
out200() ->
[{status, 200}].
out201() ->
[{status, 201}].
out403() ->
[{status, 403}].
out409() ->
[{status, 409}].
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
end.
rmrf(_Dir, []) ->
ok;
rmrf(Dir, [H|T]) ->
F = filename:join(Dir, H),
case rmrf(F) of
ok ->
rmrf(Dir, T);
Err ->
Err
end.
store_client_data(Fd, CliSock, all, SSlBool) ->
store_client_data_all(Fd, CliSock, SSlBool);
store_client_data(Fd, CliSock, Len, SSlBool) ->
store_client_data_len(Fd, CliSock, Len, SSlBool).
%% not nice to support this for ssl sockets
store_chunked_client_data(Fd, CliSock, SSL) ->
yaws:setopts(CliSock, [binary, {packet, line}], SSL),
%% Ignore chunk extentions
{N, _Exts} = yaws:get_chunk_header(CliSock, SSL),
yaws:setopts(CliSock, [binary, {packet, raw}], SSL),
if
N == 0 ->
%% Ignore chunk trailer
yaws:get_chunk_trailer(CliSock, SSL),
ok;
true ->
B = yaws:get_chunk(CliSock, N, 0,SSL),
yaws:eat_crnl(CliSock,SSL),
ok = file:write(Fd, B),
store_chunked_client_data(Fd, CliSock, SSL)
end.
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
{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.
store_client_data_all(Fd, CliSock, SSlBool) ->
case yaws:cli_recv(CliSock, 4000, SSlBool) of
{ok, B} ->
ok = file:write(Fd, B),
store_client_data_all(Fd, CliSock, SSlBool);
eof ->
ok;
_Other ->
?Debug("store_client_data_all: ~p~n", [_Other]),
exit(normal)
end.
Jump to Line
Something went wrong with that request. Please try again.