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.

217 lines
8.2 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) =:= rebar_utils: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 ~ts 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 ~ts", [CachePath]),
  42. serve_from_download(TmpDir, CachePath, Pkg, NewETag, Body, State);
  43. error when ETag =/= false ->
  44. ?INFO("Download error, using cached file at ~ts", [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 ~ts", [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 ~ts ETag ~ts doesn't match returned ETag ~ts", [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. HttpOptions = [{ssl, ssl_opts(Url)}, {relaxed, true} | rebar_utils:get_proxy_auth()],
  96. case httpc:request(get, {Url, [{"if-none-match", ETag} || ETag =/= false]++[{"User-Agent", rebar_utils:user_agent()}]},
  97. HttpOptions,
  98. [{body_format, binary}],
  99. rebar) of
  100. {ok, {{_Version, 200, _Reason}, Headers, Body}} ->
  101. ?DEBUG("Successfully downloaded ~ts", [Url]),
  102. {"etag", ETag1} = lists:keyfind("etag", 1, Headers),
  103. {ok, Body, string:strip(ETag1, both, $")};
  104. {ok, {{_Version, 304, _Reason}, _Headers, _Body}} ->
  105. ?DEBUG("Cached copy of ~ts still valid", [Url]),
  106. {ok, cached};
  107. {ok, {{_Version, Code, _Reason}, _Headers, _Body}} ->
  108. ?DEBUG("Request to ~p failed: status code ~p", [Url, Code]),
  109. error;
  110. {error, Reason} ->
  111. ?DEBUG("Request to ~p failed: ~p", [Url, Reason]),
  112. error
  113. end.
  114. etag(Path) ->
  115. case file:read_file(Path) of
  116. {ok, Binary} ->
  117. <<X:128/big-unsigned-integer>> = crypto:hash(md5, Binary),
  118. string:to_lower(lists:flatten(io_lib:format("~32.16.0b", [X])));
  119. {error, _} ->
  120. false
  121. end.
  122. %% @doc returns the SSL options adequate for the project based on
  123. %% its configuration, including for validation of certs.
  124. -spec ssl_opts(string()) -> [term()].
  125. ssl_opts(Url) ->
  126. case get_ssl_config() of
  127. ssl_verify_enabled ->
  128. ssl_opts(ssl_verify_enabled, Url);
  129. ssl_verify_disabled ->
  130. [{verify, verify_none}]
  131. end.
  132. %% @doc returns the SSL options adequate for the project based on
  133. %% its configuration, including for validation of certs.
  134. -spec ssl_opts(atom(), string()) -> [term()].
  135. ssl_opts(ssl_verify_enabled, Url) ->
  136. case check_ssl_version() of
  137. true ->
  138. {ok, {_, _, Hostname, _, _, _}} = http_uri:parse(rebar_utils:to_list(Url)),
  139. VerifyFun = {fun ssl_verify_hostname:verify_fun/3, [{check_hostname, Hostname}]},
  140. CACerts = certifi:cacerts(),
  141. [{verify, verify_peer}, {depth, 2}, {cacerts, CACerts}
  142. ,{partial_chain, fun partial_chain/1}, {verify_fun, VerifyFun}];
  143. false ->
  144. ?WARN("Insecure HTTPS request (peer verification disabled), please update to OTP 17.4 or later", []),
  145. [{verify, verify_none}]
  146. end.
  147. partial_chain(Certs) ->
  148. Certs1 = [{Cert, public_key:pkix_decode_cert(Cert, otp)} || Cert <- Certs],
  149. CACerts = certifi:cacerts(),
  150. CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts],
  151. case ec_lists:find(fun({_, Cert}) ->
  152. check_cert(CACerts1, Cert)
  153. end, Certs1) of
  154. {ok, Trusted} ->
  155. {trusted_ca, element(1, Trusted)};
  156. _ ->
  157. unknown_ca
  158. end.
  159. extract_public_key_info(Cert) ->
  160. ((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo).
  161. check_cert(CACerts, Cert) ->
  162. lists:any(fun(CACert) ->
  163. extract_public_key_info(CACert) == extract_public_key_info(Cert)
  164. end, CACerts).
  165. check_ssl_version() ->
  166. case application:get_key(ssl, vsn) of
  167. {ok, Vsn} ->
  168. parse_vsn(Vsn) >= {5, 3, 6};
  169. _ ->
  170. false
  171. end.
  172. get_ssl_config() ->
  173. GlobalConfigFile = rebar_dir:global_config(),
  174. Config = rebar_config:consult_file(GlobalConfigFile),
  175. case proplists:get_value(ssl_verify, Config, []) of
  176. false ->
  177. ssl_verify_disabled;
  178. _ ->
  179. ssl_verify_enabled
  180. end.
  181. parse_vsn(Vsn) ->
  182. version_pad(string:tokens(Vsn, ".-")).
  183. version_pad([Major]) ->
  184. {list_to_integer(Major), 0, 0};
  185. version_pad([Major, Minor]) ->
  186. {list_to_integer(Major), list_to_integer(Minor), 0};
  187. version_pad([Major, Minor, Patch]) ->
  188. {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)};
  189. version_pad([Major, Minor, Patch | _]) ->
  190. {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}.