wechat gateway

master
dustoair 3 years ago
commit 3b2b97f860

14
.gitignore vendored

@ -0,0 +1,14 @@
.idea
go.sum
main.exe
server.exe
log.txt
logs
*.log
uploads
gin.log
cert
challenge
.env
application.yaml
nogit_test.go

@ -0,0 +1,30 @@
################################################################################
## MAIN STAGE ##
################################################################################
# Copy the manager into the distroless image.
#FROM scratch
#FROM hub.sre.ink/base/distroless-static:nonroot-20210710
#FROM mirror.ccs.tencentyun.com/library/alpine:3.13
FROM alpine:3.15.0
#FROM hub.sre.ink/base/alpine:3.15.0
#FROM centos:7.9.2009
LABEL Description="sre wechat reciver gateway"
MAINTAINER sre <sre@yangqiao.org>
#RUN echo 'https://mirrors.cloud.tencent.com/alpine/v3.13/main' > /etc/apk/repositories \
# && echo 'https://mirrors.cloud.tencent.com/alpine/v3.13/community' >>/etc/apk/repositories \
# && apk update && apk add tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
# && echo "Asia/Shanghai" > /etc/timezone
RUN apk update && apk add tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
WORKDIR /app
COPY wechat.bin /app/wechat.bin
COPY application.yaml /app/application.yaml
ADD kubectl /usr/bin/kubectl
RUN mkdir -p /root/.kube
ADD config /root/.kube/config
RUN chmod -R 777 /app
#USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/wechat.bin"]

@ -0,0 +1,8 @@
server:
port: 8080
wechat:
CorpId: ww24112334343434
AppToken: xPh343434343434343434
EncodingAesKey: SJmT3434343434343434343434
SendSecret: Jk343434444444444444444343434
SendAid: 1000002

@ -0,0 +1,9 @@
package global
var CMDList = []string{
"kubectl ",
"df -h",
"free -m",
"history",
"last",
}

@ -0,0 +1,224 @@
package global
var EmojMap = map[string]string{
"/::-|": "汗",
"/:pig": "猪头",
"/:cake": "蛋糕",
"/::!": "恐慌",
"[大兵]": "大兵",
"/:showlove": "吻",
"/::Z": "睡",
"/::Q": "抓狂",
"/:--b": "流汗",
"/:B-)": "坏笑",
"/:X-)": "奸笑",
"/:fade": "花谢了",
"[爆竹]": "爆竹",
"/:sun": "太阳",
"/::P": "调皮",
"[Sigh]": "叹气",
"/::@": "发怒",
"[Awesome]": "666",
"/::T": "呕吐",
"/:P-(": "委屈",
"/:8*": "可怜",
"[Hey]": "打招呼",
"[Concerned]": "关心",
"[Flushed]": "圆眼睛",
"[Facepalm]": "绷不住",
"[Smart]": "机灵",
"/:heart": "爱心",
"/:>-|": "鄙视",
"/:cake/:gift": "生日礼物",
"/::-S": "嚷嚷",
"/::d": "目瞪口呆",
"/:,@o": "咦",
"/:rose": "玫瑰",
"/::(": "难过",
"[Onlooker]": "吃瓜",
"/:hug": "抱抱",
"/:dig": "抠鼻",
"/:,@!": "衰",
"/::O": "惊讶",
"[Hurt]": "受伤",
"/:?": "疑惑",
"/:!!!": "骷髅",
"[Sick]": "口罩",
"[Blessing]": "福气",
"/:coffee": "咖啡",
"/::|": "呆住",
"/:handclap": "故障",
"/::'|": "快哭了",
"[LetMeSee]": "我看看",
"[微笑]": "微笑",
"[Yeah!]": "茄子",
"/:@>": "右哼哼",
"/:@@": "拳头",
"[Party]": "祝贺",
"/:share": "握手",
"/:wipe": "擦汗",
"/:,@P": "抿嘴笑",
"/:|-)": "熊猫眼",
"[Doge]": "狗头",
"/::<": "泪奔",
"[Broken]": "裂开",
"[Fireworks]": "烟花",
"[NoProb]": "妥妥的",
"/:strong/:beer": "牛皮",
"[Duh]": "没精神",
"[Emm]": "嗯啊",
"/:jj": "勾引",
"/:moon": "月亮",
"[Lol]": "喜极而泣",
"/:jump": "跳一跳",
"/::~": "撇嘴",
"/:weak": "弱鸡",
"/:,@@": "困",
"/::>": "憨笑",
"[Happy]": "眯眼笑",
"/:shit": "便便",
"[Packet]": "红包",
"[MyBad]": "掌掴",
"[Boring]": "无聊",
"/::D": "裂嘴笑",
"[Respect]": "佩服",
"/:pd": "菜刀",
"/:xx": "敲打",
"/:@)": "承让",
"/::B": "色",
"/:8-)": "酷",
"/::$": "害羞",
"[Wow]": "哇偶",
"/:break": "心碎",
"/:v": "胜利",
"/::'(": "大哭",
"/:circle": "转圈圈",
"[Smirk]": "斜眼笑",
"/:shake": "瑟瑟发抖",
"[OMG]": "老天爷",
"/:ok": "ok",
"[Rich]": "发财",
"/:strong": "赞",
"[Sweats]": "汗水",
"/:,@x": "嘘",
"/:bye": "再见",
"[GoForIt]": "加油啊",
"/:gift": "礼物",
"/:,@-D": "害羞笑",
"[Worship]": "祈祷",
"/::X": "住口",
"/::*": "亲亲",
"[LetDown]": "无精打采",
}
var EmojMapReverse = map[string]string{
"抠鼻": "/:dig",
"斜眼笑": "[Smirk]",
"衰": "/:,@!",
"鄙视": "/:>-|",
"狗头": "[Doge]",
"猪头": "/:pig",
"便便": "/:shit",
"裂嘴笑": "/::D",
"裂开": "[Broken]",
"我看看": "[LetMeSee]",
"胜利": "/:v",
"红包": "[Packet]",
"太阳": "/:sun",
"月亮": "/:moon",
"扇耳光": "[MyBad]",
"握手": "/:share",
"赞": "/:strong",
"牛皮": "/:strong/:beer",
"喜极而泣": "[Lol]",
"困": "/:,@@",
"大哭": "/::'(",
"睡": "/::Z",
"呕吐": "/::T",
"茄子": "[Yeah!]",
"承让": "/:@)",
"蛋糕": "/:cake",
"咖啡": "/:coffee",
"菜刀": "/:pd",
"礼物": "/:gift",
"生日礼物": "/:cake",
"微笑": "[微笑]",
"撇嘴": "/::~",
"色": "/::B",
"呆住": "/::|",
"酷": "/:8-)",
"泪奔": "/::<",
"害羞": "/::$",
"住口": "/::X",
"汗": "/::-|",
"发怒": "/::@",
"调皮": "/::P",
"惊讶": "/::O",
"难过": "/::(",
"流汗": "/:--b",
"抓狂": "/::Q",
"抿嘴笑": "/:,@P",
"害羞笑": "/:,@-D",
"目瞪口呆": "/::d",
"咦": "/:,@o",
"熊猫眼": "/:|-)",
"恐慌": "/::!",
"憨笑": "/::>",
"大兵": "[大兵]",
"嚷嚷": "/::-S",
"疑惑": "/:?",
"嘘": "/:,@x",
"目眩": "/:,@@",
"骷髅": "/:!!!",
"敲打": "/:xx",
"再见": "/:bye",
"擦汗": "/:wipe",
"故障": "/:handclap",
"坏笑": "/:B-)",
"右哼哼": "/:@>",
"委屈": "/:P-(",
"快哭了": "/::'|",
"奸笑": "/:X-)",
"亲亲": "/::*",
"可怜": "/:8*",
"眯眼笑": "[Happy]",
"口罩": "[Sick]",
"圆眼睛": "[Flushed]",
"无精打采": "[LetDown]",
"没精神": "[Duh]",
"打招呼": "[Hey]",
"绷不住": "[Facepalm]",
"机灵": "[Smart]",
"关心": "[Concerned]",
"吃瓜": "[Onlooker]",
"加油啊": "[GoForIt]",
"汗水": "[Sweats]",
"老天爷": "[OMG]",
"嗯啊": "[Emm]",
"佩服": "[Respect]",
"妥妥的": "[NoProb]",
"掌掴": "[MyBad]",
"哇偶": "[Wow]",
"无聊": "[Boring]",
"666": "[Awesome]",
"叹气": "[Sigh]",
"受伤": "[Hurt]",
"吻": "/:showlove",
"爱心": "/:heart",
"心碎": "/:break",
"抱抱": "/:hug",
"弱鸡": "/:weak",
"勾引": "/:jj",
"拳头": "/:@@",
"ok": "/:ok",
"祈祷": "[Worship]",
"玫瑰": "/:rose",
"花谢了": "/:fade",
"祝贺": "[Party]",
"发财": "[Rich]",
"福气": "[Blessing]",
"烟花": "[Fireworks]",
"爆竹": "[爆竹]",
"跳一跳": "/:jump",
"瑟瑟发抖": "/:shake",
"转圈圈": "/:circle",
}

@ -0,0 +1,12 @@
package global
import (
"fmt"
"testing"
)
func TestEmoj(tt *testing.T) {
for k, v := range EmojMap {
fmt.Println(fmt.Sprintf("%s:%s", v, k))
}
}

@ -0,0 +1,15 @@
package global
import (
"context"
"github.com/go-redis/redis/v8"
)
var (
CTX = context.Background()
RedisDb *redis.Client
HistoryCmds []string
EnableReply = true
EnableAiReply = true
EnableCmdExec = true
)

@ -0,0 +1,11 @@
package global
var (
WxCrypt *WXBizMsgCrypt
WechatCorpId string
WechatToken string //微信回调的token
WechatEncodingAesKey string
WechatSendSecret string
WechatSendAid string
WechatAccessToken string //主动发消息的token
)

@ -0,0 +1,315 @@
package global
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"encoding/xml"
"fmt"
"math/rand"
"sort"
"strings"
)
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
ValidateSignatureError int = -40001
ParseXmlError int = -40002
ComputeSignatureError int = -40003
IllegalAesKey int = -40004
ValidateCorpidError int = -40005
EncryptAESError int = -40006
DecryptAESError int = -40007
IllegalBuffer int = -40008
EncodeBase64Error int = -40009
DecodeBase64Error int = -40010
GenXmlError int = -40010
ParseJsonError int = -40012
GenJsonError int = -40013
IllegalProtocolType int = -40014
)
type ProtocolType int
const (
XmlType ProtocolType = 1
)
type CryptError struct {
ErrCode int
ErrMsg string
}
func NewCryptError(err_code int, err_msg string) *CryptError {
return &CryptError{ErrCode: err_code, ErrMsg: err_msg}
}
type WXBizMsg4Recv struct {
Tousername string `xml:"ToUserName"`
Encrypt string `xml:"Encrypt"`
Agentid string `xml:"AgentID"`
}
type CDATA struct {
Value string `xml:",cdata"`
}
type WXBizMsg4Send struct {
XMLName xml.Name `xml:"xml"`
Encrypt CDATA `xml:"Encrypt"`
Signature CDATA `xml:"MsgSignature"`
Timestamp string `xml:"TimeStamp"`
Nonce CDATA `xml:"Nonce"`
}
func NewWXBizMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizMsg4Send {
return &WXBizMsg4Send{Encrypt: CDATA{Value: encrypt}, Signature: CDATA{Value: signature}, Timestamp: timestamp, Nonce: CDATA{Value: nonce}}
}
type ProtocolProcessor interface {
parse(src_data []byte) (*WXBizMsg4Recv, *CryptError)
serialize(msg_send *WXBizMsg4Send) ([]byte, *CryptError)
}
type WXBizMsgCrypt struct {
token string
encoding_aeskey string
receiver_id string
protocol_processor ProtocolProcessor
}
type XmlProcessor struct {
}
func (self *XmlProcessor) parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) {
var msg4_recv WXBizMsg4Recv
err := xml.Unmarshal(src_data, &msg4_recv)
if nil != err {
return nil, NewCryptError(ParseXmlError, "xml to msg fail")
}
return &msg4_recv, nil
}
func (self *XmlProcessor) serialize(msg4_send *WXBizMsg4Send) ([]byte, *CryptError) {
xml_msg, err := xml.Marshal(msg4_send)
if nil != err {
return nil, NewCryptError(GenXmlError, err.Error())
}
return xml_msg, nil
}
func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt {
var protocol_processor ProtocolProcessor
if protocol_type != XmlType {
panic("unsupport protocal")
} else {
protocol_processor = new(XmlProcessor)
}
return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor}
}
func (self *WXBizMsgCrypt) randString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
}
return string(b)
}
func (self *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte {
padding := block_size - (len(plaintext) % block_size)
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
var buffer bytes.Buffer
buffer.WriteString(plaintext)
buffer.Write(padtext)
return buffer.Bytes()
}
func (self *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) {
plaintext_len := len(plaintext)
if nil == plaintext || plaintext_len == 0 {
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero")
}
if plaintext_len%block_size != 0 {
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size")
}
padding_len := int(plaintext[plaintext_len-1])
return plaintext[:plaintext_len-padding_len], nil
}
func (self *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) {
aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
if nil != err {
return nil, NewCryptError(DecodeBase64Error, err.Error())
}
const block_size = 32
pad_msg := self.pKCS7Padding(plaintext, block_size)
block, err := aes.NewCipher(aeskey)
if err != nil {
return nil, NewCryptError(EncryptAESError, err.Error())
}
ciphertext := make([]byte, len(pad_msg))
iv := aeskey[:aes.BlockSize]
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, pad_msg)
base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext)))
base64.StdEncoding.Encode(base64_msg, ciphertext)
return base64_msg, nil
}
func (self *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) {
aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
if nil != err {
return nil, NewCryptError(DecodeBase64Error, err.Error())
}
encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg)
if nil != err {
return nil, NewCryptError(DecodeBase64Error, err.Error())
}
block, err := aes.NewCipher(aeskey)
if err != nil {
return nil, NewCryptError(DecryptAESError, err.Error())
}
if len(encrypt_msg) < aes.BlockSize {
return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid")
}
iv := aeskey[:aes.BlockSize]
if len(encrypt_msg)%aes.BlockSize != 0 {
return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(encrypt_msg, encrypt_msg)
return encrypt_msg, nil
}
func (self *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string {
sort_arr := []string{self.token, timestamp, nonce, data}
sort.Strings(sort_arr)
var buffer bytes.Buffer
for _, value := range sort_arr {
buffer.WriteString(value)
}
sha := sha1.New()
sha.Write(buffer.Bytes())
signature := fmt.Sprintf("%x", sha.Sum(nil))
return string(signature)
}
func (self *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) {
const block_size = 32
plaintext, err := self.pKCS7Unpadding(plaintext, block_size)
if nil != err {
return nil, 0, nil, nil, err
}
text_len := uint32(len(plaintext))
if text_len < 20 {
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1")
}
random := plaintext[:16]
msg_len := binary.BigEndian.Uint32(plaintext[16:20])
if text_len < (20 + msg_len) {
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2")
}
msg := plaintext[20 : 20+msg_len]
receiver_id := plaintext[20+msg_len:]
return random, msg_len, msg, receiver_id, nil
}
func (self *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) {
signature := self.calSignature(timestamp, nonce, echostr)
if strings.Compare(signature, msg_signature) != 0 {
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
}
plaintext, err := self.cbcDecrypter(echostr)
if nil != err {
return nil, err
}
_, _, msg, receiver_id, err := self.ParsePlainText(plaintext)
if nil != err {
return nil, err
}
if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
fmt.Println(string(receiver_id), self.receiver_id, len(receiver_id), len(self.receiver_id))
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
}
return msg, nil
}
func (self *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) {
rand_str := self.randString(16)
var buffer bytes.Buffer
buffer.WriteString(rand_str)
msg_len_buf := make([]byte, 4)
binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg)))
buffer.Write(msg_len_buf)
buffer.WriteString(reply_msg)
buffer.WriteString(self.receiver_id)
tmp_ciphertext, err := self.cbcEncrypter(buffer.String())
if nil != err {
return nil, err
}
ciphertext := string(tmp_ciphertext)
signature := self.calSignature(timestamp, nonce, ciphertext)
msg4_send := NewWXBizMsg4Send(ciphertext, signature, timestamp, nonce)
return self.protocol_processor.serialize(msg4_send)
}
func (self *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) {
msg4_recv, crypt_err := self.protocol_processor.parse(post_data)
if nil != crypt_err {
return nil, crypt_err
}
signature := self.calSignature(timestamp, nonce, msg4_recv.Encrypt)
if strings.Compare(signature, msg_signature) != 0 {
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
}
plaintext, crypt_err := self.cbcDecrypter(msg4_recv.Encrypt)
if nil != crypt_err {
return nil, crypt_err
}
_, _, msg, receiver_id, crypt_err := self.ParsePlainText(plaintext)
if nil != crypt_err {
return nil, crypt_err
}
if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
}
return msg, nil
}

@ -0,0 +1,26 @@
module WechatGateWay
go 1.19
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)

@ -0,0 +1,26 @@
package handle
import (
"fmt"
"log"
"net/http"
)
func HandleTencent(w http.ResponseWriter, r *http.Request) {
if r.URL.RequestURI() == "/favicon.ico" {
return
}
switch r.Method {
case "GET":
// 微信企业号验证
log.Println("接收到验证请求")
handleVerify(w, r)
case "POST":
// 微信企业号接收到消息
log.Println("接收到消息")
handleMessage(w, r)
default:
log.Println("接收到非法请求")
fmt.Fprintln(w, "接收到非法请求")
}
}

@ -0,0 +1,63 @@
package handle
import (
"WechatGateWay/global"
"encoding/xml"
"io"
"log"
"net/http"
)
func handleMessage(w http.ResponseWriter, r *http.Request) {
msgSignature := r.URL.Query().Get("msg_signature") //消息签名
timestamp := r.URL.Query().Get("timestamp")
nonce := r.URL.Query().Get("nonce") //随机数,由企业自行生成
body, err := io.ReadAll(r.Body)
if err != nil {
log.Println("读取Body错误", err.Error())
return
} else {
msg, err := global.WxCrypt.DecryptMsg(msgSignature, timestamp, nonce, body)
if err != nil {
log.Println("解密消息Body错误", err.ErrMsg)
return
} else {
var msgContent MsgContent
err := xml.Unmarshal(msg, &msgContent)
if err != nil {
log.Println("反序列化错误")
return
} else {
log.Println("消息验证成功:", msgContent)
if global.EnableReply {
Reply(msgContent, timestamp, nonce, w)
} else {
return
}
}
}
}
}
func Reply(msgContent MsgContent, timestamp, nonce string, w http.ResponseWriter) {
switch msgContent.MsgType {
case "text":
log.Println("接收到文本消息")
replyText(msgContent, timestamp, nonce, w)
case "image":
log.Println("收到图片消息")
replyImage(msgContent, timestamp, nonce, w)
return
case "voice":
log.Println("收到语音消息")
return
case "event":
log.Println("收到事件消息")
return
case "location":
log.Println("收到地理位置消息")
return
}
}

@ -0,0 +1,26 @@
package handle
import (
"WechatGateWay/global"
"log"
"net/http"
)
func handleVerify(w http.ResponseWriter, r *http.Request) {
msgSignature := r.URL.Query().Get("msg_signature")
timestamp := r.URL.Query().Get("timestamp")
nonce := r.URL.Query().Get("nonce")
echoStr := r.URL.Query().Get("echostr")
// 合法性验证
echoStrBytes, err := global.WxCrypt.VerifyURL(msgSignature, timestamp, nonce, echoStr)
if err != nil {
log.Println("验证失败", err.ErrMsg)
} else {
log.Println("验证成功", string(echoStrBytes))
// 需要返回才能通过验证
_, err := w.Write(echoStrBytes)
if err != nil {
log.Println("返回验证结果失败", err.Error())
}
}
}

@ -0,0 +1,19 @@
package handle
import (
"WechatGateWay/third_part"
"fmt"
"net/http"
)
func replyImage(msgContent MsgContent, timestamp, nonce string, w http.ResponseWriter) {
fmt.Println(msgContent.ToUsername)
fmt.Println(msgContent.FromUsername)
fmt.Println(msgContent.CreateTime)
fmt.Println(msgContent.MsgType)
fmt.Println(msgContent.Content)
fmt.Println(msgContent.Msgid)
fmt.Println(msgContent.Agentid)
third_part.SendPicFile(msgContent.FromUsername, "/app/data/ygg.jpg")
}

@ -0,0 +1,73 @@
package handle
import (
"WechatGateWay/global"
"WechatGateWay/third_part"
"WechatGateWay/utils"
"encoding/xml"
"log"
"net/http"
)
func replyText(msgContent MsgContent, timestamp, nonce string, w http.ResponseWriter) {
var replyContent string
if global.EnableCmdExec && utils.SliceContain(global.CMDList, msgContent.Content) {
//开启了命令执行功能,并且消息内容包含了命令
log.Println("收到命令消息")
var cmdString string
if msgContent.Content == "last" {
cmdString = global.HistoryCmds[len(global.HistoryCmds)-1]
} else if msgContent.Content == "history" {
cmdString = utils.SlicePrint(global.HistoryCmds)
} else {
cmdString = msgContent.Content
}
global.HistoryCmds = append(global.HistoryCmds, msgContent.Content)
replyContent, err := utils.CMDShellTrick(cmdString)
if err != nil {
replyContent = "/:,@!执行失败:\n" + msgContent.Content
} else {
replyContent = "/::D执行成功\n" + replyContent
//aiAnswer = "执行命令:" + msgContent.Content
}
} else {
var replyContentAi string
if _, ok := global.EmojMap[msgContent.Content]; ok {
//系统表情消息 取字典意思丢ai回复
replyContentAi = third_part.AiChat(global.EmojMap[msgContent.Content])
} else {
//普通文本消息直接丢ai
replyContentAi = third_part.AiChat(msgContent.Content)
}
//对ai结果 有表情包的上表情包
if emoj, ok := utils.MapKey(global.EmojMap, replyContentAi); ok {
replyContent = emoj + replyContentAi
} else {
replyContent = replyContentAi
}
}
//构造回复消息
replyMsg, _ := xml.Marshal(ReplyTextMsg{
ToUsername: msgContent.FromUsername,
FromUsername: msgContent.ToUsername,
CreateTime: msgContent.CreateTime,
MsgType: "text",
Content: replyContent,
})
encryptMsg, cryptErr := global.WxCrypt.EncryptMsg(string(replyMsg), timestamp, nonce)
if cryptErr != nil {
log.Println("回复加密出错", cryptErr)
return
} else {
log.Println(string(encryptMsg))
l, err := w.Write(encryptMsg)
if err != nil {
log.Println("返回消息失败")
return
} else {
log.Println("成功写入", l)
}
}
}

@ -0,0 +1,32 @@
package handle
// ToUserName 成员UserID
// FromUserName 企业微信CorpID
// CreateTime 消息创建时间(整型)
// MsgType 消息类型此时固定为text
// Content 文本消息内容,最长不超过2048个字节超过将截断
type ReplyTextMsg struct {
ToUsername string `xml:"ToUserName"`
FromUsername string `xml:"FromUserName"`
CreateTime uint32 `xml:"CreateTime"`
MsgType string `xml:"MsgType"`
Content string `xml:"Content"`
}
type ReplyImageMsg struct {
ToUsername string `xml:"ToUserName"`
FromUsername string `xml:"FromUserName"`
CreateTime uint32 `xml:"CreateTime"`
MsgType string `xml:"MsgType"`
Content string `xml:"Content"`
}
type MsgContent struct {
ToUsername string `xml:"ToUserName"`
FromUsername string `xml:"FromUserName"`
CreateTime uint32 `xml:"CreateTime"`
MsgType string `xml:"MsgType"`
Content string `xml:"Content"`
Msgid string `xml:"MsgId"`
Agentid uint32 `xml:"AgentId"`
}

@ -0,0 +1,43 @@
package main
import (
"WechatGateWay/global"
"WechatGateWay/handle"
"fmt"
"github.com/spf13/viper"
"log"
"net/http"
"os"
)
func init() {
// 读取配置文件
workDir, _ := os.Getwd()
viper.SetConfigName("application")
viper.SetConfigType("yml")
viper.AddConfigPath(workDir)
err := viper.ReadInConfig()
if err != nil {
fmt.Println("config file not found")
os.Exit(1)
}
global.WechatCorpId = viper.GetString("wechat.CorpId")
global.WechatToken = viper.GetString("wechat.AppToken")
global.WechatEncodingAesKey = viper.GetString("wechat.EncodingAesKey")
global.WechatSendSecret = viper.GetString("wechat.SendSecret")
global.WechatSendAid = viper.GetString("wechat.SendAid")
// receive_id 企业应用的回调表示corpid
global.WxCrypt = global.NewWXBizMsgCrypt(global.WechatToken, global.WechatEncodingAesKey, global.WechatCorpId, global.XmlType)
//go third_part.GetRemoteToken(global.WechatCorpId, global.WechatSendSecret)
log.Println("server init success")
}
func main() {
// 开启一个http服务器接收来自企业微信的消息
http.HandleFunc("/", handle.HandleTencent)
port := viper.GetString("server.port")
if port == "" {
port = "8080"
}
log.Println("server start at port:", port)
log.Fatalln(http.ListenAndServe(":"+port, nil))
}

@ -0,0 +1,53 @@
# 接收消息与事件
https://developer.work.weixin.qq.com/document/10514
微信消息加解密库
https://github.com/go-laoji/wxbizmsgcrypt
https://github.com/easychen/wecomchan/blob/main/go-wecomchan/wecomchan.go
## build arm64 on oracle k8s
```bash
cd /root
rm -rf WechatGateWay
git clone https://git.sre.ink/go/WechatGateWay.git
cd WechatGateWay
/bin/cp -arf /root/wx.yaml application.yaml
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go mod tidy
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -v -a -o wechat.bin main.go
/bin/cp -arf /usr/bin/kubectl .
/bin/cp -arf /root/.kube/config .
docker build -t sre/wechatgateway:arm64 .
kubectl rollout restart deployment -n sre wehcat-gateway
```
## ingress
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-ip2region
namespace: sre
annotations:
kubernetes.io/ingress.class: "nginx" # 自动签发开关
cert-manager.io/cluster-issuer: "letsencrypt-prod-http01" # 自动签发开关
spec:
tls:
- hosts:
- test.com
secretName: ingress-tls-test-com # 需要修改
rules:
- host: test.com
http:
paths:
- path: /
backend:
service:
name: ipregion
port:
number: 8080
pathType: ImplementationSpecific
```

@ -0,0 +1,374 @@
package third_part
import (
"WechatGateWay/global"
"WechatGateWay/utils"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math"
"mime/multipart"
"net/http"
"os"
"reflect"
)
var ctx = global.CTX
var GetTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
var SendMessageApi = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s"
var UploadMediaApi = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
type Msg struct {
Content string `json:"content"`
}
type Pic struct {
MediaId string `json:"media_id"`
}
type JsonData struct {
ToUser string `json:"touser"`
AgentId string `json:"agentid"`
MsgType string `json:"msgtype"`
DuplicateCheckInterval int `json:"duplicate_check_interval"`
Text Msg `json:"text"`
Image Pic `json:"image"`
}
// GetRemoteToken 从企业微信服务端API获取access_token存在redis服务则缓存
func GetRemoteToken(corpId, appSecret string) string {
getTokenUrl := fmt.Sprintf(GetTokenApi, corpId, appSecret)
//log.Println("getTokenUrl==>", getTokenUrl)
resp, err := http.Get(getTokenUrl)
if err != nil {
log.Println(err)
}
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
}
resp.Body.Close()
tokenResponse := utils.ParseJson(string(respData))
//log.Println("企业微信获取access_token接口返回==>", tokenResponse)
accessToken := tokenResponse["access_token"].(string)
//存到gloabl
global.WechatAccessToken = accessToken
log.Println("WechatAccessToken已更新")
return accessToken
}
// PostMsg 推送消息
func PostMsg(postData JsonData, postUrl string) string {
postJson, _ := json.Marshal(postData)
log.Println("postJson ", string(postJson))
log.Println("postUrl ", postUrl)
msgReq, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(postJson))
if err != nil {
log.Println(err)
}
msgReq.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(msgReq)
if err != nil {
log.Fatalln("企业微信发送应用消息接口报错==>", err)
}
defer msgReq.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
mediaResp := utils.ParseJson(string(body))
log.Println("企业微信发送应用消息接口返回==>", mediaResp)
return string(body)
}
// UploadMediaFile 上传临时素材并返回mediaId
func UploadMediaFile(msgType string, filename string, accessToken string) (string, float64) {
// 企业微信图片上传不能大于2M
//if pic.Size > 2*1024*1024 {
// log.Fatalln("图片超过2M 无法发送")
// // 自定义code无效的图片文件
// return "", 400
//
//}
//图片类型
//fileExt := strings.ToLower(path.Ext(pic.Filename))
//if fileExt != ".png" && fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".jpeg" {
// log.Fatalln("上传失败!只允许png,jpg,gif,jpeg文件")
// return "", 400
//}
// 读取上传的文件流。
fd, err := os.Open(filename)
if err != nil {
fmt.Println("Error:", err)
return "", 400
}
defer fd.Close()
fileInfo, err := os.Stat(filename)
if err != nil {
fmt.Println("Error:", err)
return "", 400
}
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
if createFormFile, err := writer.CreateFormFile("media", fileInfo.Name()); err == nil {
readAll, _ := ioutil.ReadAll(fd)
createFormFile.Write(readAll)
}
writer.Close()
uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType)
log.Println("uploadMediaUrl==>", uploadMediaUrl)
newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf)
newRequest.Header.Set("Content-Type", writer.FormDataContentType())
log.Println("Content-Type ", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(newRequest)
respData, _ := ioutil.ReadAll(resp.Body)
mediaResp := utils.ParseJson(string(respData))
log.Println("企业微信上传临时素材接口返回==>", mediaResp)
if err != nil {
log.Fatalln("上传临时素材出错==>", err)
return "", mediaResp["errcode"].(float64)
} else {
return mediaResp["media_id"].(string), float64(0)
}
}
// UploadMedia 上传临时素材并返回mediaId
func UploadMedia(msgType string, pic *multipart.FileHeader, accessToken string) (string, float64) {
// 企业微信图片上传不能大于2M
//if pic.Size > 2*1024*1024 {
// log.Fatalln("图片超过2M 无法发送")
// // 自定义code无效的图片文件
// return "", 400
//
//}
//图片类型
//fileExt := strings.ToLower(path.Ext(pic.Filename))
//if fileExt != ".png" && fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".jpeg" {
// log.Fatalln("上传失败!只允许png,jpg,gif,jpeg文件")
// return "", 400
//}
// 读取上传的文件流。
fd, err := pic.Open()
if err != nil {
fmt.Println("Error:", err)
return "", 400
}
defer fd.Close()
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
if createFormFile, err := writer.CreateFormFile("media", pic.Filename); err == nil {
readAll, _ := ioutil.ReadAll(fd)
createFormFile.Write(readAll)
}
writer.Close()
uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType)
log.Println("uploadMediaUrl==>", uploadMediaUrl)
newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf)
newRequest.Header.Set("Content-Type", writer.FormDataContentType())
log.Println("Content-Type ", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(newRequest)
respData, _ := ioutil.ReadAll(resp.Body)
mediaResp := utils.ParseJson(string(respData))
log.Println("企业微信上传临时素材接口返回==>", mediaResp)
if err != nil {
log.Fatalln("上传临时素材出错==>", err)
return "", mediaResp["errcode"].(float64)
} else {
return mediaResp["media_id"].(string), float64(0)
}
}
// ValidateToken 判断accessToken是否失效
// true-未失效, false-失效需重新获取
func ValidateToken(errcode any) bool {
codeTyp := reflect.TypeOf(errcode)
log.Println("errcode的数据类型==>", codeTyp)
if !codeTyp.Comparable() {
log.Printf("type is not comparable: %v", codeTyp)
return true
}
// 如果errcode为42001表明token已失效则清空redis中的token缓存
// 已知codeType为float64
if math.Abs(errcode.(float64)-float64(42001)) < 1e-3 {
log.Println("现需重新获取token")
return false
}
return true
}
// GetAccessToken 获取企业微信的access_token
func GetAccessToken() string {
var WecomCid = global.WechatCorpId
var WecomSecret = global.WechatSendSecret
accessToken := global.WechatAccessToken
if accessToken == "" {
log.Println("get access_token from remote API")
accessToken = GetRemoteToken(WecomCid, WecomSecret)
} else {
log.Println("get access_token from redis")
}
return accessToken
}
// InitJsonData 初始化Json公共部分数据
func InitJsonData(msgType, toUser string) JsonData {
var WecomAid = global.WechatSendAid
return JsonData{
ToUser: toUser,
AgentId: WecomAid,
MsgType: msgType,
DuplicateCheckInterval: 600,
}
}
// due func
func SendText(toUser, msg string) {
var res http.ResponseWriter
// 获取token
accessToken := GetAccessToken()
// 默认token有效
tokenValid := true
// 准备发送应用消息所需参数
postData := InitJsonData("text", toUser)
postData.Text = Msg{
Content: msg,
}
postStatus := ""
for i := 0; i <= 3; i++ {
sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
postStatus = PostMsg(postData, sendMessageUrl)
postResponse := utils.ParseJson(postStatus)
errcode := postResponse["errcode"]
log.Println("发送应用消息接口返回errcode==>", errcode)
tokenValid = ValidateToken(errcode)
// token有效则跳出循环继续执行否则重试3次
if tokenValid {
break
}
// 刷新token
accessToken = GetAccessToken()
}
res.Header().Set("Content-type", "application/json")
_, _ = res.Write([]byte(postStatus))
}
// SendPicFile send pic
func SendPicFile(toUser string, filename string) {
var res http.ResponseWriter
// 获取token
accessToken := GetAccessToken()
// 默认token有效
tokenValid := true
msgType := "image"
// 默认mediaId为空
mediaId := ""
// token有效则跳出循环继续执行否则重试3次
for i := 0; i <= 3; i++ {
var errcode float64
mediaId, errcode = UploadMediaFile(msgType, filename, accessToken)
log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode)
tokenValid = ValidateToken(errcode)
if tokenValid {
break
}
accessToken = GetAccessToken()
}
// 准备发送应用消息所需参数
postData := InitJsonData("image", toUser)
postData.Image = Pic{
MediaId: mediaId,
}
postStatus := ""
for i := 0; i <= 3; i++ {
sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
postStatus = PostMsg(postData, sendMessageUrl)
postResponse := utils.ParseJson(postStatus)
errcode := postResponse["errcode"]
log.Println("发送应用消息接口返回errcode==>", errcode)
tokenValid = ValidateToken(errcode)
// token有效则跳出循环继续执行否则重试3次
if tokenValid {
break
}
// 刷新token
accessToken = GetAccessToken()
}
res.Header().Set("Content-type", "application/json")
_, _ = res.Write([]byte(postStatus))
}
// wechat send pic
func SendPic(toUser string, pic *multipart.FileHeader) {
var res http.ResponseWriter
// 获取token
accessToken := GetAccessToken()
// 默认token有效
tokenValid := true
msgType := "image"
// 默认mediaId为空
mediaId := ""
// token有效则跳出循环继续执行否则重试3次
for i := 0; i <= 3; i++ {
var errcode float64
mediaId, errcode = UploadMedia(msgType, pic, accessToken)
log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode)
tokenValid = ValidateToken(errcode)
if tokenValid {
break
}
accessToken = GetAccessToken()
}
// 准备发送应用消息所需参数
postData := InitJsonData("image", toUser)
postData.Image = Pic{
MediaId: mediaId,
}
postStatus := ""
for i := 0; i <= 3; i++ {
sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
postStatus = PostMsg(postData, sendMessageUrl)
postResponse := utils.ParseJson(postStatus)
errcode := postResponse["errcode"]
log.Println("发送应用消息接口返回errcode==>", errcode)
tokenValid = ValidateToken(errcode)
// token有效则跳出循环继续执行否则重试3次
if tokenValid {
break
}
// 刷新token
accessToken = GetAccessToken()
}
res.Header().Set("Content-type", "application/json")
_, _ = res.Write([]byte(postStatus))
}

@ -0,0 +1,26 @@
package third_part
import (
"WechatGateWay/utils"
"encoding/json"
"log"
"strings"
)
type qingyunkeRes struct {
Result int `json:"result"`
Content string `json:"content"`
}
func AiChat(msg string) string {
log.Println("分析关键词:", msg)
url := "http://api.qingyunke.com/api.php?key=free&appid=0&msg=" + msg
ansByte := utils.HTTPGet(url)
if ansByte == nil {
return "没听懂"
}
var ansRes qingyunkeRes
json.Unmarshal(ansByte, &ansRes)
//{br}替换成换行
return strings.Replace(ansRes.Content, "{br}", "\n", -1)
}

@ -0,0 +1,9 @@
package third_part
import (
"testing"
)
func TestAiChat(t *testing.T) {
t.Log(AiChat("你好"))
}

@ -0,0 +1,31 @@
package utils
import (
"fmt"
"log"
"os/exec"
)
func CMDBool(name string, arg []string) bool {
cmdExec := exec.Command(name, arg...)
log.Println(cmdExec.String())
out, err := cmdExec.CombinedOutput()
if err != nil {
log.Println(string(out))
log.Printf("cmd.Run() failed with %s", err)
return false
}
fmt.Printf("combined out:%s", string(out))
return true
}
func CMDString(name string, arg []string) (string, error) {
cmdExec := exec.Command(name, arg...)
out, err := cmdExec.CombinedOutput()
return string(out), err
}
func CMDShellTrick(cmd string) (string, error) {
out, err := exec.Command("sh", "-c", cmd).Output()
return string(out), err
}

@ -0,0 +1,10 @@
package utils
import "testing"
func TestCMDString(t *testing.T) {
t.Log(CMDString("ping", []string{"www.baidu.com"}))
}
func TestCMDTrick(t *testing.T) {
t.Log(CMDShellTrick("ping www.baidu.com"))
}

@ -0,0 +1,33 @@
package utils
import (
"fmt"
"io"
"net/http"
)
func HTTPGet(url string) []byte {
method := "GET"
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return nil
}
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return nil
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return nil
}
return body
}

@ -0,0 +1,10 @@
package utils
import (
"fmt"
"testing"
)
func TestHTTPGet(t *testing.T) {
fmt.Println(HTTPGet("http://www.baidu.com"))
}

@ -0,0 +1,18 @@
package utils
import (
"encoding/json"
"log"
)
// ParseJson 将json字符串解析为map
func ParseJson(jsonStr string) map[string]any {
var wecomResponse map[string]any
if string(jsonStr) != "" {
err := json.Unmarshal([]byte(string(jsonStr)), &wecomResponse)
if err != nil {
log.Println("生成json字符串错误")
}
}
return wecomResponse
}

@ -0,0 +1,21 @@
package utils
import "math/rand"
func RandMapKey(m map[string]string) string {
mapKeys := make([]string, 0, len(m)) // pre-allocate exact size
for key := range m {
mapKeys = append(mapKeys, key)
}
return mapKeys[rand.Intn(len(mapKeys))]
}
// MapKey 知道值 拿第一个key
func MapKey(m map[string]string, value string) (string, bool) {
for k, v := range m {
if v == value {
return k, true
}
}
return "", false
}

@ -0,0 +1,12 @@
package utils
import (
"WechatGateWay/global"
"fmt"
"testing"
)
func TestRandMap(tt *testing.T) {
fmt.Println(RandMapKey(global.EmojMap))
}

@ -0,0 +1,22 @@
package utils
import (
"strings"
)
func SliceContain(cmds []string, cmd string) bool {
for _, v := range cmds {
if strings.Contains(cmd, v) {
return true
}
}
return false
}
func SlicePrint(cmds []string) string {
res := ""
for _, v := range cmds {
res = res + v + "\n"
}
return res
}

@ -0,0 +1,18 @@
package utils
import (
"WechatGateWay/global"
"fmt"
"testing"
)
func TestSLiceContain(t *testing.T) {
fmt.Println(SliceContain(global.CMDList, "history"))
fmt.Println(SliceContain(global.CMDList, "history1"))
fmt.Println(SliceContain(global.CMDList, "kubectl "))
fmt.Println(SliceContain(global.CMDList, "kubectlssss "))
fmt.Println(SliceContain(global.CMDList, "kubectl get ns "))
}
func TestSlicePrint(t *testing.T) {
fmt.Println(SlicePrint(global.CMDList))
}
Loading…
Cancel
Save