├── .gitignore ├── LICENSE ├── README.md └── lib └── resty └── wechat ├── config.lua ├── jssdk_config.lua ├── oauth.lua ├── proxy.lua ├── proxy_access_filter.lua ├── proxy_access_token.lua ├── server.lua └── utils ├── aes.lua ├── base62.lua ├── basex.lua ├── cookie.lua ├── hex.lua ├── http.lua ├── http_headers.lua ├── libxml2.so ├── random.lua ├── redis.lua ├── urlcodec.lua └── xml2lib.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 CharLemAznable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-wechat 2 | 3 | [![GitHub license](https://img.shields.io/github/license/CharLemAznable/lua-resty-wechat.svg)](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/LICENSE) 4 | 5 | [![GitHub watchers](https://img.shields.io/github/watchers/CharLemAznable/lua-resty-wechat.svg?style=social&label=Watch&maxAge=86400)](https://GitHub.com/CharLemAznable/lua-resty-wechat/watchers/) 6 | [![GitHub stars](https://img.shields.io/github/stars/CharLemAznable/lua-resty-wechat.svg?style=social&label=Star&maxAge=86400)](https://GitHub.com/CharLemAznable/lua-resty-wechat/stargazers/) 7 | [![GitHub forks](https://img.shields.io/github/forks/CharLemAznable/lua-resty-wechat.svg?style=social&label=Fork&maxAge=86400)](https://GitHub.com/CharLemAznable/lua-resty-wechat/network/) 8 | 9 | 使用Lua编写的nginx服务器微信公众平台代理. 10 | 11 | 目标: 12 | * 在前置的nginx内做微信代理, 降低内部应用层和微信服务的耦合. 13 | * 配置微信公众号的自动回复, 在nginx内处理部分用户消息, 减小应用层压力. 14 | * 统一管理微信公众号API中使用的```access_token```, 作为中控服务器隔离业务层和API实现, 降低```access_token```冲突率, 增加服务稳定性. 15 | * 部署微信JS-SDK授权回调页面, 减小应用层压力. 16 | 17 | ## 子模块说明 18 | 19 | ### 全局配置 20 | 21 | [config](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/config.lua) 22 | 23 | 公众号全局配置数据, 包括接口Token, 自动回复设置. 24 | 25 | ### 作为服务端由微信请求并响应 26 | 27 | [server](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/server.lua) 28 | 29 | 接收微信发出的普通消息和事件推送等请求, 并按配置做出响应, 未做对应配置则按微信要求返回```success```. 30 | 31 | 此部分核心代码由[aCayF/lua-resty-wechat](https://github.com/aCayF/lua-resty-wechat)做重构修改而来. 32 | 33 | 使用```config.autoreplyurl```, 配置后台处理服务地址, 转发处理并响应复杂的微信消息. (依赖[pintsized/lua-resty-http](https://github.com/pintsized/lua-resty-http)) 34 | 35 | ### 作为客户端代理调用微信公众号API 36 | 37 | [proxy_access_token](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/proxy_access_token.lua) 38 | 39 | 使用Redis缓存```access_token```和```jsapi_ticket```, 定时自动调用微信服务更新, 支持分布式更新. 40 | 41 | [proxy](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/proxy.lua) 42 | 43 | 代理调用微信公众平台API接口, 自动添加```access_token```参数. 44 | 45 | [proxy_access_filter](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/proxy_access_filter.lua) 46 | 47 | 过滤客户端IP, 限制请求来源. 48 | 49 | ### 代理网页授权获取用户基本信息 50 | 51 | [oauth](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/oauth.lua) 52 | 53 | ### JS-SDK权限签名 54 | 55 | [jssdk_config](https://github.com/CharLemAznable/lua-resty-wechat/blob/master/lib/resty/wechat/jssdk_config.lua) 56 | 57 | ## 示例 58 | 59 | nginx配置: 60 | 61 | ``` nginx 62 | http { 63 | lua_package_path 'path to lua files'; 64 | resolver 114.114.114.114; 65 | 66 | lua_shared_dict wechat 1M; # 利用共享内存保持单例定时器 67 | init_by_lua ' 68 | ngx.shared.wechat:delete("updater") -- 清除定时器标识 69 | require("resty.wechat.config") 70 | '; 71 | init_worker_by_lua ' 72 | local ok, err = ngx.shared.wechat:add("updater", "1") -- 单进程启动定时器 73 | if not ok or err then return end 74 | require("resty.wechat.proxy_access_token")() 75 | '; 76 | server { 77 | location /wechat-server { 78 | content_by_lua ' 79 | require("resty.wechat.server")() 80 | '; 81 | } 82 | location /wechat-proxy/ { 83 | rewrite_by_lua ' 84 | require("resty.wechat.proxy")("wechat-proxy") -- 参数为location路径 85 | '; 86 | access_by_lua ' 87 | require("resty.wechat.proxy_access_filter")() 88 | '; 89 | proxy_pass https://api.weixin.qq.com/; 90 | } 91 | location /wechat-baseoauth { # param: goto 92 | rewrite_by_lua ' 93 | require("resty.wechat.oauth").base_oauth("path to /wechat-redirect") 94 | '; 95 | } 96 | location /wechat-useroauth { # param: goto 97 | rewrite_by_lua ' 98 | require("resty.wechat.oauth").userinfo_oauth("path to /wechat-redirect") 99 | '; 100 | } 101 | location /wechat-redirect { 102 | rewrite_by_lua ' 103 | require("resty.wechat.oauth").redirect() 104 | '; 105 | } 106 | location /wechat-jssdk-config { # GET/POST, param: url, [api] 107 | add_header Access-Control-Allow-Origin "if need cross-domain call"; 108 | content_by_lua ' 109 | require("resty.wechat.jssdk_config")() 110 | '; 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | 网页注入JS-SDK权限: 117 | 118 | ``` javascript 119 | $.ajax({ 120 | url: "url path to /wechat-jssdk-config", 121 | data: { 122 | url: window.location.href, 123 | api: "onMenuShareTimeline|onMenuShareAppMessage|onMenuShareQQ|onMenuShareWeibo|onMenuShareQZone" 124 | }, 125 | success: function(response) { 126 | wx.config(response); 127 | } 128 | }); 129 | 130 | $.ajax({ 131 | url: "url path to /wechat-jssdk-config", 132 | data: { 133 | url: window.location.href 134 | }, 135 | success: function(response) { 136 | wx.config({ 137 | appId: response.appId, 138 | timestamp: response.timestamp, 139 | nonceStr: response.nonceStr, 140 | signature: response.signature, 141 | jsApiList: [ 142 | 'onMenuShareTimeline', 143 | 'onMenuShareAppMessage', 144 | 'onMenuShareQQ', 145 | 'onMenuShareWeibo', 146 | 'onMenuShareQZone' 147 | ] 148 | }); 149 | } 150 | }); 151 | ``` 152 | 153 | 使用Java解析代理网页授权获得的cookie 154 | 155 | ``` java 156 | Map authInfo = JSON.parseObject(decryptAES(unBase64("cookie value"), getKey("AES key"))); 157 | 158 | // 默认AES key: "vFrItmxI9ct8JbAg" 159 | // 配置于config.lua -> cookie_aes_key 160 | 161 | // 依赖方法 162 | 163 | import com.alibaba.fastjson.JSON; 164 | import com.google.common.base.Charsets; 165 | import javax.crypto.Cipher; 166 | import javax.crypto.spec.SecretKeySpec; 167 | import java.security.Key; 168 | 169 | public StringBuilder padding(String s, char letter, int repeats) { 170 | StringBuilder sb = new StringBuilder(s); 171 | while (repeats-- > 0) { 172 | sb.append(letter); 173 | } 174 | return sb; 175 | } 176 | 177 | public String padding(String s) { 178 | return padding(s, '=', s.length() % 4).toString(); 179 | } 180 | 181 | public byte[] unBase64(String value) { 182 | return org.apache.commons.codec.binary.Base64.decodeBase64(padding(value)); 183 | } 184 | 185 | public String string(byte[] bytes) { 186 | return new String(bytes, Charsets.UTF_8); 187 | } 188 | 189 | public String decryptAES(byte[] value, Key key) { 190 | try { 191 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 192 | cipher.init(Cipher.DECRYPT_MODE, key); 193 | byte[] decrypted = cipher.doFinal(value); 194 | return string(decrypted); 195 | } catch (Exception e) { 196 | throw new RuntimeException(e); 197 | } 198 | } 199 | 200 | public byte[] bytes(String str) { 201 | return str == null ? null : str.getBytes(Charsets.UTF_8); 202 | } 203 | 204 | public Key keyFromString(String keyString) { 205 | return new SecretKeySpec(bytes(keyString), "AES"); 206 | } 207 | 208 | public Key getKey(String key) { 209 | if (key.length() >= 16) { 210 | return keyFromString(key.substring(0, 16)); 211 | } 212 | StringBuilder sb = new StringBuilder(key); 213 | while (sb.length() < 16) { 214 | sb.append(key); 215 | } 216 | return keyFromString(sb.toString().substring(0, 16)); 217 | } 218 | ``` 219 | -------------------------------------------------------------------------------- /lib/resty/wechat/config.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_config" 2 | local _M = { _VERSION = '0.0.2' } 3 | _G[modname] = _M 4 | 5 | _M.appid = "" -- 公众平台AppID 6 | _M.appsecret = "" -- 公众平台AppSecret 7 | 8 | _M.token = "" -- 公众平台接口配置Token 9 | 10 | ---------- Optional ---------- 11 | 12 | -- _M.autoreply = { -- 简单的自动回复设置 13 | -- text = { 14 | -- { cond = { content = "用户发出的文字消息全文匹配的正则表达式" }, 15 | -- resp = { msgtype = "text或其他消息类型", 以及对应消息所需的字段和内容 }, 16 | -- continue = true/false -- 消息是否透传到autoreplyurl 17 | -- }, 18 | -- }, 19 | -- image = { }, 20 | -- voice = { }, 21 | -- video = { }, 22 | -- location = { }, 23 | -- link = { }, 24 | -- event = { 25 | -- { cond = { event = "CLICK或其他事件类型", 以及事件标识的全文匹配正则表达式 }, 26 | -- resp = { msgtype = "text或其他消息类型", 以及对应消息所需的字段和内容 } 27 | -- }, 28 | -- }, 29 | -- } 30 | 31 | -- _M.autoreplyurl = "" -- 转发消息到指定URL, 对应服务可返回消息内容的JSON, 或直接返回success 32 | 33 | -- _M.redis = { -- redis配置 34 | -- host = "127.0.0.1", 35 | -- port = 6379, 36 | -- timeout = 5000, 37 | -- maxIdleTimeout = 10000, 38 | -- poolSize = 10, 39 | -- distributedLockTimeout = 10, 40 | -- } 41 | 42 | -- _M.accessTokenUpdateTime = 6000 -- AccessToken更新时间 43 | -- _M.accessTokenPollingTime = 600 -- AccessToken更新轮询时间 44 | -- _M.accessTokenKey = _M.appid -- AccessToken存储在redis的key 45 | -- _M.jsapiTicketKey = _M.appid .. "_ticket" -- jsapi_ticket存储在redis的key 46 | 47 | -- _M.permitClientIPs = { "127.0.0.1" } -- 允许访问的客户端IP列表 48 | 49 | -- _M.base_oauth_key = "__rywy_base" -- 网页授权跳转后保存用户基本信息的cookie的key 50 | -- _M.userinfo_oauth_key = "__rywy_userinfo" -- 网页授权跳转后保存用户完整信息的cookie的key 51 | -- _M.cookie_domain = nil -- 网页授权跳转后保存用户信息的cookie的domain 52 | -- _M.cookie_path = nil -- 网页授权跳转后保存用户信息的cookie的path 53 | -- _M.cookie_aes_key = "vFrItmxI9ct8JbAg" -- 网页授权跳转后保存用户信息的cookie的AES密钥 54 | 55 | return _M 56 | -------------------------------------------------------------------------------- /lib/resty/wechat/jssdk_config.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_jssdk_config" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local random = require("resty.wechat.utils.random") 6 | local hex = require("resty.wechat.utils.hex") 7 | local cjson = require("cjson") 8 | 9 | local table_insert = table.insert 10 | local table_sort = table.sort 11 | local table_concat = table.concat 12 | local ngx_req = ngx.req 13 | 14 | local jsapiTicketKey = wechat_config.jsapiTicketKey or (wechat_config.appid .. "_ticket") 15 | 16 | local function string_split(str, delimiter) 17 | if str == nil or str == '' or delimiter == nil then 18 | return nil 19 | end 20 | 21 | local result = {} 22 | for match in (str..delimiter):gmatch("(.-)"..delimiter) do 23 | table_insert(result, match) 24 | end 25 | return result 26 | end 27 | 28 | local function get_request_args() 29 | local request_method = ngx.var.request_method 30 | if "GET" == request_method then 31 | return ngx_req.get_uri_args() 32 | elseif "POST" == request_method then 33 | ngx_req.read_body() 34 | return ngx_req.get_post_args() 35 | end 36 | return {} 37 | end 38 | 39 | local mt = { 40 | __call = function(_, url_param_name, api_list_param_name) 41 | local noncestr = random.token(16) 42 | local jsapi_ticket = require("resty.wechat.utils.redis"):connect(wechat_config.redis).redis:get(jsapiTicketKey) 43 | local timestamp = os.time() 44 | 45 | local args = get_request_args() 46 | local url = args[url_param_name or "url"] or "" 47 | url = string_split(url, "#")[1] 48 | 49 | local tmptab = {} 50 | table_insert(tmptab, "noncestr=" .. noncestr) 51 | table_insert(tmptab, "jsapi_ticket=" .. jsapi_ticket) 52 | table_insert(tmptab, "timestamp=" .. timestamp) 53 | table_insert(tmptab, "url=" .. url) 54 | table_sort(tmptab) 55 | local signature = hex(ngx.sha1_bin(table_concat(tmptab, "&"))) 56 | 57 | local result = { 58 | appId = wechat_config.appid, 59 | timestamp = timestamp, 60 | nonceStr = noncestr, 61 | signature = signature, 62 | } 63 | local api_list_param = args[api_list_param_name or "api"] 64 | if api_list_param then 65 | result["jsApiList"] = string_split(api_list_param, "|") 66 | end 67 | 68 | ngx.header["Content-Type"] = "application/json" 69 | ngx.print(cjson.encode(result)) 70 | return ngx.exit(ngx.HTTP_OK) 71 | end, 72 | } 73 | 74 | return setmetatable(_M, mt) 75 | -------------------------------------------------------------------------------- /lib/resty/wechat/oauth.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_oauth" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local urlcodec = require("resty.wechat.utils.urlcodec") 6 | local base62 = require("resty.wechat.utils.base62") 7 | local cjson = require("cjson") 8 | local aescodec = require("resty.wechat.utils.aes").new(wechat_config.cookie_aes_key or "vFrItmxI9ct8JbAg") 9 | local cookie = require("resty.wechat.utils.cookie") 10 | local base_oauth_key = wechat_config.base_oauth_key or "__rywy_base" 11 | local userinfo_oauth_key = wechat_config.userinfo_oauth_key or "__rywy_userinfo" 12 | 13 | local ngx_log = ngx.log 14 | local ngx_exit = ngx.exit 15 | 16 | --------------------------------------------------private methods 17 | 18 | local authorize_addr = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" .. wechat_config.appid .. "&redirect_uri=" 19 | local response_type_and_scope = "&response_type=code&scope=" 20 | local state = "&state=" 21 | local hash = "#wechat_redirect?" 22 | 23 | local function build_oauth_redirect_addr(redirect_uri, scope, target) 24 | return authorize_addr .. urlcodec.encodeURI(redirect_uri) .. response_type_and_scope .. scope .. state .. base62:encode(target) .. hash 25 | end 26 | 27 | local access_token_addr = "https://api.weixin.qq.com/sns/oauth2/access_token" 28 | 29 | local function oauth_access_token(code) 30 | local param = { 31 | method = "GET", 32 | query = { 33 | grant_type = "authorization_code", 34 | appid = wechat_config.appid, 35 | secret = wechat_config.appsecret, 36 | code = code, 37 | }, 38 | ssl_verify = false, 39 | headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, 40 | } 41 | 42 | local res, err = require("resty.wechat.utils.http").new():request_uri(access_token_addr, param) 43 | if not res or err or tostring(res.status) ~= "200" then 44 | return nil, err or tostring(res.status) 45 | end 46 | 47 | local resbody = cjson.decode(res.body) 48 | if not resbody.access_token then 49 | return nil, res.body 50 | end 51 | 52 | return resbody 53 | end 54 | 55 | local userinfo_addr = "https://api.weixin.qq.com/sns/userinfo" 56 | 57 | local function oauth_userinfo(access_token, openid) 58 | local param = { 59 | method = "GET", 60 | query = { 61 | access_token = access_token, 62 | openid = openid, 63 | }, 64 | ssl_verify = false, 65 | headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, 66 | } 67 | 68 | local res, err = require("resty.wechat.utils.http").new():request_uri(userinfo_addr, param) 69 | if not res or err or tostring(res.status) ~= "200" then 70 | return nil, err or tostring(res.status) 71 | end 72 | 73 | local resbody = cjson.decode(res.body) 74 | if not resbody.openid then 75 | return nil, res.body 76 | end 77 | 78 | return resbody 79 | end 80 | 81 | local function request_oauth_infomation(code) 82 | local baseinfo, err = oauth_access_token(code) 83 | if err then return nil, nil, "failed to get oauth access token: " .. err end 84 | 85 | if baseinfo.scope ~= "snsapi_userinfo" then 86 | return { openid = baseinfo.openid } 87 | end 88 | 89 | local userinfo, err = oauth_userinfo(baseinfo.access_token, baseinfo.openid) 90 | if err then return nil, nil, "failed to get oauth userinfo: " .. err end 91 | 92 | return { openid = baseinfo.openid }, userinfo 93 | end 94 | 95 | local function process_share_from_param(target) 96 | local from_param = ngx.var.arg_from 97 | if not from_param then return target end 98 | return target .. (string.match(target, "?") and "&" or "?") .. "from=" .. from_param 99 | end 100 | 101 | --------------------------------------------------public methods 102 | 103 | function _M.base_oauth(redirect_uri, goto_param_name) 104 | local goto_param = ngx.var["arg_" .. (goto_param_name or "goto")] 105 | if not goto_param then return ngx_exit(ngx.HTTP_BAD_REQUEST) end 106 | local target = process_share_from_param(urlcodec.decodeURI(goto_param)) 107 | 108 | if cookie.get(base_oauth_key) then 109 | return ngx.redirect(target, ngx.HTTP_MOVED_TEMPORARILY) 110 | end 111 | return ngx.redirect(build_oauth_redirect_addr(redirect_uri, "snsapi_base", target), ngx.HTTP_MOVED_TEMPORARILY) 112 | end 113 | 114 | function _M.userinfo_oauth(redirect_uri, goto_param_name) 115 | local goto_param = ngx.var["arg_" .. (goto_param_name or "goto")] 116 | if not goto_param then return ngx_exit(ngx.HTTP_BAD_REQUEST) end 117 | local target = process_share_from_param(urlcodec.decodeURI(goto_param)) 118 | 119 | if cookie.get(userinfo_oauth_key) then 120 | return ngx.redirect(target, ngx.HTTP_MOVED_TEMPORARILY) 121 | end 122 | return ngx.redirect(build_oauth_redirect_addr(redirect_uri, "snsapi_userinfo", target), ngx.HTTP_MOVED_TEMPORARILY) 123 | end 124 | 125 | function _M.redirect() 126 | local state = base62:decode(tostring(ngx.var.arg_state)) 127 | if not ngx.var.arg_code then -- unauthorized 128 | return ngx.redirect(state, ngx.HTTP_MOVED_TEMPORARILY) 129 | end 130 | 131 | local code = tostring(ngx.var.arg_code) 132 | local baseinfo, userinfo, err = request_oauth_infomation(code) 133 | if err then 134 | ngx_log(ngx.ERR, err) 135 | return ngx_exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 136 | end 137 | 138 | local encrypted_baseinfo = ngx.encode_base64(aescodec:encrypt(cjson.encode(baseinfo))) 139 | cookie.set({ 140 | key = base_oauth_key, 141 | value = encrypted_baseinfo, 142 | expires = ngx.cookie_time(ngx.now() + 7200), 143 | domain = wechat_config.cookie_domain, 144 | path = wechat_config.cookie_path, 145 | }) 146 | 147 | if userinfo then 148 | local encrypted_userinfo = ngx.encode_base64(aescodec:encrypt(cjson.encode(userinfo))) 149 | cookie.set({ 150 | key = userinfo_oauth_key, 151 | value = encrypted_userinfo, 152 | expires = ngx.cookie_time(ngx.now() + 7200), 153 | domain = wechat_config.cookie_domain, 154 | path = wechat_config.cookie_path, 155 | }) 156 | end 157 | 158 | ngx.log(ngx.ERR, cjson.encode(ngx.header.Set_Cookie)) 159 | 160 | return ngx.redirect(state, ngx.HTTP_MOVED_TEMPORARILY) 161 | end 162 | 163 | return _M 164 | -------------------------------------------------------------------------------- /lib/resty/wechat/proxy.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_proxy" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local ngx_re_sub = ngx.re.sub 6 | local ngx_req_set_uri = ngx.req.set_uri 7 | local ngx_req_get_uri_args = ngx.req.get_uri_args 8 | local ngx_req_set_uri_args = ngx.req.set_uri_args 9 | 10 | local accessTokenKey = wechat_config.accessTokenKey or wechat_config.appid 11 | 12 | local mt = { 13 | __call = function(_, location_root) 14 | local uri = ngx_re_sub(ngx.var.uri, "^/" .. location_root .. "(.*)", "$1", "o") 15 | ngx_req_set_uri(uri) 16 | 17 | local args = ngx_req_get_uri_args() 18 | args["access_token"] = require("resty.wechat.utils.redis"):connect(wechat_config.redis).redis:get(accessTokenKey) 19 | ngx_req_set_uri_args(args) 20 | end, 21 | } 22 | 23 | return setmetatable(_M, mt) 24 | -------------------------------------------------------------------------------- /lib/resty/wechat/proxy_access_filter.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_proxy_access_filter" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local permitClientIPs = wechat_config.permitClientIPs or { "127.0.0.1" } 6 | 7 | local function tableContainsValue(t, value) 8 | for k, v in pairs(t) do 9 | if value == v then return true end 10 | end 11 | return false 12 | end 13 | 14 | if not tableContainsValue(permitClientIPs, "127.0.0.1") then 15 | table.insert(permitClientIPs, "127.0.0.1") 16 | end 17 | 18 | local mt = { 19 | __call = function(_) 20 | local real_ip = ngx.req.get_headers()["X-Real-IP"] 21 | if not real_ip then real_ip = ngx.req.get_headers()["X-Forwarded-For"] end 22 | if not real_ip then real_ip = ngx.var.remote_addr end 23 | 24 | if not tableContainsValue(permitClientIPs, real_ip) then 25 | ngx.exit(ngx.HTTP_FORBIDDEN) 26 | end 27 | end, 28 | } 29 | 30 | return setmetatable(_M, mt) 31 | -------------------------------------------------------------------------------- /lib/resty/wechat/proxy_access_token.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_proxy_access_token" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local ngx_log = ngx.log 6 | local ngx_timer_at = ngx.timer.at 7 | 8 | local cjson = require("cjson") 9 | 10 | local updateurl = "https://api.weixin.qq.com/cgi-bin/token" 11 | local updateparam = { 12 | method = "GET", 13 | query = { 14 | grant_type = "client_credential", 15 | appid = wechat_config.appid, 16 | secret = wechat_config.appsecret, 17 | }, 18 | ssl_verify = false, 19 | headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, 20 | } 21 | 22 | local ticketurl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket" 23 | local ticketparam = { 24 | method = "GET", 25 | query = { 26 | type = "jsapi", 27 | }, 28 | ssl_verify = false, 29 | headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, 30 | } 31 | 32 | local updateTime = wechat_config.accessTokenUpdateTime or 6000 33 | local pollingTime = wechat_config.accessTokenPollingTime or 600 34 | local accessTokenKey = wechat_config.accessTokenKey or wechat_config.appid 35 | local jsapiTicketKey = wechat_config.jsapiTicketKey or (wechat_config.appid .. "_ticket") 36 | 37 | local mt = { 38 | __call = function(_) 39 | local updateAccessToken 40 | updateAccessToken = function() 41 | require("resty.wechat.utils.redis"):connect(wechat_config.redis):lockProcess( 42 | "accessTokenLocker", 43 | function(weredis) 44 | if 0 < tonumber(weredis.redis:ttl(accessTokenKey) or 0) then 45 | return 46 | end 47 | 48 | -- access_token time out, refresh 49 | local res, err = require("resty.wechat.utils.http").new():request_uri(updateurl, updateparam) 50 | if not res or err or tostring(res.status) ~= "200" then 51 | ngx_log(ngx.ERR, "failed to update access token: ", err or tostring(res.status)) 52 | return 53 | end 54 | local resbody = cjson.decode(res.body) 55 | if not resbody.access_token then 56 | ngx_log(ngx.ERR, "failed to update access token: ", res.body) 57 | return 58 | end 59 | 60 | local ok, err = weredis.redis:setex(accessTokenKey, updateTime - 1, resbody.access_token) 61 | if not ok then 62 | ngx_log(ngx.ERR, "failed to set access token: ", err) 63 | return 64 | end 65 | 66 | ngx_log(ngx.NOTICE, "succeed to set access token: ", res.body) 67 | 68 | -- refresh jsapi_ticket after refresh access_token 69 | ticketparam.query.access_token = resbody.access_token 70 | local res, err = require("resty.wechat.utils.http").new():request_uri(ticketurl, ticketparam) 71 | ticketparam.query.access_token = nil 72 | if not res or err or tostring(res.status) ~= "200" then 73 | ngx_log(ngx.ERR, "failed to update jsapi ticket: ", err or tostring(res.status)) 74 | return 75 | end 76 | local resbody = cjson.decode(res.body) 77 | if not resbody.ticket then 78 | ngx_log(ngx.ERR, "failed to update jsapi ticket: ", res.body) 79 | return 80 | end 81 | 82 | local ok, err = weredis.redis:setex(jsapiTicketKey, updateTime - 1, resbody.ticket) 83 | if not ok then 84 | ngx_log(ngx.ERR, "failed to set jsapi ticket: ", err) 85 | return 86 | end 87 | 88 | ngx_log(ngx.NOTICE, "succeed to set jsapi ticket: ", res.body) 89 | end 90 | ) 91 | 92 | local ok, err = ngx_timer_at(pollingTime, updateAccessToken) 93 | if not ok then 94 | ngx_log(ngx.ERR, "failed to create the Access Token Updater: ", err) 95 | return 96 | end 97 | end 98 | 99 | local ok, err = ngx_timer_at(5, updateAccessToken) 100 | if not ok then 101 | ngx_log(ngx.ERR, "failed to create the Access Token Updater: ", err) 102 | return 103 | end 104 | end, 105 | } 106 | 107 | return setmetatable(_M, mt) 108 | -------------------------------------------------------------------------------- /lib/resty/wechat/server.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- This module is licensed under the BSD license. 3 | -- 4 | -- Copyright (C) 2013-2014, by aCayF (潘力策) . 5 | -- 6 | -- All rights reserved. 7 | -- 8 | -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 9 | -- 10 | -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | -- 12 | -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | -- 14 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -- 16 | 17 | local modname = "wechat_server" 18 | local _M = { _VERSION = '0.0.3' } 19 | _G[modname] = _M 20 | 21 | --------------------------------------------------pre defines 22 | 23 | local ffi = require "ffi" 24 | local ffi_str = ffi.string 25 | 26 | local hex = require "resty.wechat.utils.hex" 27 | local xml2lib = require "resty.wechat.utils.xml2lib" 28 | 29 | local rcvmsgfmt = { 30 | common = { "tousername", "fromusername", "createtime", "msgtype" }, 31 | msgtype = { 32 | text = { "content", "msgid" }, 33 | image = { "picurl", "msgid", "mediaid" }, 34 | voice = { "mediaid", "format", "msgid", { "recognition", optional = true } }, 35 | video = { "mediaid", "thumbmediaid", "msgid" }, 36 | location = { "location_x", "location_y", "scale", "label", "msgid" }, 37 | link = { "title", "description", "url", "msgid" }, 38 | event = { "event" } 39 | }, 40 | event = { 41 | subscribe = { { "eventkey", optional = true }, { "ticket", optional = true } }, 42 | scan = { "eventkey", "ticket" }, 43 | unsubscribe = { { "eventkey", optional = true } }, 44 | location = { "latitude", "longitude", "precision" }, 45 | click = { "eventkey" }, 46 | view = { "eventkey", "menuid" }, 47 | scancode_push = { "eventkey", { "scancodeinfo", subnodes = { "scantype", "scanresult" } } }, 48 | scancode_waitmsg = { "eventkey", { "scancodeinfo", subnodes = { "scantype", "scanresult" } } }, 49 | location_select = { "eventkey", { "sendlocationinfo", subnodes = { "location_x", "location_y", "scale", "label", "poiname" } } }, 50 | pic_sysphoto = { "eventkey", { "sendpicsinfo", subnodes = { "count" } } }, 51 | pic_photo_or_album = { "eventkey", { "sendpicsinfo", subnodes = { "count" } } }, 52 | pic_weixin = { "eventkey", { "sendpicsinfo", subnodes = { "count" } } } 53 | } 54 | } 55 | 56 | local elementnode = "e" 57 | local textnode = "t" 58 | local cdatanode = "c" 59 | 60 | local sndmsgfmt = { 61 | common = { 62 | { "ToUserName", cdatanode }, 63 | { "FromUserName", cdatanode }, 64 | { "CreateTime", textnode }, 65 | { "MsgType", cdatanode } 66 | }, 67 | text = { 68 | { "Content", cdatanode } 69 | }, 70 | image = { 71 | { "Image", elementnode, { { "MediaId", cdatanode } } } 72 | }, 73 | voice = { 74 | { "Voice", elementnode, { { "MediaId", cdatanode } } } 75 | }, 76 | video = { 77 | { "Video", elementnode, { 78 | { "MediaId", cdatanode }, 79 | { "Title", cdatanode, optional = true }, 80 | { "Description", cdatanode, optional = true } } 81 | } 82 | }, 83 | music = { 84 | { "Music", elementnode, { 85 | { "Title", cdatanode, optional = true }, 86 | { "Description", cdatanode, optional = true }, 87 | { "MusicUrl", cdatanode, optional = true }, 88 | { "HQMusicUrl", cdatanode, optional = true }, 89 | { "ThumbMediaId", cdatanode, optional = true } } 90 | } 91 | }, 92 | news = { 93 | { "ArticleCount", textnode }, 94 | { "Articles", elementnode, { 95 | { "item", elementnode, { 96 | { "Title", cdatanode, optional = true }, 97 | { "Description", cdatanode, optional = true }, 98 | { "PicUrl", cdatanode, optional = true }, 99 | { "Url", cdatanode, optional = true } } 100 | } } 101 | } 102 | }, 103 | } 104 | 105 | local table_sort = table.sort 106 | local table_concat = table.concat 107 | local string_lower = string.lower 108 | local string_match = string.match 109 | local string_format = string.format 110 | local string_gsub = string.gsub 111 | local ngx_re_gsub = ngx.re.gsub 112 | local ngx_req = ngx.req 113 | local ngx_log = ngx.log 114 | local ngx_print = ngx.print 115 | local ngx_exit = ngx.exit 116 | 117 | local cjson = require("cjson") 118 | 119 | --------------------------------------------------private methods 120 | 121 | local function _check_signature(params) 122 | local signature = params.signature 123 | local timestamp = params.timestamp 124 | local nonce = params.nonce 125 | local token = params.token 126 | local tmptab = {token, timestamp, nonce} 127 | table_sort(tmptab) 128 | 129 | local tmpstr = table_concat(tmptab) 130 | tmpstr = ngx.sha1_bin(tmpstr) 131 | tmpstr = hex(tmpstr) 132 | 133 | if tmpstr ~= signature then 134 | return nil, "signature mismatch" 135 | end 136 | 137 | return true 138 | end 139 | 140 | local function _verify_request_params(params) 141 | if params.method == "GET" and not params.echostr then 142 | return nil, "missing echostr" 143 | end 144 | return _check_signature(params) 145 | end 146 | 147 | -------------------------------------------------- 148 | 149 | local _parse_key, _parse_keytable 150 | 151 | _parse_key = function(nodePtr, key, rcvmsg) 152 | local node = nodePtr.node 153 | local name = ffi_str(node[0].name) 154 | local istable = (type(key) == "table") 155 | local optional = istable and key.optional or false 156 | local k = istable and key[1] or key 157 | local subnodes = istable and key.subnodes or nil 158 | 159 | if string_lower(name) ~= k then -- case insensitive 160 | if not optional then 161 | return nil, "invalid node name -- " .. name 162 | else 163 | return true 164 | end 165 | end 166 | 167 | if node[0].type ~= xml2lib.XML_ELEMENT_NODE then 168 | return nil, "invalid node type" 169 | end 170 | 171 | node = node[0].children 172 | if node == nil then 173 | return nil, "invalid subnode" 174 | end 175 | 176 | if not subnodes then 177 | if node[0].type ~= xml2lib.XML_TEXT_NODE and node[0].type ~= xml2lib.XML_CDATA_SECTION_NODE then 178 | return nil, "invalid subnode type" 179 | end 180 | 181 | rcvmsg[k] = ffi_str(node[0].content) 182 | 183 | else 184 | local subnodePtr = { node = node } 185 | local ok, err = _parse_keytable(subnodePtr, subnodes, rcvmsg) 186 | if not ok then 187 | return nil, err .. " when parsing " .. name .. " subnodes" 188 | end 189 | end 190 | 191 | node = node[0].parent 192 | if node[0].next ~= nil then 193 | node = node[0].next 194 | end 195 | nodePtr.node = node 196 | 197 | return rcvmsg[k] 198 | end 199 | 200 | _parse_keytable = function(nodePtr, keytable, rcvmsg) 201 | for i = 1, #keytable do 202 | local key = keytable[i] 203 | 204 | local value, err = _parse_key(nodePtr, key, rcvmsg) 205 | if err then 206 | return nil, err 207 | end 208 | end 209 | 210 | return true 211 | end 212 | 213 | local function _retrieve_keytable(nodePtr, key, rcvmsg) 214 | local root = rcvmsgfmt.msgtype 215 | 216 | while true do 217 | if not root[key] then 218 | return nil, "invalid key -- " .. key 219 | end 220 | 221 | -- indicates that no subkeys present 222 | if not rcvmsgfmt[key] then 223 | break 224 | end 225 | 226 | local value, err = _parse_key(nodePtr, key, rcvmsg) 227 | if err then 228 | return nil, err 229 | end 230 | 231 | root = rcvmsgfmt[key] 232 | key = string_lower(value) -- case insensitive 233 | end 234 | 235 | return root[key] 236 | end 237 | 238 | local function _parse_xml(node, rcvmsg) 239 | local keytable, ok, err 240 | 241 | -- element node named xml is expected 242 | if node[0].type ~= xml2lib.XML_ELEMENT_NODE or ffi_str(node[0].name) ~= "xml" then 243 | return nil, "invalid xml title when parsing xml" 244 | end 245 | 246 | if node[0].children == nil then 247 | return nil, "invalid xml content when parsing xml" 248 | end 249 | 250 | -- parse common components 251 | local nodePtr = { node = node[0].children } 252 | keytable = rcvmsgfmt.common 253 | 254 | ok, err = _parse_keytable(nodePtr, keytable, rcvmsg) 255 | if not ok then 256 | return nil, err .. " when parsing common part" 257 | end 258 | 259 | -- retrieve msgtype-specific keytable 260 | keytable, err = _retrieve_keytable(nodePtr, rcvmsg.msgtype, rcvmsg) 261 | if err then 262 | return nil, err .. " when retrieving keytable" 263 | end 264 | 265 | -- parse msgtype-specific components 266 | ok, err = _parse_keytable(nodePtr, keytable, rcvmsg) 267 | if not ok then 268 | return nil, err .. " when parsing msgtype-specific part" 269 | end 270 | 271 | return true 272 | end 273 | 274 | local function _parse_request_body(params) 275 | if not params.body then 276 | return nil, "invalid request body" 277 | end 278 | 279 | local doc = xml2lib.newXmlDoc() 280 | local node = xml2lib.newXmlNode() 281 | local body = params.body 282 | local rcvmsg = params.rcvmsg 283 | 284 | doc = xml2lib.xmlReadMemory(body, #body, nil, nil, 0) 285 | if doc == nil then 286 | return nil, "invalid xml data" 287 | end 288 | 289 | -- root node 290 | node = doc[0].children 291 | local ok, err = _parse_xml(node, rcvmsg) 292 | 293 | -- cleanup used memory anyway 294 | xml2lib.xmlFreeDoc(doc) 295 | xml2lib.xmlCleanupParser() 296 | 297 | if not ok then 298 | return nil, err 299 | end 300 | 301 | return rcvmsg 302 | end 303 | 304 | -------------------------------------------------- 305 | 306 | local function _match_auto_reply(rcvmsg) 307 | local autoreply = wechat_config.autoreply or {} 308 | 309 | local replies = autoreply[rcvmsg.msgtype] 310 | if not replies or #replies == 0 then 311 | return nil 312 | end 313 | 314 | for i = 1, #replies do 315 | local reply = replies[i] 316 | if reply.cond and reply.resp then 317 | local match = true 318 | for k, v in pairs(reply.cond) do 319 | if string_match(rcvmsg[k], v) ~= rcvmsg[k] then -- case sensitive 320 | match = false 321 | break 322 | end 323 | end 324 | 325 | if match then 326 | return reply.resp, reply.continue 327 | end 328 | end 329 | end 330 | 331 | return nil 332 | end 333 | 334 | -------------------------------------------------- 335 | 336 | local function _insert_items(n) 337 | local newsfmts = sndmsgfmt["news"] 338 | local node = newsfmts[2] 339 | local tb = node[3] 340 | 341 | for i = 1, n - 1 do 342 | local item = { "item", elementnode, { 343 | { "Title" .. i, cdatanode, optional = true }, 344 | { "Description" .. i, cdatanode, optional = true }, 345 | { "PicUrl" .. i, cdatanode, optional = true }, 346 | { "Url" .. i, cdatanode, optional = true } } 347 | } 348 | -- push 349 | tb[#tb + 1] = item 350 | end 351 | end 352 | 353 | local function _cleanup_items(n) 354 | local newsfmts = sndmsgfmt["news"] 355 | local node = newsfmts[2] 356 | local tb = node[3] 357 | 358 | for i = 1, n - 1 do 359 | tb[#tb] = nil 360 | end 361 | end 362 | 363 | local function _normalize_items(str) 364 | str = ngx_re_gsub(str, "Title[1-9]>", "Title>") 365 | str = ngx_re_gsub(str, "Description[1-9]>", "Description>") 366 | str = ngx_re_gsub(str, "PicUrl[1-9]>", "PicUrl>") 367 | str = ngx_re_gsub(str, "Url[1-9]>", "Url>") 368 | return str 369 | end 370 | 371 | local function _retrieve_content(sndmsg, fmt) 372 | local name = string_lower(fmt[1]) 373 | local content = sndmsg[name] or "" 374 | local optional = fmt.optional 375 | 376 | if not optional and content == "" then 377 | return nil, "missing required argment -- " .. name 378 | end 379 | return content 380 | end 381 | 382 | local function _build_xml_table(sndmsg, fmts, resultable) 383 | local count = #resultable 384 | for i = 1, #fmts do 385 | local fmt = fmts[i] 386 | local name = fmt[1] 387 | local nodetype = fmt[2] 388 | local subfmts = fmt[3] 389 | local content, err 390 | 391 | if nodetype == elementnode then 392 | content = {} 393 | err = _build_xml_table(sndmsg, subfmts, content) 394 | if err then 395 | return err 396 | end 397 | 398 | if #content ~= 0 then 399 | count = count + 1 400 | resultable[count] = string_format("<%s>", name) 401 | 402 | for i, v in ipairs(content) do 403 | count = count + 1 404 | resultable[count] = v 405 | end 406 | 407 | count = count + 1 408 | resultable[count] = string_format("", name) 409 | end 410 | 411 | elseif nodetype == textnode then 412 | content, err = _retrieve_content(sndmsg, fmt) 413 | if err then 414 | return err 415 | end 416 | 417 | if content ~= "" then 418 | resultable[count + 1] = string_format("<%s>", name) 419 | resultable[count + 2] = content 420 | resultable[count + 3] = string_format("", name) 421 | count = count + 3 422 | end 423 | 424 | elseif nodetype == cdatanode then 425 | content, err = _retrieve_content(sndmsg, fmt) 426 | if err then 427 | return err 428 | end 429 | 430 | if content ~= "" then 431 | resultable[count + 1] = string_format("<%s>", name) 432 | resultable[count + 2] = string_format("", content) 433 | resultable[count + 3] = string_format("", name) 434 | count = count + 3 435 | end 436 | end 437 | end 438 | 439 | return nil 440 | end 441 | 442 | local function _build_response_body(rcvmsg, sndmsg) 443 | local msgtype = sndmsg.msgtype 444 | local fmts = sndmsgfmt[msgtype] 445 | local n = tonumber(sndmsg.articlecount or 0) 446 | local xmltable, stream, err 447 | 448 | n = n > 10 and 10 or n 449 | 450 | if not fmts then 451 | return nil, "invalid msgtype" 452 | end 453 | 454 | if not rcvmsg.fromusername or not rcvmsg.tousername then 455 | return nil, "invalid recieve message" 456 | end 457 | 458 | sndmsg.tousername = rcvmsg.fromusername 459 | sndmsg.fromusername = rcvmsg.tousername 460 | sndmsg.createtime = tostring(os.time()) 461 | 462 | if n > 1 then 463 | _insert_items(n) 464 | end 465 | 466 | xmltable = { "" } 467 | err = _build_xml_table(sndmsg, sndmsgfmt.common, xmltable) 468 | if err then 469 | return nil, err 470 | end 471 | err = _build_xml_table(sndmsg, fmts, xmltable) 472 | if err then 473 | return nil, err 474 | end 475 | xmltable[#xmltable + 1] = "" 476 | stream = table_concat(xmltable) 477 | 478 | if n > 1 then 479 | stream = _normalize_items(stream) 480 | _cleanup_items(n) 481 | end 482 | 483 | -- parametric by rcvmsg 484 | stream = string_gsub(stream, "%${([^}]+)}", rcvmsg) 485 | 486 | return stream 487 | end 488 | 489 | --------------------------------------------------public methods 490 | 491 | local mt = { 492 | __call = function(_) 493 | -- request arguments 494 | local args = ngx_req.get_uri_args() 495 | -- request method 496 | local method = ngx_req.get_method() 497 | -- request body 498 | ngx_req.read_body() 499 | local body = ngx_req.get_body_data() 500 | if body then 501 | body = ngx_re_gsub(body, "[\r\n]*", "", "i") 502 | end 503 | 504 | local params = { 505 | signature = args.signature, 506 | timestamp = args.timestamp, 507 | nonce = args.nonce, 508 | echostr = args.echostr, 509 | token = wechat_config.token, 510 | method = method, 511 | body = body, 512 | rcvmsg = {} 513 | } 514 | 515 | local ok, err, rcvmsg, reply, sndmsg, res 516 | ok, err = _verify_request_params(params) 517 | if not ok then 518 | ngx_log(ngx.ERR, "failed to verify server: ", err) 519 | return ngx_exit(ngx.HTTP_BAD_REQUEST) 520 | end 521 | 522 | if params.method == "GET" then -- just verify 523 | ngx_print(params.echostr) 524 | return ngx_exit(ngx.HTTP_OK) 525 | end 526 | 527 | rcvmsg, err = _parse_request_body(params) 528 | if err then 529 | ngx_log(ngx.ERR, "failed to parse message: ", err) 530 | return ngx_exit(ngx.HTTP_BAD_REQUEST) 531 | end 532 | 533 | reply, continue = _match_auto_reply(rcvmsg) 534 | if reply then 535 | sndmsg, err = _build_response_body(rcvmsg, reply) 536 | if err then 537 | ngx_log(ngx.ERR, "failed to build message: ", err) 538 | return ngx_exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 539 | end 540 | ngx_print(sndmsg) 541 | if not continue then return ngx_exit(ngx.HTTP_OK) end 542 | end 543 | 544 | if wechat_config.autoreplyurl and wechat_config.autoreplyurl ~= "" then 545 | res, err = require("resty.wechat.utils.http").new():request_uri(wechat_config.autoreplyurl, { 546 | method = "POST", body = cjson.encode(rcvmsg), 547 | headers = { ["Content-Type"] = "application/json" }, 548 | }) 549 | if not res or err or tostring(res.status) ~= "200" then 550 | ngx_log(ngx.ERR, "failed to request auto reply URL ", wechat_config.autoreplyurl, ": ", err or tostring(res.status)) 551 | return ngx_exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 552 | end 553 | 554 | if not reply and res.body and res.body ~= "" and string_lower(res.body) ~= "success" then 555 | sndmsg, err = _build_response_body(rcvmsg, cjson.decode(res.body)) 556 | if err then 557 | ngx_log(ngx.ERR, "failed to build message: ", err) 558 | return ngx_exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 559 | end 560 | ngx_print(sndmsg) 561 | return ngx_exit(res.status) 562 | end 563 | end 564 | 565 | if not reply then ngx_print("success") end 566 | return ngx_exit(ngx.HTTP_OK) 567 | end, 568 | } 569 | 570 | return setmetatable(_M, mt) 571 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/aes.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_aes" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | local mt = { __index = _M } 5 | 6 | local resty_aes = require("resty.aes") 7 | 8 | function _M.new(key) 9 | if not key or #key ~= 16 then 10 | return nil, "bad key length: need be 16" 11 | end 12 | return resty_aes:new(key, nil, resty_aes.cipher(128, "ecb"), {iv=key}) 13 | end 14 | 15 | return _M 16 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/base62.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_basex62" 2 | _G[modname] = require("resty.wechat.utils.basex")("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 3 | 4 | return _G[modname] 5 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/basex.lua: -------------------------------------------------------------------------------- 1 | -- lua-basex 2 | -- version 0.1.1 3 | -- un.def, 2016 4 | 5 | local basex, alphabets, basex_meta, basex_instance_meta 6 | 7 | local math_floor = math.floor 8 | local table_insert = table.insert 9 | local table_concat = table.concat 10 | local string_char = string.char 11 | 12 | alphabets = { 13 | BASE16LOWER = '0123456789abcdef', 14 | BASE16UPPER = '0123456789ABCDEF', 15 | BASE58BITCOIN = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', 16 | BASE58FLICKR = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', 17 | BASE58RIPPLE = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz', 18 | } 19 | 20 | basex = { 21 | _VERSION = 'lua-basex 0.1.0', 22 | _URL = 'https://github.com/un-def/lua-basex', 23 | _DESCRIPTION = 'Base encoding/decoding of any given alphabet ' .. 24 | 'using bitcoin style leading zero compression', 25 | _LICENSE = [[ 26 | Copyright (c) 2016, un.def 27 | All rights reserved. 28 | 29 | Redistribution and use in source and binary forms, with or without 30 | modification, are permitted provided that the following conditions are met: 31 | 32 | * Redistributions of source code must retain the above copyright notice, 33 | this list of conditions and the following disclaimer. 34 | * Redistributions in binary form must reproduce the above copyright 35 | notice, this list of conditions and the following disclaimer in the 36 | documentation and/or other materials provided with the distribution. 37 | 38 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY 39 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 40 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 41 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 42 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 43 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 44 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 45 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 46 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 47 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 48 | DAMAGE. 49 | ]], 50 | alphabets = alphabets, 51 | } 52 | 53 | 54 | basex_meta = { 55 | __call = function(_, alphabet) 56 | local alphabet_map = {} 57 | local base = #alphabet 58 | local leader = alphabet:sub(1, 1) 59 | for i = 1, base do 60 | alphabet_map[alphabet:sub(i, i)] = i - 1 61 | end 62 | local basex_instance = { 63 | alphabet = alphabet, 64 | base = base, 65 | leader = leader, 66 | alphabet_map = alphabet_map, 67 | } 68 | return setmetatable(basex_instance, basex_instance_meta) 69 | end, 70 | 71 | __index = function(cls, key) 72 | local alphabet = cls.alphabets[key:upper()] 73 | if not alphabet then return nil end 74 | local basex_instance = cls(alphabet) 75 | cls[key] = basex_instance 76 | return basex_instance 77 | end 78 | } 79 | 80 | 81 | basex_instance_meta = { 82 | __index = { 83 | encode = function(self, source) 84 | if #source == 0 then return '' end 85 | 86 | local digits = {0} 87 | 88 | for i = 1, #source do 89 | local carry = source:byte(i, i) 90 | for j = 1, #digits do 91 | carry = carry + digits[j] * 256 92 | digits[j] = carry % self.base 93 | carry = math_floor(carry / self.base) 94 | end 95 | while carry > 0 do 96 | table_insert(digits, carry % self.base) 97 | carry = math_floor(carry / self.base) 98 | end 99 | end 100 | 101 | for k = 1, #source-1 do 102 | if source:byte(k, k) ~= 0 then break end 103 | table_insert(digits, 0) 104 | end 105 | 106 | local ii = 1 107 | local jj = #digits 108 | local tmp 109 | while true do 110 | tmp = self.alphabet:sub(digits[ii]+1, digits[ii]+1) 111 | digits[ii] = self.alphabet:sub(digits[jj]+1, digits[jj]+1) 112 | digits[jj] = tmp 113 | ii = ii + 1 114 | jj = jj - 1 115 | if ii > jj then break end 116 | end 117 | 118 | return table_concat(digits) 119 | end, 120 | 121 | decode = function(self, str) 122 | if #str == 0 then return '' end 123 | 124 | local bytes = {0} 125 | 126 | local value, carry 127 | for i = 1, #str do 128 | value = self.alphabet_map[str:sub(i, i)] 129 | if value == nil then error('Non-base' .. self.base .. ' character') end 130 | carry = value 131 | for j = 1, #bytes do 132 | carry = carry + bytes[j] * self.base 133 | bytes[j] = carry % 256 134 | carry = math_floor(carry / 256) 135 | end 136 | while carry > 0 do 137 | table_insert(bytes, carry % 256) 138 | carry = math_floor(carry / 256) 139 | end 140 | end 141 | 142 | for k = 1, #str-1 do 143 | if str:sub(k, k) ~= self.leader then break end 144 | table_insert(bytes, 0) 145 | end 146 | 147 | local decoded = '' 148 | for i = #bytes, 1, -1 do 149 | decoded = decoded .. string_char(bytes[i]) 150 | end 151 | 152 | return decoded 153 | end 154 | } 155 | } 156 | 157 | 158 | return setmetatable(basex, basex_meta) 159 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/cookie.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_cookie" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | function _M.get(key) 6 | return ngx.var["cookie_" .. key] 7 | end 8 | 9 | function _M.set(opt) 10 | local conf = { 11 | key = opt and opt.key or nil, 12 | value = opt and opt.value or nil, 13 | domain = opt and opt.domain or nil, 14 | path = opt and opt.path or nil, 15 | expires = opt and opt.expires or nil 16 | } 17 | if not conf.key or not conf.value then return end 18 | 19 | local cookies = ngx.header.Set_Cookie 20 | if not cookies then cookies = {} end 21 | if type(cookies) ~= "table" then cookies = {cookies} end 22 | 23 | local cookie = conf.key .. "=" .. conf.value .. ";" 24 | if conf.domain then cookie = cookie .. " domain=" .. conf.domain .. ";" end 25 | if conf.path then cookie = cookie .. " path=" .. conf.path .. ";" end 26 | if conf.expires then cookie = cookie .. " expires=" .. conf.expires .. ";" end 27 | 28 | table.insert(cookies, cookie) 29 | ngx.header.Set_Cookie = cookies 30 | end 31 | 32 | return _M 33 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/hex.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_hex" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local ffi = require "ffi" 6 | local str_type = ffi.typeof("uint8_t[?]") 7 | local ffi_str = ffi.string 8 | 9 | ffi.cdef[[ 10 | typedef unsigned char u_char; 11 | u_char * ngx_hex_dump(u_char *dst, const u_char *src, size_t len); 12 | ]] 13 | 14 | local mt = { 15 | __call = function(_, s) 16 | local len = #s * 2 17 | local buf = ffi.new(str_type, len) 18 | ffi.C.ngx_hex_dump(buf, s, #s) 19 | return ffi_str(buf, len) 20 | end 21 | } 22 | 23 | return setmetatable(_M, mt) 24 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/http.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Copyright (c) 2013, James Hurst 3 | -- All rights reserved. 4 | -- 5 | -- Redistribution and use in source and binary forms, with or without modification, 6 | -- are permitted provided that the following conditions are met: 7 | -- 8 | -- Redistributions of source code must retain the above copyright notice, this 9 | -- list of conditions and the following disclaimer. 10 | -- 11 | -- Redistributions in binary form must reproduce the above copyright notice, this 12 | -- list of conditions and the following disclaimer in the documentation and/or 13 | -- other materials provided with the distribution. 14 | -- 15 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | -- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | -- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | -- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | -- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | -- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | -- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | -- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -- 26 | 27 | local http_headers = require "resty.wechat.utils.http_headers" 28 | 29 | local ngx_socket_tcp = ngx.socket.tcp 30 | local ngx_req = ngx.req 31 | local ngx_req_socket = ngx_req.socket 32 | local ngx_req_get_headers = ngx_req.get_headers 33 | local ngx_req_get_method = ngx_req.get_method 34 | local str_gmatch = string.gmatch 35 | local str_lower = string.lower 36 | local str_upper = string.upper 37 | local str_find = string.find 38 | local str_sub = string.sub 39 | local tbl_concat = table.concat 40 | local tbl_insert = table.insert 41 | local ngx_encode_args = ngx.encode_args 42 | local ngx_re_match = ngx.re.match 43 | local ngx_re_gsub = ngx.re.gsub 44 | local ngx_re_find = ngx.re.find 45 | local ngx_log = ngx.log 46 | local ngx_DEBUG = ngx.DEBUG 47 | local ngx_ERR = ngx.ERR 48 | local ngx_var = ngx.var 49 | local ngx_print = ngx.print 50 | local co_yield = coroutine.yield 51 | local co_create = coroutine.create 52 | local co_status = coroutine.status 53 | local co_resume = coroutine.resume 54 | local setmetatable = setmetatable 55 | local tonumber = tonumber 56 | local tostring = tostring 57 | local unpack = unpack 58 | local rawget = rawget 59 | local select = select 60 | local ipairs = ipairs 61 | local pairs = pairs 62 | local pcall = pcall 63 | local type = type 64 | 65 | 66 | -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 67 | local HOP_BY_HOP_HEADERS = { 68 | ["connection"] = true, 69 | ["keep-alive"] = true, 70 | ["proxy-authenticate"] = true, 71 | ["proxy-authorization"] = true, 72 | ["te"] = true, 73 | ["trailers"] = true, 74 | ["transfer-encoding"] = true, 75 | ["upgrade"] = true, 76 | ["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal 77 | -- with this (may send chunked for example). 78 | } 79 | 80 | 81 | -- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot 82 | -- be resumed. This protects user code from inifite loops when doing things like 83 | -- repeat 84 | -- local chunk, err = res.body_reader() 85 | -- if chunk then -- <-- This could be a string msg in the core wrap function. 86 | -- ... 87 | -- end 88 | -- until not chunk 89 | local co_wrap = function(func) 90 | local co = co_create(func) 91 | if not co then 92 | return nil, "could not create coroutine" 93 | else 94 | return function(...) 95 | if co_status(co) == "suspended" then 96 | return select(2, co_resume(co, ...)) 97 | else 98 | return nil, "can't resume a " .. co_status(co) .. " coroutine" 99 | end 100 | end 101 | end 102 | end 103 | 104 | 105 | local _M = { 106 | _VERSION = '0.10', 107 | } 108 | _M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version 109 | 110 | local mt = { __index = _M } 111 | 112 | 113 | local HTTP = { 114 | [1.0] = " HTTP/1.0\r\n", 115 | [1.1] = " HTTP/1.1\r\n", 116 | } 117 | 118 | local DEFAULT_PARAMS = { 119 | method = "GET", 120 | path = "/", 121 | version = 1.1, 122 | } 123 | 124 | 125 | function _M.new(self) 126 | local sock, err = ngx_socket_tcp() 127 | if not sock then 128 | return nil, err 129 | end 130 | return setmetatable({ sock = sock, keepalive = true }, mt) 131 | end 132 | 133 | 134 | function _M.set_timeout(self, timeout) 135 | local sock = self.sock 136 | if not sock then 137 | return nil, "not initialized" 138 | end 139 | 140 | return sock:settimeout(timeout) 141 | end 142 | 143 | 144 | function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout) 145 | local sock = self.sock 146 | if not sock then 147 | return nil, "not initialized" 148 | end 149 | 150 | return sock:settimeouts(connect_timeout, send_timeout, read_timeout) 151 | end 152 | 153 | 154 | function _M.ssl_handshake(self, ...) 155 | local sock = self.sock 156 | if not sock then 157 | return nil, "not initialized" 158 | end 159 | 160 | self.ssl = true 161 | 162 | return sock:sslhandshake(...) 163 | end 164 | 165 | 166 | function _M.connect(self, ...) 167 | local sock = self.sock 168 | if not sock then 169 | return nil, "not initialized" 170 | end 171 | 172 | self.host = select(1, ...) 173 | self.port = select(2, ...) 174 | 175 | -- If port is not a number, this is likely a unix domain socket connection. 176 | if type(self.port) ~= "number" then 177 | self.port = nil 178 | end 179 | 180 | self.keepalive = true 181 | 182 | return sock:connect(...) 183 | end 184 | 185 | 186 | function _M.set_keepalive(self, ...) 187 | local sock = self.sock 188 | if not sock then 189 | return nil, "not initialized" 190 | end 191 | 192 | if self.keepalive == true then 193 | return sock:setkeepalive(...) 194 | else 195 | -- The server said we must close the connection, so we cannot setkeepalive. 196 | -- If close() succeeds we return 2 instead of 1, to differentiate between 197 | -- a normal setkeepalive() failure and an intentional close(). 198 | local res, err = sock:close() 199 | if res then 200 | return 2, "connection must be closed" 201 | else 202 | return res, err 203 | end 204 | end 205 | end 206 | 207 | 208 | function _M.get_reused_times(self) 209 | local sock = self.sock 210 | if not sock then 211 | return nil, "not initialized" 212 | end 213 | 214 | return sock:getreusedtimes() 215 | end 216 | 217 | 218 | function _M.close(self) 219 | local sock = self.sock 220 | if not sock then 221 | return nil, "not initialized" 222 | end 223 | 224 | return sock:close() 225 | end 226 | 227 | 228 | local function _should_receive_body(method, code) 229 | if method == "HEAD" then return nil end 230 | if code == 204 or code == 304 then return nil end 231 | if code >= 100 and code < 200 then return nil end 232 | return true 233 | end 234 | 235 | 236 | function _M.parse_uri(self, uri, query_in_path) 237 | if query_in_path == nil then query_in_path = true end 238 | 239 | local m, err = ngx_re_match(uri, [[^(?:(http[s]?):)?//([^:/\?]+)(?::(\d+))?([^\?]*)\??(.*)]], "jo") 240 | 241 | if not m then 242 | if err then 243 | return nil, "failed to match the uri: " .. uri .. ", " .. err 244 | end 245 | 246 | return nil, "bad uri: " .. uri 247 | else 248 | -- If the URI is schemaless (i.e. //example.com) try to use our current 249 | -- request scheme. 250 | if not m[1] then 251 | local scheme = ngx.var.scheme 252 | if scheme == "http" or scheme == "https" then 253 | m[1] = scheme 254 | else 255 | return nil, "schemaless URIs require a request context: " .. uri 256 | end 257 | end 258 | 259 | if m[3] then 260 | m[3] = tonumber(m[3]) 261 | else 262 | if m[1] == "https" then 263 | m[3] = 443 264 | else 265 | m[3] = 80 266 | end 267 | end 268 | if not m[4] or "" == m[4] then m[4] = "/" end 269 | 270 | if query_in_path and m[5] and m[5] ~= "" then 271 | m[4] = m[4] .. "?" .. m[5] 272 | m[5] = nil 273 | end 274 | 275 | return m, nil 276 | end 277 | end 278 | 279 | 280 | local function _format_request(params) 281 | local version = params.version 282 | local headers = params.headers or {} 283 | 284 | local query = params.query or "" 285 | if type(query) == "table" then 286 | query = "?" .. ngx_encode_args(query) 287 | elseif query ~= "" and str_sub(query, 1, 1) ~= "?" then 288 | query = "?" .. query 289 | end 290 | 291 | -- Initialize request 292 | local req = { 293 | str_upper(params.method), 294 | " ", 295 | params.path, 296 | query, 297 | HTTP[version], 298 | -- Pre-allocate slots for minimum headers and carriage return. 299 | true, 300 | true, 301 | true, 302 | } 303 | local c = 6 -- req table index it's faster to do this inline vs table.insert 304 | 305 | -- Append headers 306 | for key, values in pairs(headers) do 307 | if type(values) ~= "table" then 308 | values = {values} 309 | end 310 | 311 | key = tostring(key) 312 | for _, value in pairs(values) do 313 | req[c] = key .. ": " .. tostring(value) .. "\r\n" 314 | c = c + 1 315 | end 316 | end 317 | 318 | -- Close headers 319 | req[c] = "\r\n" 320 | 321 | return tbl_concat(req) 322 | end 323 | 324 | 325 | local function _receive_status(sock) 326 | local line, err = sock:receive("*l") 327 | if not line then 328 | return nil, nil, nil, err 329 | end 330 | 331 | return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8)), str_sub(line, 14) 332 | end 333 | 334 | 335 | local function _receive_headers(sock) 336 | local headers = http_headers.new() 337 | 338 | repeat 339 | local line, err = sock:receive("*l") 340 | if not line then 341 | return nil, err 342 | end 343 | 344 | local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.+)", "jo") 345 | if not m then 346 | break 347 | end 348 | 349 | local key = m[1] 350 | local val = m[2] 351 | if headers[key] then 352 | if type(headers[key]) ~= "table" then 353 | headers[key] = { headers[key] } 354 | end 355 | tbl_insert(headers[key], tostring(val)) 356 | else 357 | headers[key] = tostring(val) 358 | end 359 | until ngx_re_find(line, "^\\s*$", "jo") 360 | 361 | return headers, nil 362 | end 363 | 364 | 365 | local function _chunked_body_reader(sock, default_chunk_size) 366 | return co_wrap(function(max_chunk_size) 367 | local remaining = 0 368 | local length 369 | max_chunk_size = max_chunk_size or default_chunk_size 370 | 371 | repeat 372 | -- If we still have data on this chunk 373 | if max_chunk_size and remaining > 0 then 374 | 375 | if remaining > max_chunk_size then 376 | -- Consume up to max_chunk_size 377 | length = max_chunk_size 378 | remaining = remaining - max_chunk_size 379 | else 380 | -- Consume all remaining 381 | length = remaining 382 | remaining = 0 383 | end 384 | else -- This is a fresh chunk 385 | 386 | -- Receive the chunk size 387 | local str, err = sock:receive("*l") 388 | if not str then 389 | co_yield(nil, err) 390 | end 391 | 392 | length = tonumber(str, 16) 393 | 394 | if not length then 395 | co_yield(nil, "unable to read chunksize") 396 | end 397 | 398 | if max_chunk_size and length > max_chunk_size then 399 | -- Consume up to max_chunk_size 400 | remaining = length - max_chunk_size 401 | length = max_chunk_size 402 | end 403 | end 404 | 405 | if length > 0 then 406 | local str, err = sock:receive(length) 407 | if not str then 408 | co_yield(nil, err) 409 | end 410 | 411 | max_chunk_size = co_yield(str) or default_chunk_size 412 | 413 | -- If we're finished with this chunk, read the carriage return. 414 | if remaining == 0 then 415 | sock:receive(2) -- read \r\n 416 | end 417 | else 418 | -- Read the last (zero length) chunk's carriage return 419 | sock:receive(2) -- read \r\n 420 | end 421 | 422 | until length == 0 423 | end) 424 | end 425 | 426 | 427 | local function _body_reader(sock, content_length, default_chunk_size) 428 | return co_wrap(function(max_chunk_size) 429 | max_chunk_size = max_chunk_size or default_chunk_size 430 | 431 | if not content_length and max_chunk_size then 432 | -- We have no length, but wish to stream. 433 | -- HTTP 1.0 with no length will close connection, so read chunks to the end. 434 | repeat 435 | local str, err, partial = sock:receive(max_chunk_size) 436 | if not str and err == "closed" then 437 | co_yield(partial, err) 438 | end 439 | 440 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 441 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 442 | 443 | if not max_chunk_size then 444 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 445 | break 446 | end 447 | until not str 448 | 449 | elseif not content_length then 450 | -- We have no length but don't wish to stream. 451 | -- HTTP 1.0 with no length will close connection, so read to the end. 452 | co_yield(sock:receive("*a")) 453 | 454 | elseif not max_chunk_size then 455 | -- We have a length and potentially keep-alive, but want everything. 456 | co_yield(sock:receive(content_length)) 457 | 458 | else 459 | -- We have a length and potentially a keep-alive, and wish to stream 460 | -- the response. 461 | local received = 0 462 | repeat 463 | local length = max_chunk_size 464 | if received + length > content_length then 465 | length = content_length - received 466 | end 467 | 468 | if length > 0 then 469 | local str, err = sock:receive(length) 470 | if not str then 471 | co_yield(nil, err) 472 | end 473 | received = received + length 474 | 475 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 476 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 477 | 478 | if not max_chunk_size then 479 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 480 | break 481 | end 482 | end 483 | 484 | until length == 0 485 | end 486 | end) 487 | end 488 | 489 | 490 | local function _no_body_reader() 491 | return nil 492 | end 493 | 494 | 495 | local function _read_body(res) 496 | local reader = res.body_reader 497 | 498 | if not reader then 499 | -- Most likely HEAD or 304 etc. 500 | return nil, "no body to be read" 501 | end 502 | 503 | local chunks = {} 504 | local c = 1 505 | 506 | local chunk, err 507 | repeat 508 | chunk, err = reader() 509 | 510 | if err then 511 | return nil, err, tbl_concat(chunks) -- Return any data so far. 512 | end 513 | if chunk then 514 | chunks[c] = chunk 515 | c = c + 1 516 | end 517 | until not chunk 518 | 519 | return tbl_concat(chunks) 520 | end 521 | 522 | 523 | local function _trailer_reader(sock) 524 | return co_wrap(function() 525 | co_yield(_receive_headers(sock)) 526 | end) 527 | end 528 | 529 | 530 | local function _read_trailers(res) 531 | local reader = res.trailer_reader 532 | if not reader then 533 | return nil, "no trailers" 534 | end 535 | 536 | local trailers = reader() 537 | setmetatable(res.headers, { __index = trailers }) 538 | end 539 | 540 | 541 | local function _send_body(sock, body) 542 | if type(body) == 'function' then 543 | repeat 544 | local chunk, err, partial = body() 545 | 546 | if chunk then 547 | local ok, err = sock:send(chunk) 548 | 549 | if not ok then 550 | return nil, err 551 | end 552 | elseif err ~= nil then 553 | return nil, err, partial 554 | end 555 | 556 | until chunk == nil 557 | elseif body ~= nil then 558 | local bytes, err = sock:send(body) 559 | 560 | if not bytes then 561 | return nil, err 562 | end 563 | end 564 | return true, nil 565 | end 566 | 567 | 568 | local function _handle_continue(sock, body) 569 | local status, version, reason, err = _receive_status(sock) 570 | if not status then 571 | return nil, nil, err 572 | end 573 | 574 | -- Only send body if we receive a 100 Continue 575 | if status == 100 then 576 | local ok, err = sock:receive("*l") -- Read carriage return 577 | if not ok then 578 | return nil, nil, err 579 | end 580 | _send_body(sock, body) 581 | end 582 | return status, version, err 583 | end 584 | 585 | 586 | function _M.send_request(self, params) 587 | -- Apply defaults 588 | setmetatable(params, { __index = DEFAULT_PARAMS }) 589 | 590 | local sock = self.sock 591 | local body = params.body 592 | local headers = http_headers.new() 593 | 594 | local params_headers = params.headers 595 | if params_headers then 596 | -- We assign one by one so that the metatable can handle case insensitivity 597 | -- for us. You can blame the spec for this inefficiency. 598 | for k,v in pairs(params_headers) do 599 | headers[k] = v 600 | end 601 | end 602 | 603 | -- Ensure minimal headers are set 604 | if type(body) == 'string' and not headers["Content-Length"] then 605 | headers["Content-Length"] = #body 606 | end 607 | if not headers["Host"] then 608 | if (str_sub(self.host, 1, 5) == "unix:") then 609 | return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one." 610 | end 611 | -- If we have a port (i.e. not connected to a unix domain socket), and this 612 | -- port is non-standard, append it to the Host heaer. 613 | if self.port then 614 | if self.ssl and self.port ~= 443 then 615 | headers["Host"] = self.host .. ":" .. self.port 616 | elseif not self.ssl and self.port ~= 80 then 617 | headers["Host"] = self.host .. ":" .. self.port 618 | else 619 | headers["Host"] = self.host 620 | end 621 | else 622 | headers["Host"] = self.host 623 | end 624 | end 625 | if not headers["User-Agent"] then 626 | headers["User-Agent"] = _M._USER_AGENT 627 | end 628 | if params.version == 1.0 and not headers["Connection"] then 629 | headers["Connection"] = "Keep-Alive" 630 | end 631 | 632 | params.headers = headers 633 | 634 | -- Format and send request 635 | local req = _format_request(params) 636 | ngx_log(ngx_DEBUG, "\n", req) 637 | local bytes, err = sock:send(req) 638 | 639 | if not bytes then 640 | return nil, err 641 | end 642 | 643 | -- Send the request body, unless we expect: continue, in which case 644 | -- we handle this as part of reading the response. 645 | if headers["Expect"] ~= "100-continue" then 646 | local ok, err, partial = _send_body(sock, body) 647 | if not ok then 648 | return nil, err, partial 649 | end 650 | end 651 | 652 | return true 653 | end 654 | 655 | 656 | function _M.read_response(self, params) 657 | local sock = self.sock 658 | 659 | local status, version, reason, err 660 | 661 | -- If we expect: continue, we need to handle this, sending the body if allowed. 662 | -- If we don't get 100 back, then status is the actual status. 663 | if params.headers["Expect"] == "100-continue" then 664 | local _status, _version, _err = _handle_continue(sock, params.body) 665 | if not _status then 666 | return nil, _err 667 | elseif _status ~= 100 then 668 | status, version, err = _status, _version, _err 669 | end 670 | end 671 | 672 | -- Just read the status as normal. 673 | if not status then 674 | status, version, reason, err = _receive_status(sock) 675 | if not status then 676 | return nil, err 677 | end 678 | end 679 | 680 | 681 | local res_headers, err = _receive_headers(sock) 682 | if not res_headers then 683 | return nil, err 684 | end 685 | 686 | -- keepalive is true by default. Determine if this is correct or not. 687 | local ok, connection = pcall(str_lower, res_headers["Connection"]) 688 | if ok then 689 | if (version == 1.1 and connection == "close") or 690 | (version == 1.0 and connection ~= "keep-alive") then 691 | self.keepalive = false 692 | end 693 | else 694 | -- no connection header 695 | if version == 1.0 then 696 | self.keepalive = false 697 | end 698 | end 699 | 700 | local body_reader = _no_body_reader 701 | local trailer_reader, err 702 | local has_body = false 703 | 704 | -- Receive the body_reader 705 | if _should_receive_body(params.method, status) then 706 | local ok, encoding = pcall(str_lower, res_headers["Transfer-Encoding"]) 707 | if ok and version == 1.1 and encoding == "chunked" then 708 | body_reader, err = _chunked_body_reader(sock) 709 | has_body = true 710 | else 711 | 712 | local ok, length = pcall(tonumber, res_headers["Content-Length"]) 713 | if ok then 714 | body_reader, err = _body_reader(sock, length) 715 | has_body = true 716 | end 717 | end 718 | end 719 | 720 | if res_headers["Trailer"] then 721 | trailer_reader, err = _trailer_reader(sock) 722 | end 723 | 724 | if err then 725 | return nil, err 726 | else 727 | return { 728 | status = status, 729 | reason = reason, 730 | headers = res_headers, 731 | has_body = has_body, 732 | body_reader = body_reader, 733 | read_body = _read_body, 734 | trailer_reader = trailer_reader, 735 | read_trailers = _read_trailers, 736 | } 737 | end 738 | end 739 | 740 | 741 | function _M.request(self, params) 742 | local res, err = self:send_request(params) 743 | if not res then 744 | return res, err 745 | else 746 | return self:read_response(params) 747 | end 748 | end 749 | 750 | 751 | function _M.request_pipeline(self, requests) 752 | for _, params in ipairs(requests) do 753 | if params.headers and params.headers["Expect"] == "100-continue" then 754 | return nil, "Cannot pipeline request specifying Expect: 100-continue" 755 | end 756 | 757 | local res, err = self:send_request(params) 758 | if not res then 759 | return res, err 760 | end 761 | end 762 | 763 | local responses = {} 764 | for i, params in ipairs(requests) do 765 | responses[i] = setmetatable({ 766 | params = params, 767 | response_read = false, 768 | }, { 769 | -- Read each actual response lazily, at the point the user tries 770 | -- to access any of the fields. 771 | __index = function(t, k) 772 | local res, err 773 | if t.response_read == false then 774 | res, err = _M.read_response(self, t.params) 775 | t.response_read = true 776 | 777 | if not res then 778 | ngx_log(ngx_ERR, err) 779 | else 780 | for rk, rv in pairs(res) do 781 | t[rk] = rv 782 | end 783 | end 784 | end 785 | return rawget(t, k) 786 | end, 787 | }) 788 | end 789 | return responses 790 | end 791 | 792 | 793 | function _M.request_uri(self, uri, params) 794 | if not params then params = {} end 795 | 796 | local parsed_uri, err = self:parse_uri(uri, false) 797 | if not parsed_uri then 798 | return nil, err 799 | end 800 | 801 | local scheme, host, port, path, query = unpack(parsed_uri) 802 | if not params.path then params.path = path end 803 | if not params.query then params.query = query end 804 | 805 | local c, err = self:connect(host, port) 806 | if not c then 807 | return nil, err 808 | end 809 | 810 | if scheme == "https" then 811 | local verify = true 812 | if params.ssl_verify == false then 813 | verify = false 814 | end 815 | local ok, err = self:ssl_handshake(nil, host, verify) 816 | if not ok then 817 | return nil, err 818 | end 819 | end 820 | 821 | local res, err = self:request(params) 822 | if not res then 823 | return nil, err 824 | end 825 | 826 | local body, err = res:read_body() 827 | if not body then 828 | return nil, err 829 | end 830 | 831 | res.body = body 832 | 833 | local ok, err = self:set_keepalive() 834 | if not ok then 835 | ngx_log(ngx_ERR, err) 836 | end 837 | 838 | return res, nil 839 | end 840 | 841 | 842 | function _M.get_client_body_reader(self, chunksize, sock) 843 | chunksize = chunksize or 65536 844 | 845 | if not sock then 846 | local ok, err 847 | ok, sock, err = pcall(ngx_req_socket) 848 | 849 | if not ok then 850 | return nil, sock -- pcall err 851 | end 852 | 853 | if not sock then 854 | if err == "no body" then 855 | return nil 856 | else 857 | return nil, err 858 | end 859 | end 860 | end 861 | 862 | local headers = ngx_req_get_headers() 863 | local length = headers.content_length 864 | local encoding = headers.transfer_encoding 865 | if length then 866 | return _body_reader(sock, tonumber(length), chunksize) 867 | elseif encoding and str_lower(encoding) == 'chunked' then 868 | -- Not yet supported by ngx_lua but should just work... 869 | return _chunked_body_reader(sock, chunksize) 870 | else 871 | return nil 872 | end 873 | end 874 | 875 | 876 | function _M.proxy_request(self, chunksize) 877 | return self:request{ 878 | method = ngx_req_get_method(), 879 | path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""), 880 | body = self:get_client_body_reader(chunksize), 881 | headers = ngx_req_get_headers(), 882 | } 883 | end 884 | 885 | 886 | function _M.proxy_response(self, response, chunksize) 887 | if not response then 888 | ngx_log(ngx_ERR, "no response provided") 889 | return 890 | end 891 | 892 | ngx.status = response.status 893 | 894 | -- Filter out hop-by-hop headeres 895 | for k,v in pairs(response.headers) do 896 | if not HOP_BY_HOP_HEADERS[str_lower(k)] then 897 | ngx.header[k] = v 898 | end 899 | end 900 | 901 | local reader = response.body_reader 902 | repeat 903 | local chunk, err = reader(chunksize) 904 | if err then 905 | ngx_log(ngx_ERR, err) 906 | break 907 | end 908 | 909 | if chunk then 910 | local res, err = ngx_print(chunk) 911 | if not res then 912 | ngx_log(ngx_ERR, err) 913 | break 914 | end 915 | end 916 | until not chunk 917 | end 918 | 919 | 920 | return _M 921 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/http_headers.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Copyright (c) 2013, James Hurst 3 | -- All rights reserved. 4 | -- 5 | -- Redistribution and use in source and binary forms, with or without modification, 6 | -- are permitted provided that the following conditions are met: 7 | -- 8 | -- Redistributions of source code must retain the above copyright notice, this 9 | -- list of conditions and the following disclaimer. 10 | -- 11 | -- Redistributions in binary form must reproduce the above copyright notice, this 12 | -- list of conditions and the following disclaimer in the documentation and/or 13 | -- other materials provided with the distribution. 14 | -- 15 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | -- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | -- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | -- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | -- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | -- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | -- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | -- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -- 26 | 27 | local rawget, rawset, setmetatable = 28 | rawget, rawset, setmetatable 29 | 30 | local str_find, str_lower, str_sub = 31 | string.find, string.lower, string.sub 32 | 33 | 34 | local _M = { 35 | _VERSION = '0.10', 36 | } 37 | 38 | 39 | local function hyphenate(k) 40 | local k_hyphened = "" 41 | local match = false 42 | local prev_pos = 0 43 | 44 | repeat 45 | local pos = str_find(k, "_", prev_pos, true) 46 | if pos then 47 | match = true 48 | k_hyphened = k_hyphened .. str_sub(k, prev_pos, pos - 1) .. "-" 49 | elseif match == false then 50 | -- Didn't find an underscore and first check 51 | return k 52 | else 53 | -- No more underscores, append the rest of the key 54 | k_hyphened = k_hyphened .. str_sub(k, prev_pos) 55 | break 56 | end 57 | prev_pos = pos + 1 58 | until not pos 59 | 60 | return k_hyphened 61 | end 62 | 63 | 64 | -- Returns an empty headers table with internalised case normalisation. 65 | -- Supports the same cases as in ngx_lua: 66 | -- 67 | -- headers.content_length 68 | -- headers["content-length"] 69 | -- headers["Content-Length"] 70 | function _M.new(self) 71 | local mt = { 72 | normalised = {}, 73 | } 74 | 75 | mt.__index = function(t, k) 76 | local k_hyphened = hyphenate(k) 77 | local k_normalised = str_lower(k_hyphened) 78 | return rawget(t, mt.normalised[k_normalised]) 79 | end 80 | 81 | -- First check the normalised table. If there's no match (first time) add an entry for 82 | -- our current case in the normalised table. This is to preserve the human (prettier) case 83 | -- instead of outputting lowercased header names. 84 | -- 85 | -- If there's a match, we're being updated, just with a different case for the key. We use 86 | -- the normalised table to give us the original key, and perorm a rawset(). 87 | mt.__newindex = function(t, k, v) 88 | -- we support underscore syntax, so always hyphenate. 89 | local k_hyphened = hyphenate(k) 90 | 91 | -- lowercase hyphenated is "normalised" 92 | local k_normalised = str_lower(k_hyphened) 93 | 94 | if not mt.normalised[k_normalised] then 95 | mt.normalised[k_normalised] = k_hyphened 96 | rawset(t, k_hyphened, v) 97 | else 98 | rawset(t, mt.normalised[k_normalised], v) 99 | end 100 | end 101 | 102 | return setmetatable({}, mt) 103 | end 104 | 105 | 106 | return _M 107 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/libxml2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharLemAznable/lua-resty-wechat/27f040352f6c63326d3b0562c6edc94cd47beabb/lib/resty/wechat/utils/libxml2.so -------------------------------------------------------------------------------- /lib/resty/wechat/utils/random.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_random2" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local require = require 6 | local ffi = require "ffi" 7 | local ffi_new = ffi.new 8 | local ffi_str = ffi.string 9 | local ffi_typeof = ffi.typeof 10 | local C = ffi.C 11 | local type = type 12 | local random = math.random 13 | local randomseed = math.randomseed 14 | local concat = table.concat 15 | local tostring = tostring 16 | local pcall = pcall 17 | 18 | ffi.cdef[[ 19 | typedef unsigned char u_char; 20 | u_char * ngx_hex_dump(u_char *dst, const u_char *src, size_t len); 21 | int RAND_bytes(u_char *buf, int num); 22 | ]] 23 | 24 | local ok, new_tab = pcall(require, "table.new") 25 | if not ok then 26 | new_tab = function () return {} end 27 | end 28 | 29 | local alnum = { 30 | 'A','B','C','D','E','F','G','H','I','J','K','L','M', 31 | 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', 32 | 'a','b','c','d','e','f','g','h','i','j','k','l','m', 33 | 'n','o','p','q','r','s','t','u','v','w','x','y','z', 34 | '0','1','2','3','4','5','6','7','8','9' 35 | } 36 | 37 | local t = ffi_typeof "uint8_t[?]" 38 | 39 | local function bytes(len, format) 40 | local s = ffi_new(t, len) 41 | C.RAND_bytes(s, len) 42 | if not s then return nil end 43 | if format == "hex" then 44 | local b = ffi_new(t, len * 2) 45 | C.ngx_hex_dump(b, s, len) 46 | return ffi_str(b, len * 2), true 47 | else 48 | return ffi_str(s, len), true 49 | end 50 | end 51 | 52 | local function seed() 53 | local a,b,c,d = bytes(4):byte(1, 4) 54 | return randomseed(a * 0x1000000 + b * 0x10000 + c * 0x100 + d) 55 | end 56 | 57 | local function number(min, max, reseed) 58 | if reseed then seed() end 59 | if min and max then return random(min, max) 60 | elseif min then return random(min) 61 | else return random() end 62 | end 63 | _M.number = number 64 | 65 | function _M.token(len, chars, sep) 66 | chars = chars or alnum 67 | local count 68 | local token = new_tab(len, 0) 69 | if type(chars) ~= "table" then 70 | chars = tostring(chars) 71 | count = #chars 72 | local n 73 | for i=1,len do 74 | n = number(1, count) 75 | token[i] = chars:sub(n, n) 76 | end 77 | else 78 | count = #chars 79 | for i=1,len do token[i] = chars[number(1, count)] end 80 | end 81 | return concat(token, sep) 82 | end 83 | 84 | seed() 85 | 86 | return _M 87 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/redis.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_redis" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | local mt = { __index = _M } 5 | 6 | local ngx_now = ngx.now 7 | local ngx_sleep = ngx.sleep 8 | 9 | local function defaultReplyValue(originValue, defaultValue) 10 | if originValue and originValue ~= null and originValue ~= ngx.null then 11 | return originValue 12 | end 13 | return defaultValue 14 | end 15 | 16 | function _M.connect(self, opt) 17 | local conf = { 18 | host = opt and opt.host or "127.0.0.1", 19 | port = opt and opt.port or 6379, 20 | timeout = opt and opt.timeout or 5000, 21 | maxIdleTimeout = opt and opt.maxIdleTimeout or 10000, 22 | poolSize = opt and opt.poolSize or 10, 23 | distributedLockTimeout = opt and opt.distributedLockTimeout or 20, 24 | } 25 | 26 | local redis = require("resty.redis"):new() 27 | redis:set_timeout(conf.timeout) 28 | local ok, err = redis:connect(conf.host, conf.port) 29 | if not ok then return nil end 30 | 31 | return setmetatable({ redis = redis, conf = conf }, mt) 32 | end 33 | 34 | function _M.keepalive(self, maxIdleTimeout, poolSize) 35 | local redis = self.redis 36 | local conf = self.conf 37 | return redis and redis:set_keepalive(maxIdleTimeout or conf.maxIdleTimeout, poolSize or conf.poolSize) 38 | end 39 | 40 | function _M.close(self) 41 | local redis = self.redis 42 | return redis and redis:close() 43 | end 44 | 45 | function _M.lockProcess(self, key, proc) 46 | -- lock expire time 47 | local timeout = self.conf.distributedLockTimeout 48 | -- distributed lock 49 | local lock = nil 50 | while not lock do 51 | lock = self.redis:setnx(key, ngx_now() * 1000 + timeout * 1000 + 1) 52 | if defaultReplyValue(lock, nil) then break end 53 | 54 | local locktime = self.redis:get(key) 55 | if ngx_now() * 1000 > tonumber(defaultReplyValue(locktime, nil) or 0) then -- if lock timeout, try to get lock. 56 | local origin_locktime = self.redis:getset(key, ngx_now() * 1000 + timeout * 1000 + 1) -- set new lock timeout. 57 | if ngx_now() * 1000 > tonumber(defaultReplyValue(origin_locktime, nil) or 0) then break end -- if origin lock timeout, lock get. 58 | end 59 | ngx_sleep(timeout) 60 | end 61 | -- locked and do job 62 | pcall(proc, self) 63 | -- unlocked if needed 64 | if ngx_now() * 1000 < tonumber(defaultReplyValue(self.redis:get(key), nil) or 0) then 65 | self.redis:del(key) 66 | end 67 | end 68 | 69 | return _M 70 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/urlcodec.lua: -------------------------------------------------------------------------------- 1 | local modname = "wechat_urlcodec" 2 | local _M = { _VERSION = '0.0.1' } 3 | _G[modname] = _M 4 | 5 | local function escape(c) 6 | return string.format("%%%02X", string.byte(c)) 7 | end 8 | 9 | local function unescape(h) 10 | return string.char(tonumber(h, 16)) 11 | end 12 | 13 | function _M.encodeURI(s) 14 | local result = string.gsub(s, "([^%w%.%-])", escape) 15 | return result 16 | end 17 | 18 | function _M.decodeURI(s) 19 | local result = string.gsub(s, "%%(%x%x)", unescape) 20 | return result 21 | end 22 | 23 | return _M 24 | -------------------------------------------------------------------------------- /lib/resty/wechat/utils/xml2lib.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- This module is licensed under the BSD license. 3 | -- 4 | -- Copyright (C) 2013-2014, by aCayF (潘力策) . 5 | -- 6 | -- All rights reserved. 7 | -- 8 | -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 9 | -- 10 | -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | -- 12 | -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | -- 14 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -- 16 | 17 | local modname = "wechat_xml2lib" 18 | local _M = { _VERSION = '0.0.2' } 19 | _G[modname] = _M 20 | 21 | local ffi = require "ffi" 22 | local xml2lib = ffi.load("xml2") 23 | local mt = { __index = xml2lib } 24 | 25 | ffi.cdef[[ 26 | typedef unsigned char xmlChar; 27 | typedef enum { 28 | XML_ELEMENT_NODE = 1, 29 | XML_TEXT_NODE = 3, 30 | XML_CDATA_SECTION_NODE = 4, 31 | } xmlElementType; 32 | struct _xmlNode { 33 | void *_private; 34 | xmlElementType type; 35 | const xmlChar *name; 36 | struct _xmlNode *children; 37 | struct _xmlNode *last; 38 | struct _xmlNode *parent; 39 | struct _xmlNode *next; 40 | struct _xmlNode *prev; 41 | struct _xmlDoc *doc; 42 | struct _xmlNs *ns; 43 | xmlChar *content; 44 | struct _xmlAttr *properties; 45 | struct _xmlNs *nsDef; 46 | void *psvi; 47 | unsigned short line; 48 | unsigned short extra; 49 | }; 50 | struct _xmlDoc { 51 | void *_private; 52 | xmlElementType type; 53 | char *name; 54 | struct _xmlNode *children; 55 | struct _xmlNode *last; 56 | struct _xmlNode *parent; 57 | struct _xmlNode *next; 58 | struct _xmlNode *prev; 59 | struct _xmlDoc *doc; 60 | int compression; 61 | int standalone; 62 | struct _xmlDtd *intSubset; 63 | struct _xmlDtd *extSubset; 64 | struct _xmlNs *oldNs; 65 | const xmlChar *version; 66 | const xmlChar *encoding; 67 | void *ids; 68 | void *refs; 69 | const xmlChar *URL; 70 | int charset; 71 | struct _xmlDict *dict; 72 | void *psvi; 73 | int parseFlags; 74 | int properties; 75 | }; 76 | 77 | struct _xmlDoc * xmlReadMemory(const char * buffe, int size, const char * URL, const char * encoding, int options); 78 | void xmlFreeDoc(struct _xmlDoc * cur); 79 | void xmlCleanupParser(void); 80 | ]] 81 | 82 | function _M.newXmlDoc() 83 | return ffi.new("struct _xmlDoc *") 84 | end 85 | 86 | function _M.newXmlNode() 87 | return ffi.new("struct _xmlNode *") 88 | end 89 | 90 | return setmetatable(_M, mt) 91 | --------------------------------------------------------------------------------