Current section

Files

Jump to
webdavfilez src webdavfilez_request.erl
Raw

src/webdavfilez_request.erl

%% @doc WebDAV http request handler and streamer using httpc.
%% @author Marc Worrell
%% @copyright 2022-2025 Marc Worrell
%% @end
%% Copyright 2022-2025 Marc Worrell
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
-module(webdavfilez_request).
-export([
request/5,
request_with_body/5,
stream_to_fun/3,
stream_loop/4,
http_status/1,
ct/1,
basic_auth/1
]).
-define(CONNECT_TIMEOUT, 60000). % 60s
-define(TIMEOUT, 1800000). % 30m
-include_lib("kernel/include/logger.hrl").
request(Config, Method, Url, Headers, Options) ->
case hostname(Url) of
{ok, Host} ->
Date = httpd_util:rfc1123_date(),
AllHeaders = [
{"Authorization", basic_auth(Config)},
{"Date", Date}
| Headers
],
httpc:request(Method, {to_list(Url), AllHeaders},
opts(Host, Config), [{body_format, binary}|Options],
httpc_webdavfilez_profile);
{error, _} = Error ->
Error
end.
request_with_body(Config, Method, Url, Headers, Body) ->
case hostname(Url) of
{ok, Host} ->
{"Content-Type", ContentType} = proplists:lookup("Content-Type", Headers),
Date = httpd_util:rfc1123_date(),
Hs1 = [
{"Authorization", basic_auth(Config)},
{"Date", Date}
| Headers
],
jobs:run(webdavfilez_jobs,
fun() ->
httpc:request(Method, {to_list(Url), Hs1, ContentType, Body},
opts(Host, Config), [],
httpc_webdavfilez_profile)
end);
{error, _} = Error ->
Error
end.
stream_to_fun(Config, Url, Fun) ->
case request(Config, get, Url, [], [{stream,{self,once}}, {sync,false}]) of
{ok, RequestId} ->
receive
{http, {RequestId, stream_start, Headers, Pid}} ->
start_fun(Fun),
call_fun(Fun, {content_type, ct(Headers)}),
httpc:stream_next(Pid),
?MODULE:stream_loop(RequestId, Pid, Url, Fun);
{http, {RequestId, {_,_,_} = HttpRet}}->
case http_status(HttpRet) of
ok ->
eof_fun(Fun),
ok;
{error, Reason} = Error ->
error_fun(Fun, Reason),
Error
end;
{http, {RequestId, Other}} ->
?LOG_ERROR(#{
text => <<"Unexpected HTTP message">>,
in => webdavfilez,
result => error,
reason => Other,
url => Url
}),
error_fun(Fun, Other),
{error, Other}
after ?CONNECT_TIMEOUT ->
error_fun(Fun, timeout),
{error, timeout}
end;
{error, _} = Error ->
Error
end.
%% @private
stream_loop(RequestId, Pid, Url, Fun) ->
receive
{http, {RequestId, stream_end, Headers}} ->
call_fun(Fun, {headers, Headers}),
eof_fun(Fun),
ok;
{http, {RequestId, stream, Data}} ->
call_fun(Fun, Data),
httpc:stream_next(Pid),
?MODULE:stream_loop(RequestId, Pid, Url, Fun);
{http, {RequestId, Other}} ->
?LOG_ERROR(#{
text => <<"Unexpected HTTP message">>,
in => webdavfilez,
result => error,
reason => Other,
url => Url
}),
error_fun(Fun, Other),
{error, Other}
after ?TIMEOUT ->
error_fun(Fun, timeout),
{error, timeout}
end.
start_fun({Ref, Pid}) when is_pid(Pid) ->
Pid ! {webdav, {Ref, stream_start, []}};
start_fun(Fun) ->
call_fun(Fun, stream_start).
call_fun({M,F,A}, Arg) ->
erlang:apply(M,F,A++[Arg]);
call_fun(Fun, Arg) when is_function(Fun) ->
Fun(Arg);
call_fun(Pid, Arg) when is_pid(Pid) ->
Pid ! {webdav, Arg};
call_fun({Ref, Pid}, Arg) when is_pid(Pid) ->
Pid ! {webdav, {Ref, stream, Arg}}.
eof_fun({M,F,A}) ->
erlang:apply(M,F,A++[eof]);
eof_fun(Fun) when is_function(Fun) ->
Fun(eof);
eof_fun(Pid) when is_pid(Pid) ->
Pid ! {webdav, eof};
eof_fun({Ref, Pid}) when is_pid(Pid) ->
Pid ! {webdav, {Ref, stream_end, []}}.
error_fun({M,F,A}, Error) ->
erlang:apply(M,F,A++[Error]);
error_fun(Fun, Error) when is_function(Fun) ->
Fun(Error);
error_fun(Pid, Error) when is_pid(Pid) ->
Pid ! {webdav, Error};
error_fun({Ref, Pid}, Error) when is_pid(Pid) ->
Pid ! {webdav, {Ref, Error}}.
http_status({{_,Code,_}, _Headers, _Body})
when Code =:= 200;
Code =:= 201;
Code =:= 204;
Code =:= 206 ->
ok;
http_status({{_,404,_}, _Headers, _Body}) ->
{error, enoent};
http_status({{_,403,_}, _Headers, _Body}) ->
{error, forbidden};
http_status({{_,Code,_}, _Headers, _Body}) ->
{error, Code}.
opts(Host, Config) ->
[
{connect_timeout, ?CONNECT_TIMEOUT},
{ssl, tls_options(Host, Config)},
{timeout, ?TIMEOUT}
].
tls_options(_Host, #{ tls_options := Options }) when is_list(Options), Options =/= [] ->
Options;
tls_options(Host, _Config) ->
case z_ip_address:is_local_name(Host) of
true ->
[ {verify, verify_none} ];
false ->
tls_certificate_check:options(Host)
end.
basic_auth(#{ username := Username, password := Password }) ->
Username1 = unicode:characters_to_binary(Username),
Password1 = unicode:characters_to_binary(Password),
"Basic " ++ base64:encode_to_string(<<Username1/binary, $:, Password1/binary>>).
hostname(Url) ->
case uri_string:parse(Url) of
#{ host := Host } ->
{ok, to_list(Host)};
_ ->
{error, url_hostname}
end.
to_list(B) when is_binary(B) -> binary_to_list(B);
to_list(L) when is_list(L) -> L.
ct(Headers) ->
Mime = list_to_binary(proplists:get_value("content-type", Headers, "binary/octet-stream")),
[ Mime1 | _ ] = binary:split(Mime, <<";">>),
z_string:trim(Mime1).