%%% Copyright (c) 2014-2015, NORDUnet A/S. %%% See LICENSE for licensing information. %%% -module(plop_httputil). -export([request/4]). -include_lib("hackney/include/hackney_lib.hrl"). -include_lib("public_key/include/public_key.hrl"). get_auth_header(Headers) -> case hackney_headers:get_value("X-Catlfish-Auth", Headers) of undefined -> undefined; Result when is_binary(Result) -> lager:debug("received auth header: ~p", [Result]), binary_to_list(Result) end. add_auth(Method, Path, Headers, Data) -> AuthHeader = http_auth:create_auth(Method, Path, Data), lager:debug("sent auth header: ~p", [AuthHeader]), [{"X-Catlfish-Auth", AuthHeader} | Headers]. get_cert_hostname(Cert) -> TBSCert = Cert#'OTPCertificate'.tbsCertificate, Subject = TBSCert#'OTPTBSCertificate'.subject, {rdnSequence, RDNs} = Subject, CNs = lists:map(fun([#'AttributeTypeAndValue'{type=?'id-at-commonName', value=Value}]) -> Value; (_) -> none end, RDNs), case [CN || CN <- CNs, CN /= none] of [{utf8String, BinCN} | _] when is_binary(BinCN) -> binary_to_list(BinCN); [CN | _] -> CN; [] -> "" end. verify_fun(_,{bad_cert, _} = Reason, _) -> {fail, Reason}; verify_fun(_,{extension, _}, UserState) -> {unknown, UserState}; verify_fun(_, valid, UserState) -> {valid, UserState}; verify_fun(Cert, valid_peer, UserState) -> CertHostname = get_cert_hostname(Cert), case lists:keyfind(check_hostname, 1, UserState) of {check_hostname, CertHostname} -> {valid, UserState}; {check_hostname, ExpectedHostname} -> lager:info("Expected hostname ~p but certificate has hostname ~p", [ExpectedHostname, CertHostname]), {fail, "Hostname does not match"}; false -> {valid, UserState} end. read_and_verify_cacertfile(Filename) -> {ok, PemBin} = file:read_file(Filename), [KeyPem] = public_key:pem_decode(PemBin), {'Certificate', Der, _} = KeyPem, CalculatedHash = crypto:hash(sha256, Der), CorrectHash = application:get_env(plop, https_cacert_fingerprint, none), CorrectHash = CalculatedHash, Der. request(DebugTag, URL, Headers, <<>>) -> request(DebugTag, URL, Headers, <<>>, get); request(DebugTag, URL, Headers, RequestBody) -> request(DebugTag, URL, Headers, RequestBody, post). -define(MAX_CHUNK_SIZE, 512*1024). chunk_data(<<>>) -> eof; chunk_data(Data) when is_binary(Data) -> ChunkSize = min(size(Data), ?MAX_CHUNK_SIZE), lager:debug("~p data left, sending ~p bytes", [size(Data), ChunkSize]), {Chunk, Rest} = split_binary(Data, ChunkSize), {ok, Chunk, Rest}. request(DebugTag, URL, Headers, RequestBody, Method) -> Starttime = os:timestamp(), ParsedURL = hackney_url:parse_url(URL), CACertFile = application:get_env(plop, https_cacertfile, none), CACert = read_and_verify_cacertfile(CACertFile), MethodString = case Method of get -> "GET"; post -> "POST" end, #hackney_url{path = Path, host = Host, qs = QueryString, raw_path = RawPath} = ParsedURL, lager:debug("~s: sending http request to ~p", [DebugTag, URL]), case hackney:connect(ParsedURL, [{ssl_options, [{cacerts, [CACert]}, {verify, verify_peer}, {verify_fun, {fun verify_fun/3, [{check_hostname, Host}]}} ]}]) of {ok, ConnRef} -> lager:debug("~s: connected to ~p", [DebugTag, URL]), {ok, StatusCode, RespHeaders, ClientRef} = case Method of post -> hackney:send_request(ConnRef, {Method, Path, add_auth(MethodString, Path, Headers, RequestBody), {fun chunk_data/1, RequestBody}}); get -> hackney:send_request(ConnRef, {Method, RawPath, add_auth(MethodString, Path, Headers, QueryString), <<>>}) end, lager:debug("~s: received headers for ~p: ~p", [DebugTag, URL, RespHeaders]), {ok, Body} = hackney:body(ClientRef), Stoptime = os:timestamp(), hackney:close(ClientRef), lager:debug("~s: received body for ~p: status ~p, time ~p", [DebugTag, URL, StatusCode, timer:now_diff(Stoptime, Starttime)]), StatusLine = {none, StatusCode, none}, AuthHeader = get_auth_header(hackney_headers:new(RespHeaders)), {http_auth:verify_auth(AuthHeader, "REPLY", binary_to_list(Path), Body), StatusLine, RespHeaders, Body}; {error, Error} -> {error, Error} end.