You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
7.8 KiB

преди 10 години
преди 10 години
преди 10 години
преди 10 години
преди 10 години
преди 10 години
преди 9 години
преди 9 години
  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. -module(rebar_pkg_resource).
  4. -behaviour(rebar_resource).
  5. -export([lock/2
  6. ,download/3
  7. ,needs_update/2
  8. ,make_vsn/1]).
  9. -export([request/2
  10. ,etag/1
  11. ,ssl_opts/1]).
  12. -include("rebar.hrl").
  13. -include_lib("public_key/include/OTP-PUB-KEY.hrl").
  14. lock(_AppDir, Source) ->
  15. Source.
  16. needs_update(Dir, {pkg, _Name, Vsn, _Hash}) ->
  17. [AppInfo] = rebar_app_discover:find_apps([Dir], all),
  18. case rebar_app_info:original_vsn(AppInfo) =:= ec_cnv:to_list(Vsn) of
  19. true ->
  20. false;
  21. false ->
  22. true
  23. end.
  24. download(TmpDir, Pkg={pkg, Name, Vsn, _Hash}, State) ->
  25. CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN),
  26. {ok, PackageDir} = rebar_packages:package_dir(State),
  27. Package = binary_to_list(<<Name/binary, "-", Vsn/binary, ".tar">>),
  28. CachePath = filename:join(PackageDir, Package),
  29. case rebar_utils:url_append_path(CDN, filename:join(?REMOTE_PACKAGE_DIR, Package)) of
  30. {ok, Url} ->
  31. cached_download(TmpDir, CachePath, Pkg, Url, etag(CachePath), State);
  32. _ ->
  33. {fetch_fail, Name, Vsn}
  34. end.
  35. cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn, _Hash}, Url, ETag, State) ->
  36. case request(Url, ETag) of
  37. {ok, cached} ->
  38. ?INFO("Version cached at ~s is up to date, reusing it", [CachePath]),
  39. serve_from_cache(TmpDir, CachePath, Pkg, State);
  40. {ok, Body, NewETag} ->
  41. ?INFO("Downloaded package, caching at ~s", [CachePath]),
  42. serve_from_download(TmpDir, CachePath, Pkg, NewETag, Body, State);
  43. error when ETag =/= false ->
  44. ?INFO("Download error, using cached file at ~s", [CachePath]),
  45. serve_from_cache(TmpDir, CachePath, Pkg, State);
  46. error ->
  47. {fetch_fail, Name, Vsn}
  48. end.
  49. serve_from_cache(TmpDir, CachePath, Pkg, State) ->
  50. {Files, Contents, Version, Meta} = extract(TmpDir, CachePath),
  51. case checksums(Pkg, Files, Contents, Version, Meta, State) of
  52. {Chk, Chk, Chk, Chk} ->
  53. ok = erl_tar:extract({binary, Contents}, [{cwd, TmpDir}, compressed]),
  54. {ok, true};
  55. {_Hash, Chk, Chk, Chk} ->
  56. ?DEBUG("Expected hash ~p does not match checksums ~p", [_Hash, Chk]),
  57. {unexpected_hash, CachePath, _Hash, Chk};
  58. {Chk, _Bin, Chk, Chk} ->
  59. ?DEBUG("Checksums: registry: ~p, pkg: ~p", [Chk, _Bin]),
  60. {failed_extract, CachePath};
  61. {Chk, Chk, _Reg, Chk} ->
  62. ?DEBUG("Checksums: registry: ~p, pkg: ~p", [_Reg, Chk]),
  63. {bad_registry_checksum, CachePath};
  64. {_Hash, _Bin, _Reg, _Tar} ->
  65. ?DEBUG("Checksums: expected: ~p, registry: ~p, pkg: ~p, meta: ~p", [_Hash, _Reg, _Bin, _Tar]),
  66. {bad_checksum, CachePath}
  67. end.
  68. serve_from_download(TmpDir, CachePath, Package, ETag, Binary, State) ->
  69. ?DEBUG("Writing ~p to cache at ~s", [Package, CachePath]),
  70. file:write_file(CachePath, Binary),
  71. case etag(CachePath) of
  72. ETag ->
  73. serve_from_cache(TmpDir, CachePath, Package, State);
  74. FileETag ->
  75. ?DEBUG("Downloaded file ~s ETag ~s doesn't match returned ETag ~s", [CachePath, ETag, FileETag]),
  76. {bad_download, CachePath}
  77. end.
  78. extract(TmpDir, CachePath) ->
  79. ec_file:mkdir_p(TmpDir),
  80. {ok, Files} = erl_tar:extract(CachePath, [memory]),
  81. {"contents.tar.gz", Contents} = lists:keyfind("contents.tar.gz", 1, Files),
  82. {"VERSION", Version} = lists:keyfind("VERSION", 1, Files),
  83. {"metadata.config", Meta} = lists:keyfind("metadata.config", 1, Files),
  84. {Files, Contents, Version, Meta}.
  85. checksums(Pkg={pkg, _Name, _Vsn, Hash}, Files, Contents, Version, Meta, State) ->
  86. Blob = <<Version/binary, Meta/binary, Contents/binary>>,
  87. <<X:256/big-unsigned>> = crypto:hash(sha256, Blob),
  88. BinChecksum = list_to_binary(string:to_upper(lists:flatten(io_lib:format("~64.16.0b", [X])))),
  89. RegistryChecksum = rebar_packages:registry_checksum(Pkg, State),
  90. {"CHECKSUM", TarChecksum} = lists:keyfind("CHECKSUM", 1, Files),
  91. {Hash, BinChecksum, RegistryChecksum, TarChecksum}.
  92. make_vsn(_) ->
  93. {error, "Replacing version of type pkg not supported."}.
  94. request(Url, ETag) ->
  95. case httpc:request(get, {Url, [{"if-none-match", ETag} || ETag =/= false]++[{"User-Agent", rebar_utils:user_agent()}]},
  96. [{ssl, ssl_opts(Url)}, {relaxed, true}],
  97. [{body_format, binary}],
  98. rebar) of
  99. {ok, {{_Version, 200, _Reason}, Headers, Body}} ->
  100. ?DEBUG("Successfully downloaded ~s", [Url]),
  101. {"etag", ETag1} = lists:keyfind("etag", 1, Headers),
  102. {ok, Body, string:strip(ETag1, both, $")};
  103. {ok, {{_Version, 304, _Reason}, _Headers, _Body}} ->
  104. ?DEBUG("Cached copy of ~s still valid", [Url]),
  105. {ok, cached};
  106. {ok, {{_Version, Code, _Reason}, _Headers, _Body}} ->
  107. ?DEBUG("Request to ~p failed: status code ~p", [Url, Code]),
  108. error;
  109. {error, Reason} ->
  110. ?DEBUG("Request to ~p failed: ~p", [Url, Reason]),
  111. error
  112. end.
  113. etag(Path) ->
  114. case file:read_file(Path) of
  115. {ok, Binary} ->
  116. <<X:128/big-unsigned-integer>> = crypto:hash(md5, Binary),
  117. string:to_lower(lists:flatten(io_lib:format("~32.16.0b", [X])));
  118. {error, _} ->
  119. false
  120. end.
  121. ssl_opts(Url) ->
  122. case get_ssl_config() of
  123. ssl_verify_enabled ->
  124. ssl_opts(ssl_verify_enabled, Url);
  125. ssl_verify_disabled ->
  126. [{verify, verify_none}]
  127. end.
  128. ssl_opts(ssl_verify_enabled, Url) ->
  129. case check_ssl_version() of
  130. true ->
  131. {ok, {_, _, Hostname, _, _, _}} = http_uri:parse(ec_cnv:to_list(Url)),
  132. VerifyFun = {fun ssl_verify_hostname:verify_fun/3, [{check_hostname, Hostname}]},
  133. CACerts = certifi:cacerts(),
  134. [{verify, verify_peer}, {depth, 2}, {cacerts, CACerts}
  135. ,{partial_chain, fun partial_chain/1}, {verify_fun, VerifyFun}];
  136. false ->
  137. ?WARN("Insecure HTTPS request (peer verification disabled), please update to OTP 17.4 or later", []),
  138. [{verify, verify_none}]
  139. end.
  140. partial_chain(Certs) ->
  141. Certs1 = [{Cert, public_key:pkix_decode_cert(Cert, otp)} || Cert <- Certs],
  142. CACerts = certifi:cacerts(),
  143. CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts],
  144. case ec_lists:find(fun({_, Cert}) ->
  145. check_cert(CACerts1, Cert)
  146. end, Certs1) of
  147. {ok, Trusted} ->
  148. {trusted_ca, element(1, Trusted)};
  149. _ ->
  150. unknown_ca
  151. end.
  152. extract_public_key_info(Cert) ->
  153. ((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo).
  154. check_cert(CACerts, Cert) ->
  155. lists:any(fun(CACert) ->
  156. extract_public_key_info(CACert) == extract_public_key_info(Cert)
  157. end, CACerts).
  158. check_ssl_version() ->
  159. case application:get_key(ssl, vsn) of
  160. {ok, Vsn} ->
  161. parse_vsn(Vsn) >= {5, 3, 6};
  162. _ ->
  163. false
  164. end.
  165. get_ssl_config() ->
  166. GlobalConfigFile = rebar_dir:global_config(),
  167. Config = rebar_config:consult_file(GlobalConfigFile),
  168. case proplists:get_value(ssl_verify, Config, []) of
  169. false ->
  170. ssl_verify_disabled;
  171. _ ->
  172. ssl_verify_enabled
  173. end.
  174. parse_vsn(Vsn) ->
  175. version_pad(string:tokens(Vsn, ".-")).
  176. version_pad([Major]) ->
  177. {list_to_integer(Major), 0, 0};
  178. version_pad([Major, Minor]) ->
  179. {list_to_integer(Major), list_to_integer(Minor), 0};
  180. version_pad([Major, Minor, Patch]) ->
  181. {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)};
  182. version_pad([Major, Minor, Patch | _]) ->
  183. {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}.