1defmodule Mix.Tasks.Hex do 2 use Mix.Task 3 4 @apikey_tag "HEXAPIKEY" 5 6 @shortdoc "Prints Hex help information" 7 8 @moduledoc """ 9 Prints Hex tasks and their information. 10 11 $ mix hex 12 13 See `mix help hex.config` to see all available configuration options. 14 """ 15 16 @impl true 17 def run(_args) do 18 Hex.start() 19 20 Hex.Shell.info("Hex v" <> Hex.version()) 21 Hex.Shell.info("Hex is a package manager for the Erlang ecosystem.") 22 23 print_available_tasks() 24 25 Hex.Shell.info("Further information can be found here: https://hex.pm/docs") 26 end 27 28 defp print_available_tasks() do 29 line_break() 30 Hex.Shell.info("Available tasks:") 31 line_break() 32 33 pattern = "hex." 34 modules = Enum.filter(load_tasks(), &String.contains?(Mix.Task.task_name(&1), pattern)) 35 {docs, max} = build_task_doc_list(modules) 36 display_doc_list(docs, max) 37 line_break() 38 end 39 40 defp load_tasks() do 41 Enum.filter(Mix.Task.load_all(), &(Mix.Task.moduledoc(&1) != false)) 42 end 43 44 defp build_task_doc_list(modules) do 45 Enum.reduce(modules, {[], 0}, fn module, {docs, max} -> 46 task = "mix " <> Mix.Task.task_name(module) 47 48 task_list = 49 if Keyword.has_key?(module.__info__(:functions), :tasks) do 50 Enum.map(module.tasks(), fn {subtask, docs} -> {"#{task} #{subtask}", docs} end) 51 else 52 [] 53 end 54 55 max = 56 Enum.reduce(task_list, max, fn {task, _}, max_now -> 57 max(byte_size(task), max_now) 58 end) 59 60 if Enum.empty?(task_list) do 61 {docs, max} 62 else 63 {docs ++ [task_list], max} 64 end 65 end) 66 end 67 68 defp display_doc_list(docs, max) do 69 Enum.each(Enum.sort_by(docs, &List.first/1), fn tasks -> 70 Enum.each(tasks, fn {task, doc} -> 71 Mix.shell().info(format_task(task, max, doc)) 72 end) 73 74 line_break() 75 end) 76 end 77 78 defp format_task(task, max, doc) do 79 Hex.Stdlib.string_pad_trailing(task, max) <> " # " <> doc 80 end 81 82 defp line_break(), do: Hex.Shell.info("") 83 84 @doc false 85 def print_table(header, values) do 86 header = Enum.map(header, &[:underline, &1]) 87 widths = widths([header | values]) 88 89 print_row(header, widths) 90 Enum.each(values, &print_row(&1, widths)) 91 end 92 93 defp ansi_length(binary) when is_binary(binary) do 94 byte_size(binary) 95 end 96 97 defp ansi_length(list) when is_list(list) do 98 Enum.reduce(list, 0, &(ansi_length(&1) + &2)) 99 end 100 101 defp ansi_length(atom) when is_atom(atom) do 102 0 103 end 104 105 defp print_row(strings, widths) do 106 Enum.zip(strings, widths) 107 |> Enum.map(fn {string, width} -> 108 pad_size = width - ansi_length(string) + 2 109 pad = :lists.duplicate(pad_size, ?\s) 110 [string || "", :reset, pad] 111 end) 112 |> IO.ANSI.format() 113 |> Hex.Shell.info() 114 end 115 116 defp widths([head | tail]) do 117 widths = Enum.map(head, &ansi_length/1) 118 119 Enum.reduce(tail, widths, fn list, acc -> 120 Enum.zip(list, acc) 121 |> Enum.map(fn {string, width} -> max(width, ansi_length(string)) end) 122 end) 123 end 124 125 @doc false 126 def auth(opts \\ []) do 127 username = Hex.Shell.prompt("Username:") |> Hex.Stdlib.string_trim() 128 account_password = Mix.Tasks.Hex.password_get("Account password:") |> Hex.Stdlib.string_trim() 129 Mix.Tasks.Hex.generate_all_user_keys(username, account_password, opts) 130 end 131 132 @local_password_prompt "You have authenticated on Hex using your account password. However, " <> 133 "Hex requires you to have a local password that applies only to this machine for security " <> 134 "purposes. Please enter it." 135 136 @doc false 137 def generate_user_key(key_name, permissions, opts) do 138 case Hex.API.Key.new(key_name, permissions, opts) do 139 {:ok, {201, body, _}} -> 140 {:ok, body["secret"]} 141 142 other -> 143 Mix.shell().error("Generation of key failed") 144 Hex.Utils.print_error_result(other) 145 :error 146 end 147 end 148 149 @doc false 150 def generate_all_user_keys(username, password, opts \\ []) do 151 Hex.Shell.info("Generating keys...") 152 auth = [user: username, pass: password] 153 key_name = api_key_name(opts[:key_name]) 154 permissions = [%{"domain" => "api"}] 155 156 case generate_user_key(key_name, permissions, auth) do 157 {:ok, write_key} -> 158 key_name = api_key_name(opts[:key_name], "read") 159 permissions = [%{"domain" => "api", "resource" => "read"}] 160 161 case generate_user_key(key_name, permissions, key: write_key) do 162 {:ok, read_key} -> 163 key_name = repositories_key_name(opts[:key_name]) 164 permissions = [%{"domain" => "repositories"}] 165 166 case generate_user_key(key_name, permissions, key: write_key) do 167 {:ok, organization_key} -> 168 auth_organization("hexpm", organization_key) 169 170 Hex.Shell.info(@local_password_prompt) 171 prompt_encrypt_key(write_key, read_key) 172 {:ok, write_key, read_key, organization_key} 173 174 :error -> 175 :ok 176 end 177 178 :error -> 179 :error 180 end 181 182 :error -> 183 :error 184 end 185 end 186 187 @doc false 188 def generate_organization_key(organization_name, key_name, permissions, auth \\ nil) do 189 auth = auth || auth_info(:write) 190 191 case Hex.API.Key.Organization.new(organization_name, key_name, permissions, auth) do 192 {:ok, {201, body, _}} -> 193 {:ok, body["secret"]} 194 195 other -> 196 Mix.shell().error("Generation of key failed") 197 Hex.Utils.print_error_result(other) 198 :error 199 end 200 end 201 202 @doc false 203 def general_key_name(nil) do 204 {:ok, hostname} = :inet.gethostname() 205 List.to_string(hostname) 206 end 207 208 def general_key_name(key) do 209 key 210 end 211 212 @doc false 213 def api_key_name(key, extra \\ nil) do 214 {:ok, hostname} = :inet.gethostname() 215 name = "#{key || hostname}-api" 216 if extra, do: "#{name}-#{extra}", else: name 217 end 218 219 @doc false 220 def repository_key_name(organization, key) do 221 {:ok, hostname} = :inet.gethostname() 222 "#{key || hostname}-repository-#{organization}" 223 end 224 225 @doc false 226 def repositories_key_name(key) do 227 {:ok, hostname} = :inet.gethostname() 228 "#{key || hostname}-repositories" 229 end 230 231 @doc false 232 def update_keys(write_key, read_key \\ nil) do 233 Hex.Config.update( 234 "$write_key": write_key, 235 "$read_key": read_key, 236 "$encrypted_key": nil, 237 encrypted_key: nil 238 ) 239 240 Hex.State.put(:api_key_write, write_key) 241 Hex.State.put(:api_key_read, read_key) 242 end 243 244 @doc false 245 def auth_organization(name, key) do 246 repo = Hex.Repo.get_repo(name) || Hex.Repo.default_hexpm_repo() 247 repo = Map.put(repo, :auth_key, key) 248 249 Hex.State.fetch!(:repos) 250 |> Map.put(name, repo) 251 |> Hex.Config.update_repos() 252 end 253 254 @doc false 255 def auth_info(permission, opts \\ []) 256 257 def auth_info(:write, opts) do 258 api_key_write_unencrypted = Hex.State.fetch!(:api_key_write_unencrypted) 259 api_key_write = Hex.State.fetch!(:api_key_write) 260 261 cond do 262 api_key_write_unencrypted -> [key: api_key_write_unencrypted] 263 api_key_write -> [key: prompt_decrypt_key(api_key_write)] 264 Keyword.get(opts, :auth_inline, true) -> authenticate_inline() 265 true -> [] 266 end 267 end 268 269 def auth_info(:read, opts) do 270 api_key_write_unencrypted = Hex.State.fetch!(:api_key_write_unencrypted) 271 api_key_read = Hex.State.fetch!(:api_key_read) 272 273 cond do 274 api_key_write_unencrypted -> [key: api_key_write_unencrypted] 275 api_key_read -> [key: api_key_read] 276 Keyword.get(opts, :auth_inline, true) -> authenticate_inline() 277 true -> [] 278 end 279 end 280 281 defp authenticate_inline() do 282 authenticate? = 283 Hex.Shell.yes?("No authenticated user found. Do you want to authenticate now?") 284 285 if authenticate? do 286 case auth() do 287 {:ok, write_key, _read_key, _org_key} -> [key: write_key] 288 :error -> no_auth_error() 289 end 290 else 291 no_auth_error() 292 end 293 end 294 295 defp no_auth_error() do 296 Mix.raise("No authenticated user found. Run `mix hex.user auth`") 297 end 298 299 @doc false 300 def prompt_encrypt_key(write_key, read_key, challenge \\ "Local password") do 301 password = password_get("#{challenge}:") |> Hex.Stdlib.string_trim() 302 confirm = password_get("#{challenge} (confirm):") |> Hex.Stdlib.string_trim() 303 304 if password != confirm do 305 Hex.Shell.error("Entered passwords do not match. Try again") 306 prompt_encrypt_key(write_key, read_key, challenge) 307 else 308 encrypted_write_key = Hex.Crypto.encrypt(write_key, password, @apikey_tag) 309 update_keys(encrypted_write_key, read_key) 310 end 311 end 312 313 @doc false 314 def prompt_decrypt_key(encrypted_key, challenge \\ "Local password") do 315 password = password_get("#{challenge}:") |> Hex.Stdlib.string_trim() 316 317 case Hex.Crypto.decrypt(encrypted_key, password, @apikey_tag) do 318 {:ok, key} -> 319 key 320 321 :error -> 322 Hex.Shell.error("Wrong password. Try again") 323 prompt_decrypt_key(encrypted_key, challenge) 324 end 325 end 326 327 @doc false 328 def encrypt_key(password, key) do 329 Hex.Crypto.encrypt(key, password, @apikey_tag) 330 end 331 332 @doc false 333 def decrypt_key(password, key) do 334 Hex.Crypto.decrypt(key, password, @apikey_tag) 335 end 336 337 @doc false 338 def required_opts(opts, required) do 339 Enum.map(required, fn req -> 340 unless Keyword.has_key?(opts, req) do 341 Mix.raise("Missing command line option: #{req}") 342 end 343 end) 344 end 345 346 @doc false 347 def convert_permissions([]) do 348 nil 349 end 350 351 @doc false 352 def convert_permissions(permissions) do 353 Enum.map(permissions, fn permission -> 354 permission = String.downcase(permission) 355 destructure [domain, resource], String.split(permission, ":", parts: 2) 356 %{"domain" => domain, "resource" => resource} 357 end) 358 end 359 360 # Password prompt that hides input by every 1ms 361 # clearing the line with stderr 362 @doc false 363 def password_get(prompt) do 364 if Hex.State.fetch!(:clean_pass) do 365 password_clean(prompt) 366 else 367 Hex.Shell.prompt(prompt <> " ") 368 end 369 end 370 371 defp password_clean(prompt) do 372 pid = spawn_link(fn -> loop(prompt) end) 373 ref = make_ref() 374 value = IO.gets(prompt <> " ") 375 376 send(pid, {:done, self(), ref}) 377 receive do: ({:done, ^pid, ^ref} -> :ok) 378 379 value 380 end 381 382 defp loop(prompt) do 383 receive do 384 {:done, parent, ref} -> 385 send(parent, {:done, self(), ref}) 386 IO.write(:standard_error, "\e[2K\r") 387 after 388 1 -> 389 IO.write(:standard_error, "\e[2K\r#{prompt} ") 390 loop(prompt) 391 end 392 end 393 394 @progress_steps 25 395 396 @doc false 397 def progress(nil) do 398 fn _ -> nil end 399 end 400 401 def progress(max) do 402 put_progress(0, 0) 403 404 fn size -> 405 fraction = size / max 406 completed = trunc(fraction * @progress_steps) 407 put_progress(completed, trunc(fraction * 100)) 408 size 409 end 410 end 411 412 defp put_progress(completed, percent) do 413 unfilled = @progress_steps - completed 414 str = "\r[#{String.duplicate("#", completed)}#{String.duplicate(" ", unfilled)}]" 415 IO.write(:stderr, str <> " #{percent}%") 416 end 417 418 if Mix.env() == :test do 419 @doc false 420 def set_exit_code(code), do: throw({:exit_code, code}) 421 else 422 @doc false 423 def set_exit_code(code), do: System.at_exit(fn _ -> System.halt(code) end) 424 end 425end 426