Current section

29 Versions

Jump to

Compare versions

8 files changed
+150 additions
-48 deletions
  @@ -1,5 +1,7 @@
1 1 # Wechat
2 2
3 + [![Join the chat at https://gitter.im/goofansu/wechat_elixir](https://badges.gitter.im/goofansu/wechat_elixir.svg)](https://gitter.im/goofansu/wechat_elixir?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 +
3 5 Wechat API wrapper in Elixir.
4 6
5 7 ## Installation
  @@ -30,7 +32,8 @@ Add config in `config.exs`:
30 32 config :wechat, Wechat,
31 33 appid: "wechat app id",
32 34 secret: "wechat app secret",
33 - token: "wechat token"
35 + token: "wechat token",
36 + encoding_aes_key: "32bits key" # 只有"兼容模式"和"安全模式"才需要配置这个值
34 37 ```
35 38
36 39 ## API Usage
  @@ -70,7 +73,7 @@ iex> Wechat.Media.download("GuSq91L0FXQFOIFtKwX2i5UPXH9QKnnu63_z4JHZwIw3TMIn1C-x
70 73
71 74 * `Wechat.Plugs.CheckMsgSignature`
72 75
73 - Parse xml message (encrypted msg not supported at the moment)
76 + Parse xml message (support decrypt msg)
74 77
75 78 * router.ex
76 79
  @@ -99,7 +102,7 @@ iex> Wechat.Media.download("GuSq91L0FXQFOIFtKwX2i5UPXH9QKnnu63_z4JHZwIw3TMIn1C-x
99 102 use MyApp.Web, :controller
100 103
101 104 plug Wechat.Plugs.CheckUrlSignature
102 - plug Wechat.Plugs.CheckMsgSignature, only: [:create]
105 + plug Wechat.Plugs.CheckMsgSignature when action in [:create]
103 106
104 107 def index(conn, %{"echostr" => echostr}) do
105 108 text conn, echostr
  @@ -5,9 +5,11 @@
5 5 {<<"files">>,
6 6 [<<"lib/wechat.ex">>,<<"lib/wechat/access_token.ex">>,
7 7 <<"lib/wechat/api_base.ex">>,<<"lib/wechat/api_file.ex">>,
8 - <<"lib/wechat/media.ex">>,<<"lib/wechat/plugs/check_msg_signature.ex">>,
9 - <<"lib/wechat/plugs/check_url_signature.ex">>,<<"lib/wechat/user.ex">>,
10 - <<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}.
8 + <<"lib/wechat/cipher.ex">>,<<"lib/wechat/media.ex">>,
9 + <<"lib/wechat/msg_parser.ex">>,
10 + <<"lib/wechat/plugs/check_msg_signature.ex">>,
11 + <<"lib/wechat/plugs/check_url_signature.ex">>,<<"lib/wechat/signature.ex">>,
12 + <<"lib/wechat/user.ex">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}.
11 13 {<<"licenses">>,[<<"MIT">>]}.
12 14 {<<"links">>,[{<<"Github">>,<<"https://github.com/goofansu/wechat_elixir">>}]}.
13 15 {<<"maintainers">>,[<<"goofansu">>]}.
  @@ -29,4 +31,4 @@
29 31 {<<"name">>,<<"floki">>},
30 32 {<<"optional">>,false},
31 33 {<<"requirement">>,<<"~> 0.9.0">>}]]}.
32 - {<<"version">>,<<"0.1.1">>}.
34 + {<<"version">>,<<"0.1.2">>}.
  @@ -0,0 +1,36 @@
1 + defmodule Wechat.Cipher do
2 + @moduledoc """
3 + Decrypt wechat msg.
4 + """
5 +
6 + def decrypt(msg_encrypt, aes_key) do
7 + plain_text =
8 + msg_encrypt
9 + |> Base.decode64!
10 + |> decrypt_aes(aes_key)
11 + |> decode_padding
12 +
13 + # http://mp.weixin.qq.com/wiki/2/3478f69c0d0bbe8deb48d66a3111ff6e.html
14 + # random(16B) + msg_len(4B) + msg + appid
15 + <<_ :: binary-size(16),
16 + msg_len :: integer-size(32),
17 + msg :: binary-size(msg_len),
18 + appid :: binary>> = plain_text
19 +
20 + {appid, msg}
21 + end
22 +
23 + defp decrypt_aes(aes_encrypt, aes_key) do
24 + iv = binary_part(aes_key, 0, 16)
25 + :crypto.block_decrypt(:aes_cbc128, aes_key, iv, aes_encrypt)
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
  @@ -0,0 +1,30 @@
1 + defmodule Wechat.MsgParser do
2 + @moduledoc """
3 + Parse wechat xml message.
4 + Generate a map structured message with plain xml.
5 + """
6 +
7 + # attrs
8 + # [{<<"tousername">>,[],[<<"gh_7e071da28932">>]},
9 + # {<<"fromusername">>,[],[<<"oi00OuKAhA8bm5okpaIDs7WmUZr4">>]},
10 + # {<<"createtime">>,[],[<<"1468999644">>]},
11 + # {<<"msgtype">>,[],[<<"text">>]},
12 + # {<<"content">>,[],[<<229,147,136>>]},
13 + # {<<"msgid">>,[],[<<"6309305429231578443">>]}]
14 +
15 + # return
16 + # %{
17 + # content => <<229,147,136>>,
18 + # createtime => <<"1468999644">>,
19 + # fromusername => <<"oi00OuKAhA8bm5okpaIDs7WmUZr4">>,
20 + # msgid => <<"6309305429231578443">>,
21 + # msgtype => <<"text">>,
22 + # tousername => <<"gh_7e071da28932">>
23 + # }
24 + def parse(xml) do
25 + [{"xml", [], attrs}] = Floki.find(xml, "xml")
26 + for {key, _, [value]} <- attrs, into: %{} do
27 + {String.to_atom(key), value}
28 + end
29 + end
30 + end
  @@ -4,39 +4,63 @@ defmodule Wechat.Plugs.CheckMsgSignature do
4 4 """
5 5
6 6 import Plug.Conn
7 + import Wechat.MsgParser
8 + import Wechat.Signature
9 + import Wechat.Cipher
7 10
8 11 def init(opts) do
9 - opts
12 + Keyword.merge opts,
13 + token: Wechat.config[:token],
14 + appid: Wechat.config[:appid],
15 + aes_key: aes_key(Wechat.config[:encoding_aes_key])
10 16 end
11 17
12 - def call(conn, _opts) do
13 - {:ok, body, conn} = read_body(conn)
14 - msg = parse_xml(body)
15 - assign(conn, :msg, msg)
18 + defp aes_key(nil) do
19 + nil
20 + end
21 + defp aes_key(encoding_aes_key) do
22 + encoding_aes_key <> "=" |> Base.decode64!
16 23 end
17 24
25 + def call(conn, opts) do
26 + {:ok, xml, conn} = read_body(conn)
27 + msg = xml |> parse
28 + case msg_encrypted?(conn.params) do
29 + true -> decrypt_msg(conn, msg, opts)
30 + false -> conn |> assign(:msg, msg)
31 + end
32 + end
18 33
19 - # attrs
20 - # [{<<"tousername">>,[],[<<"gh_7e071da28932">>]},
21 - # {<<"fromusername">>,[],[<<"oi00OuKAhA8bm5okpaIDs7WmUZr4">>]},
22 - # {<<"createtime">>,[],[<<"1468999644">>]},
23 - # {<<"msgtype">>,[],[<<"text">>]},
24 - # {<<"content">>,[],[<<229,147,136>>]},
25 - # {<<"msgid">>,[],[<<"6309305429231578443">>]}]
34 + defp decrypt_msg(conn, %{encrypt: msg_encrypt} = msg, opts) do
35 + appid = Keyword.fetch!(opts, :appid)
36 + token = Keyword.fetch!(opts, :token)
37 + aes_key = Keyword.fetch!(opts, :aes_key)
38 + case msg_valid?(msg_encrypt, conn.params, token) do
39 + true ->
40 + case decrypt(msg_encrypt, aes_key) do
41 + {^appid, msg_decrypt} ->
42 + conn
43 + |> assign(:msg, msg_decrypt |> parse)
44 + _ ->
45 + conn
46 + |> send_resp(400, "decrypt msg failed")
47 + |> halt
48 + end
49 + false ->
50 + conn
51 + |> send_resp(400, "invalid msg")
52 + |> halt
53 + end
54 + end
26 55
27 - # return
28 - # %{
29 - # content => <<229,147,136>>,
30 - # createtime => <<"1468999644">>,
31 - # fromusername => <<"oi00OuKAhA8bm5okpaIDs7WmUZr4">>,
32 - # msgid => <<"6309305429231578443">>,
33 - # msgtype => <<"text">>,
34 - # tousername => <<"gh_7e071da28932">>
35 - # }
36 - defp parse_xml(body) do
37 - [{"xml", [], attrs}] = Floki.find(body, "xml")
38 - for {key, _, [value]} <- attrs, into: %{} do
39 - {String.to_atom(key), value}
40 - end
56 + defp msg_encrypted?(params) do
57 + encrypt_type = Map.get(params, "encrypt_type")
58 + encrypt_type in [nil, "raw"] == false
59 + end
60 +
61 + defp msg_valid?(msg_encrypt, params, token) do
62 + %{"timestamp" => timestamp, "nonce" => nonce, "msg_signature" => signature} = params
63 + my_signature = [token, timestamp, nonce, msg_encrypt] |> sign
64 + my_signature == signature
41 65 end
42 66 end
Loading more files…