From b3402f10d70dfbe207422be7151cfdbc702ae3e6 Mon Sep 17 00:00:00 2001 From: Danylo Negriienko Date: Sat, 29 Jun 2019 14:48:42 +0300 Subject: [PATCH] =?UTF-8?q?Working=20only=20through=20API=20(IAM=20tokens?= =?UTF-8?q?=20disabled),=20translate=20(with=20glossary),=20detect=20and?= =?UTF-8?q?=20list=20=E2=80=94=C2=A0working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/yandex_translate.ex | 39 ------- lib/yandex_translate/auth/api_key.ex | 7 ++ lib/yandex_translate/auth/auth.ex | 15 +++ lib/yandex_translate/auth/iam_token.ex | 37 ++++++ lib/yandex_translate/auth/jwt_token_config.ex | 15 +++ lib/yandex_translate/client.ex | 106 ++++++++++++++++++ lib/yandex_translate/token.ex | 15 --- mix.exs | 6 +- mix.lock | 4 +- 9 files changed, 185 insertions(+), 59 deletions(-) create mode 100644 lib/yandex_translate/auth/api_key.ex create mode 100644 lib/yandex_translate/auth/auth.ex create mode 100644 lib/yandex_translate/auth/iam_token.ex create mode 100644 lib/yandex_translate/auth/jwt_token_config.ex create mode 100644 lib/yandex_translate/client.ex delete mode 100644 lib/yandex_translate/token.ex diff --git a/lib/yandex_translate.ex b/lib/yandex_translate.ex index 1d2410b..606aada 100755 --- a/lib/yandex_translate.ex +++ b/lib/yandex_translate.ex @@ -1,41 +1,2 @@ defmodule YandexTranslate do - @iam_token_url "https://iam.api.cloud.yandex.net/iam/v1/tokens" - - def get_key(type, key \\ Application.get_env(:yandex_translate, :private_key)) - - def get_key(:public, key), - do: get_key(:private, key) |> JOSE.JWK.to_public() - - def get_key(:private, key), - do: fetch_key(key) - - defp fetch_key("-----BEGIN" <> _ = key_content), do: JOSE.JWK.from_pem(key_content) - - defp fetch_key(key_file), do: JOSE.JWK.from_pem_file(key_file) - - def get_iam_token(), - do: Application.get_all_env(:yandex_translate) |> Map.new() |> get_iam_token() - - def get_iam_token(%{service_account_id: iss, private_key: private_key, authorized_key_id: kid}) do - now = DateTime.utc_now() |> DateTime.to_unix() - jwk = get_key(:private, private_key) |> JOSE.JWK.merge(%{"kid" => kid}) - - # JSON Web Signature (JWS) - jws = %{ - "alg" => "RS256", - "typ" => "JWT", - "kid" => kid - } - - # JSON Web Token (JWT) - jwt = - JOSE.JWT.from(%{ - "iss" => iss, - "exp" => now + 60 * 60, - "iat" => now, - "aud" => @iam_token_url - }) - - _signed = JOSE.JWT.sign(jwk, jws, jwt) |> JOSE.JWS.compact() |> elem(1) - end end diff --git a/lib/yandex_translate/auth/api_key.ex b/lib/yandex_translate/auth/api_key.ex new file mode 100644 index 0000000..78baf5f --- /dev/null +++ b/lib/yandex_translate/auth/api_key.ex @@ -0,0 +1,7 @@ +defmodule YandexTranslate.Auth.ApiKey do + def get_auth_value() do + "Api-Key " <> get_api_key() + end + + defp get_api_key(), do: Application.get_env(:yandex_translate, :api_key) +end diff --git a/lib/yandex_translate/auth/auth.ex b/lib/yandex_translate/auth/auth.ex new file mode 100644 index 0000000..b727f89 --- /dev/null +++ b/lib/yandex_translate/auth/auth.ex @@ -0,0 +1,15 @@ +defmodule YandexTranslate.Auth do + alias YandexTranslate.Auth.{ApiKey, IAMToken} + + def get_config() do + Application.get_all_env(:yandex_translate) |> Map.new() + end + + def authorization_value(config \\ get_config()) + + def authorization_value(%{private_key: _, authorized_key_id: _, service_account_id: _}), + do: IAMToken.get_auth_value() + + def authorization_value(%{api_key: _}), + do: ApiKey.get_auth_value() +end diff --git a/lib/yandex_translate/auth/iam_token.ex b/lib/yandex_translate/auth/iam_token.ex new file mode 100644 index 0000000..4dfb99d --- /dev/null +++ b/lib/yandex_translate/auth/iam_token.ex @@ -0,0 +1,37 @@ +defmodule YandexTranslate.Auth.IAMToken do + alias YandexTranslate.Auth.JWTTokenConfig + + def get_auth_value() do + "Bearer " <> get_iam_token() + end + + def get_iam_token() do + # Rewrite needed, signing process not worked. Fucking PS256 and hash length optoins on my opinion + # Pluged by old IAMToken + "CggVAgAAABoBMRKABDnO1uw1a7U-AIlRglhz8yQVQzXaKjXt1saXk5VDXHH41mlPpMpK3V9-GUgthtdposNYva9hj0DaaHE1YDZfBcZkiZU7T_45ilhFtouG5rARRP8E6GqR4-dMutrYo69XQ3Uj4-KPa0IkOa2HrCTJEePhDHGlz-iW_YnN0QXFIlKN7NMIrZQyYRJ1dvQExx29TARcy_OVWOO8234rU_Vi5BRBKXTfp7NFB589bMe6UhEoYTF-1PdOEtxDJ2DLMdqMiYw4BZnyhOwn81u8kAetHpC-R6kizR8P2omcMyZORk4zwMfgdWlDzAhrIGq4ev8aOpcQH2vH8xh8ZMTgqbRZ9rezOflEZ3bDdDVdcQWjMq4ZYABY1n278pzhucbiTZJl6uxxTXg9Ucbg7_9mpA8DOIBn72wx8dTmrJgXWTstSJ6aShpg_1VJE48gUSenO1AjXWnat9hcvhgrBGKdQFlmhqnrRTptnvhgdNctwvD_tjpjC9ZWa5pBPyE9pKdiZzYUkhSEKEpRKFAh7oGv4i0w5gCHDzQrQfrrMFxFUUPJCg8Hx0tUbLqMgF9sYXYS4vhJFWFtBl_x0dd3KMPy33Fg4oDxlbZwsolyrVI9QnT9Es2qXGp1Sw9TLMM3pAI8G8JOD7O8eTRCJalRrrFOgybbK8LjK7AKLBhI0MNtQs0A_QS-GnYKIGIxODQ5YTljMmQ4ZjRiNjQ5YTU1MzhhN2E1OTdkNDU3ELTgzugFGPSx0egFIj4KFGFqZTB1czQyMWdoNG9idmJwcW5iEg5leGNoYW5nZXJzNGRldioUYjFnbmw5N3VkNzltYXA3bmhnM2wwAjAFOAFQAVoA" + end + + def get_jwt_token() do + signer = Joken.Signer.create("PS256", %{"pem" => get_rsa_key(:private)}, get_headers()) + JWTTokenConfig.generate_and_sign(%{}, signer) |> elem(1) + end + + defp get_headers(kid \\ Application.get_env(:yandex_translate, :authorized_key_id)), + do: %{"kid" => kid} + + defp get_rsa_key(type, key \\ Application.get_env(:yandex_translate, :private_key)) + + defp get_rsa_key(:private, key), do: fetch_key(key) + + defp get_rsa_key(:public, key) do + get_rsa_key(:private, key) + |> JOSE.JWK.from_pem() + |> JOSE.JWK.to_public() + |> JOSE.JWK.to_pem() + |> elem(1) + end + + defp fetch_key("-----BEGIN" <> _ = key_content), do: key_content + + defp fetch_key(key_file), do: File.read!(key_file) |> fetch_key() +end diff --git a/lib/yandex_translate/auth/jwt_token_config.ex b/lib/yandex_translate/auth/jwt_token_config.ex new file mode 100644 index 0000000..7504518 --- /dev/null +++ b/lib/yandex_translate/auth/jwt_token_config.ex @@ -0,0 +1,15 @@ +defmodule YandexTranslate.Auth.JWTTokenConfig do + @iam_token_url "https://iam.api.cloud.yandex.net/iam/v1/tokens" + + use Joken.Config + + @impl Joken.Config + def token_config() do + default_claims( + iss: Application.get_env(:yandex_translate, :service_account_id), + default_exp: 60 * 60, + skip: [:jti, :nbf], + aud: Application.get_env(:yandex_translate, :iam_token_url) || @iam_token_url + ) + end +end diff --git a/lib/yandex_translate/client.ex b/lib/yandex_translate/client.ex new file mode 100644 index 0000000..25e282e --- /dev/null +++ b/lib/yandex_translate/client.ex @@ -0,0 +1,106 @@ +defmodule YandexTranslate.Client do + @base_host "translate.api.cloud.yandex.net" + @base_path "/translate/v2/" + @api_methods %{ + listLanguages: "languages", + detectLanguage: "detect", + translate: "translate" + } + @availaible_api_methods Map.keys(@api_methods) + @default_headers [{"content-type", "application/json"}] + + alias YandexTranslate.Auth + + defp generate_headers(headers \\ []) + + defp generate_headers(header) when is_tuple(header), + do: generate_headers([header]) + + defp generate_headers(headers) when is_list(headers), + do: [{"authorization", Auth.authorization_value()} | @default_headers ++ headers] + + defp generate_body(method, params) do + params_map = transform_body(method, params) + filter_params(method, params_map) |> Jason.encode!() + end + + defp transform_body(:translate, params) do + params + |> transform_glossary() + |> transform_text() + end + + # defp transform_body(:detectLanguage, params), do: params + + defp transform_body(_, params), do: params + + defp transform_glossary(%{glossary: glossary} = params) do + Map.put(params, :glossaryConfig, %{ + glossaryData: %{ + glossaryPairs: generate_glossary_pairs(glossary) + } + }) + end + + defp transform_glossary(result), do: result + + defp generate_glossary_pairs(glossary) when is_list(glossary), + do: glossary |> Enum.map(fn {from, to} -> %{sourceText: from, translatedText: to} end) + + defp transform_text(%{texts: text} = params) when is_binary(text), + do: %{params | texts: [text]} + + defp transform_text(%{text: text} = params) when is_binary(text), + do: Map.put(params, :texts, [text]) + + defp filter_params(method, params), do: params |> Map.take(valid_argument_keys(method)) + + defp valid_argument_keys(:detectLanguage), do: [:text, :languageCodeHints] + + defp valid_argument_keys(:translate), + do: [:texts, :sourceLanguageCode, :targetLanguageCode, :glossaryConfig] + + defp valid_argument_keys(_), do: [] + + def call(method \\ :listLanguages, args \\ %{}) + when method in @availaible_api_methods and is_map(args) do + body = generate_body(method, args) + headers = generate_headers() + + method + |> fetch(headers, body) + |> parse() + end + + defp parse(body), do: Jason.decode!(body) + + defp fetch(method, headers, body) do + {:ok, conn} = Mint.HTTP.connect(:https, @base_host, 443) + + {:ok, conn, request_ref} = + Mint.HTTP.request( + conn, + "POST", + @base_path <> @api_methods[method], + headers, + body + ) + + receive do + message -> + {:ok, conn, responses} = Mint.HTTP.stream(conn, message) + + body = + responses + |> Enum.filter(fn + {:data, ^request_ref, _} -> true + _ -> false + end) + |> Enum.map(fn {_, _, data} -> data end) + + Mint.HTTP.close(conn) + + body + end + end +end diff --git a/lib/yandex_translate/token.ex b/lib/yandex_translate/token.ex deleted file mode 100644 index 01b9b5c..0000000 --- a/lib/yandex_translate/token.ex +++ /dev/null @@ -1,15 +0,0 @@ -# defmodule YandexTranslate.Token do -# @iam_token_url "https://iam.api.cloud.yandex.net/iam/v1/tokens" - -# use Joken.Config - -# @impl Joken.Config -# def token_config() do -# default_claims( -# iss: Application.get_env(:yandex_translate, :service_account_id), -# default_exp: 60 * 60, -# skip: [:jti, :nbf], -# aud: @iam_token_url -# ) -# end -# end diff --git a/mix.exs b/mix.exs index ec90484..3154f27 100755 --- a/mix.exs +++ b/mix.exs @@ -28,11 +28,11 @@ defmodule YandexTranslate.Mixfile do defp deps do [ - # {:joken, "~> 2.1.0"}, + {:joken, "~> 2.1.0"}, # {:jose, "~> 1.9.0"}, - {:jose, github: "potatosalad/erlang-jose", branch: "master"}, {:jason, "~> 1.1.2"}, - {:mojito, "~> 0.3.0"}, + {:mint, "~> 0.3.0"}, + {:castore, "~> 0.1.2"}, {:ex_doc, "~> 0.20.2", only: :dev}, {:remix, "~> 0.0.2", only: :dev} ] diff --git a/mix.lock b/mix.lock index 55d7f26..064477a 100644 --- a/mix.lock +++ b/mix.lock @@ -9,12 +9,12 @@ "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "joken": {:hex, :joken, "2.1.0", "bf21a73105d82649f617c5e59a7f8919aa47013d2519ebcc39d998d8d12adda9", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, - "jose": {:git, "https://github.com/potatosalad/erlang-jose.git", "80551fc36abab81b6fac798e112a9c1a644b60f1", [branch: "master"]}, + "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, - "mint": {:hex, :mint, "0.2.1", "a2ec8729fcad5c8b6460e07dfa64b008b3d9697a9f4604cd5684a87b44677c99", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm"}, + "mint": {:hex, :mint, "0.3.0", "f00f09363e5cdba1572519914aab3cea1a73b4737c63a29afe74a08362f5f91c", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm"}, "mojito": {:hex, :mojito, "0.3.0", "806cd3c1832333a9ee784e7ea2799863fbe4de55ecb4623a8f4ef870c2844cc6", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 0.2.1", [hex: :mint, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "ojson": {:hex, :ojson, "1.0.0", "fd28614eadaec00a15cdb2f53f29d8717a812a508ddb80d202f2f2e2aaeabbcc", [:mix, :rebar3], [], "hexpm"},