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 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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)}.