Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

548 lines (486 sloc) 17.688 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
{sslsocket,_,_} -> "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),
N = yaws_revproxy:store_chunk_num(CliSock, SSL),
yaws:setopts(CliSock, [binary, {packet, raw}], SSL),
if
N == 0 ->
_Tmp=yaws:do_recv(CliSock, 2, SSL);%% flush last crnl
true ->
B = yaws_revproxy:store_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.