Current section
29 Versions
Jump to
Current section
29 Versions
Compare versions
9
files changed
+165
additions
-152
deletions
| @@ -3,17 +3,17 @@ | |
| 3 3 | {<<"description">>,<<"Wechat API wrapper in Elixir.">>}. |
| 4 4 | {<<"elixir">>,<<"~> 1.5">>}. |
| 5 5 | {<<"files">>, |
| 6 | - [<<"lib/plug/crypto/wechat_message_encryptor.ex">>, |
| 7 | - <<"lib/plug/crypto/wechat_signature_verifier.ex">>, |
| 8 | - <<"lib/plug/parsers/WECHAT.ex">>,<<"lib/wechat.ex">>, |
| 9 | - <<"lib/wechat/api.ex">>,<<"lib/wechat/application.ex">>, |
| 6 | + [<<"lib/wechat.ex">>,<<"lib/wechat/api.ex">>,<<"lib/wechat/application.ex">>, |
| 10 7 | <<"lib/wechat/config.ex">>,<<"lib/wechat/datacube.ex">>, |
| 11 8 | <<"lib/wechat/http.ex">>,<<"lib/wechat/media.ex">>,<<"lib/wechat/menu.ex">>, |
| 12 9 | <<"lib/wechat/merchant.ex">>,<<"lib/wechat/message/custom.ex">>, |
| 13 10 | <<"lib/wechat/message/template.ex">>,<<"lib/wechat/mp.ex">>, |
| 14 | - <<"lib/wechat/poi.ex">>,<<"lib/wechat/qrcode.ex">>,<<"lib/wechat/sns.ex">>, |
| 15 | - <<"lib/wechat/tags.ex">>,<<"lib/wechat/tags/members.ex">>, |
| 16 | - <<"lib/wechat/ticket.ex">>,<<"lib/wechat/user.ex">>, |
| 11 | + <<"lib/wechat/plugs/parse_message.ex">>, |
| 12 | + <<"lib/wechat/plugs/validate_request.ex">>,<<"lib/wechat/poi.ex">>, |
| 13 | + <<"lib/wechat/qrcode.ex">>,<<"lib/wechat/sns.ex">>,<<"lib/wechat/tags.ex">>, |
| 14 | + <<"lib/wechat/tags/members.ex">>,<<"lib/wechat/ticket.ex">>, |
| 15 | + <<"lib/wechat/user.ex">>,<<"lib/wechat/utils/message_encryptor.ex">>, |
| 16 | + <<"lib/wechat/utils/signature_verifier.ex">>, |
| 17 17 | <<"lib/wechat/workers/access_token.ex">>, |
| 18 18 | <<"lib/wechat/workers/jsapi_ticket.ex">>,<<"mix.exs">>,<<"README.md">>, |
| 19 19 | <<"LICENSE.txt">>,<<"CHANGELOG.md">>]}. |
| @@ -42,4 +42,4 @@ | |
| 42 42 | {<<"optional">>,false}, |
| 43 43 | {<<"repository">>,<<"hexpm">>}, |
| 44 44 | {<<"requirement">>,<<"~> 1.0">>}]]}. |
| 45 | - {<<"version">>,<<"0.3.3">>}. |
| 45 | + {<<"version">>,<<"0.3.4">>}. |
| @@ -1,36 +0,0 @@ | |
| 1 | - defmodule Plug.Crypto.WechatMessageEncryptor do |
| 2 | - @moduledoc """ |
| 3 | - Encrypt and decrypt wechat messages. |
| 4 | - """ |
| 5 | - |
| 6 | - def decrypt(encrypted, encoding_aes_key) do |
| 7 | - plain_text = |
| 8 | - encrypted |
| 9 | - |> Base.decode64! |
| 10 | - |> decrypt_aes(encoding_aes_key) |
| 11 | - |> decode_padding |
| 12 | - |
| 13 | - # random(16B) + msg_len(4B) + msg + appid |
| 14 | - <<_ :: binary-size(16), |
| 15 | - msg_len :: integer-size(32), |
| 16 | - msg :: binary-size(msg_len), |
| 17 | - appid :: binary>> = plain_text |
| 18 | - |
| 19 | - {appid, msg} |
| 20 | - end |
| 21 | - |
| 22 | - defp decrypt_aes(encrypted, encoding_aes_key) do |
| 23 | - aes_key = Base.decode64!(encoding_aes_key <> "=") |
| 24 | - iv = binary_part(aes_key, 0, 16) |
| 25 | - :crypto.block_decrypt(:aes_cbc, aes_key, iv, encrypted) |
| 26 | - end |
| 27 | - |
| 28 | - defp decode_padding(padded_text) do |
| 29 | - len = byte_size(padded_text) |
| 30 | - <<pad :: utf8>> = binary_part(padded_text, len, -1) |
| 31 | - case pad < 1 or pad > 32 do |
| 32 | - true -> binary_part(padded_text, 0, len) |
| 33 | - false -> binary_part(padded_text, 0, len - pad) |
| 34 | - end |
| 35 | - end |
| 36 | - end |
| @@ -1,18 +0,0 @@ | |
| 1 | - defmodule Plug.Crypto.WechatSignatureVerifier do |
| 2 | - @moduledoc """ |
| 3 | - Verify wechat signature. |
| 4 | - """ |
| 5 | - |
| 6 | - def verify(args, signature) do |
| 7 | - challenge = args |> Enum.sort |> Enum.join |> sha1 |
| 8 | - case Plug.Crypto.secure_compare(challenge, signature) do |
| 9 | - true -> :ok |
| 10 | - false -> :error |
| 11 | - end |
| 12 | - end |
| 13 | - |
| 14 | - defp sha1(str) do |
| 15 | - digest = :crypto.hash(:sha, str) |
| 16 | - Base.encode16(digest, case: :lower) |
| 17 | - end |
| 18 | - end |
| @@ -1,89 +0,0 @@ | |
| 1 | - defmodule Plug.Parsers.WECHAT do |
| 2 | - @moduledoc """ |
| 3 | - Parses wechat pushed messages. |
| 4 | - """ |
| 5 | - |
| 6 | - @behaviour Plug.Parsers |
| 7 | - |
| 8 | - defmodule ParseError do |
| 9 | - defexception [:message] |
| 10 | - end |
| 11 | - |
| 12 | - import Plug.Conn |
| 13 | - import SweetXml |
| 14 | - |
| 15 | - alias Plug.Crypto.WechatMessageEncryptor, as: MessageEncryptor |
| 16 | - alias Plug.Crypto.WechatSignatureVerifier, as: SignatureVerifier |
| 17 | - |
| 18 | - def parse(conn, "text", "xml", _headers, opts) do |
| 19 | - config = Keyword.get(opts, :wechat_config) || |
| 20 | - raise ArgumentError, "WECHAT parser expects a :wechat_config option" |
| 21 | - conn |
| 22 | - |> read_body(opts) |
| 23 | - |> decode(config) |
| 24 | - end |
| 25 | - |
| 26 | - def parse(conn, _type, _subtype, _headers, _opts) do |
| 27 | - {:next, conn} |
| 28 | - end |
| 29 | - |
| 30 | - defp decode({:more, _, conn}, _config) do |
| 31 | - {:error, :too_large, conn} |
| 32 | - end |
| 33 | - |
| 34 | - defp decode({:error, :timeout}, _config) do |
| 35 | - raise Plug.TimeoutError |
| 36 | - end |
| 37 | - |
| 38 | - defp decode({:error, _}, _config) do |
| 39 | - raise Plug.BadRequestError |
| 40 | - end |
| 41 | - |
| 42 | - defp decode({:ok, body, conn}, config) do |
| 43 | - msg = extract_xml(body) |
| 44 | - case msg_encrypted?(conn.params) do |
| 45 | - true -> |
| 46 | - %{"Encrypt" => encrypted} = msg |
| 47 | - case verify_msg_signature(config.token, encrypted, conn.params) do |
| 48 | - {:ok, encrypted} -> {:ok, decrypt(encrypted, config), conn} |
| 49 | - :error -> raise ParseError, "invalid msg_signature" |
| 50 | - end |
| 51 | - false -> |
| 52 | - {:ok, msg, conn} |
| 53 | - end |
| 54 | - rescue |
| 55 | - e -> raise Plug.Parsers.ParseError, exception: e |
| 56 | - end |
| 57 | - |
| 58 | - defp verify_msg_signature(token, encrypted, |
| 59 | - %{"timestamp" => timestamp, "nonce" => nonce, |
| 60 | - "msg_signature" => signature}) do |
| 61 | - args = [token, timestamp, nonce, encrypted] |
| 62 | - case SignatureVerifier.verify(args, signature) do |
| 63 | - :ok -> |
| 64 | - {:ok, encrypted} |
| 65 | - :error -> |
| 66 | - :error |
| 67 | - end |
| 68 | - end |
| 69 | - |
| 70 | - defp decrypt(encrypted, config) do |
| 71 | - appid = config.appid |
| 72 | - encoding_aes_key = config.encoding_aes_key |
| 73 | - case MessageEncryptor.decrypt(encrypted, encoding_aes_key) do |
| 74 | - {^appid, msg_decrypted} -> |
| 75 | - extract_xml(msg_decrypted) |
| 76 | - _ -> |
| 77 | - raise ParseError, "invalid appid" |
| 78 | - end |
| 79 | - end |
| 80 | - |
| 81 | - defp msg_encrypted?(%{"encrypt_type" => "aes"}), do: true |
| 82 | - defp msg_encrypted?(_), do: false |
| 83 | - |
| 84 | - def extract_xml(doc) do |
| 85 | - doc |
| 86 | - |> xpath(~x"//xml/*"l) |
| 87 | - |> Enum.into(%{}, &({xpath(&1, ~x"name(.)"s), xpath(&1, ~x"text()"s)})) |
| 88 | - end |
| 89 | - end |
| @@ -0,0 +1,80 @@ | |
| 1 | + defmodule Wechat.Plugs.ParseMessage do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + defmodule ParseError do |
| 5 | + defexception [:message] |
| 6 | + end |
| 7 | + |
| 8 | + import Plug.Conn |
| 9 | + import SweetXml |
| 10 | + |
| 11 | + import Wechat.Config, only: [token: 0, appid: 0, encoding_aes_key: 0] |
| 12 | + alias Wechat.Utils.MessageEncryptor |
| 13 | + alias Wechat.Utils.SignatureVerifier |
| 14 | + |
| 15 | + def init(opts) do |
| 16 | + opts |
| 17 | + end |
| 18 | + |
| 19 | + def call(%{method: "POST"} = conn, opts) do |
| 20 | + conn |
| 21 | + |> read_body(opts) |
| 22 | + |> decode |
| 23 | + end |
| 24 | + def call(conn, _opts) do |
| 25 | + conn |
| 26 | + end |
| 27 | + |
| 28 | + defp decode({:ok, body, conn}) do |
| 29 | + msg = extract_xml(body) |
| 30 | + case msg_encrypted?(conn.params) do |
| 31 | + true -> |
| 32 | + case verify_msg_signature(msg, conn.query_params) do |
| 33 | + {:ok, msg_encrypted} -> |
| 34 | + msg_decrypted = decrypt(msg_encrypted) |
| 35 | + %{conn | body_params: msg_decrypted} |
| 36 | + :error -> |
| 37 | + raise ParseError, "invalid msg_signature" |
| 38 | + end |
| 39 | + false -> |
| 40 | + %{conn | body_params: msg} |
| 41 | + end |
| 42 | + rescue |
| 43 | + _ -> |
| 44 | + conn |
| 45 | + |> send_resp(400, "") |
| 46 | + |> halt |
| 47 | + end |
| 48 | + |
| 49 | + defp verify_msg_signature(%{"Encrypt" => msg_encrypted}, |
| 50 | + %{"timestamp" => timestamp, "nonce" => nonce, |
| 51 | + "msg_signature" => signature}) do |
| 52 | + args = [token(), timestamp, nonce, msg_encrypted] |
| 53 | + case SignatureVerifier.verify(args, signature) do |
| 54 | + :ok -> |
| 55 | + {:ok, msg_encrypted} |
| 56 | + :error -> |
| 57 | + :error |
| 58 | + end |
| 59 | + end |
| 60 | + |
| 61 | + defp decrypt(msg_encrypted) do |
| 62 | + appid = appid() |
| 63 | + encoding_aes_key = encoding_aes_key() |
| 64 | + case MessageEncryptor.decrypt(msg_encrypted, encoding_aes_key) do |
| 65 | + {^appid, msg_decrypted} -> |
| 66 | + extract_xml(msg_decrypted) |
| 67 | + _ -> |
| 68 | + raise ParseError, "invalid appid" |
| 69 | + end |
| 70 | + end |
| 71 | + |
| 72 | + defp msg_encrypted?(%{"encrypt_type" => "aes"}), do: true |
| 73 | + defp msg_encrypted?(_), do: false |
| 74 | + |
| 75 | + def extract_xml(doc) do |
| 76 | + doc |
| 77 | + |> xpath(~x"//xml/*"l) |
| 78 | + |> Enum.into(%{}, &({xpath(&1, ~x"name(.)"s), xpath(&1, ~x"text()"s)})) |
| 79 | + end |
| 80 | + end |
Loading more files…