Current section
29 Versions
Jump to
Current section
29 Versions
Compare versions
8
files changed
+150
additions
-48
deletions
| @@ -1,5 +1,7 @@ | |
| 1 1 | |
| 2 2 | |
| 3 | + [](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…