Current section

29 Versions

Jump to

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…