Skip to content

Commit

Permalink
✨ support qcloud sms
Browse files Browse the repository at this point in the history
  • Loading branch information
maiqingqiang committed May 16, 2023
1 parent 4141300 commit a1b82a1
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 85 deletions.
32 changes: 25 additions & 7 deletions gateways/aliyun/aliyun.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ import (
"time"
)

const NAME = "aliyun"

const EndpointUrl = "http://dysmsapi.aliyuncs.com"
const EndpointMethod = "SendSms"
const EndpointVersion = "2017-05-25"
const EndpointFormat = "JSON"
const EndpointRegionId = "cn-hangzhou"
const EndpointSignatureMethod = "HMAC-SHA1"
const EndpointSignatureVersion = "1.0"
const OK = "OK"

var _ gsms.Gateway = (*Gateway)(nil)

type Gateway struct {
Expand All @@ -23,6 +34,15 @@ type Gateway struct {
SignName string
}

// SendSmsResponse
// https://help.aliyun.com/document_detail/419273.htm?spm=a2c4g.11186623.0.0.4a0879bebUrJyq#api-detail-40
type SendSmsResponse struct {
Code string `json:"Code"`
Message string `json:"Message"`
BizId string `json:"BizId"`
RequestId string `json:"RequestId"`
}

func (g *Gateway) Name() string {
return NAME
}
Expand Down Expand Up @@ -58,9 +78,9 @@ func (g *Gateway) Send(to *gsms.PhoneNumber, message gsms.Message, config *gsms.
}
query.Add("TemplateParam", string(marshal))

query.Add("Signature", generateSign(http.MethodGet, g.AccessKeySecret, query))
query.Add("Signature", g.generateSign(query))

var response Response
var response SendSmsResponse

d := dove.New(dove.WithTimeout(config.Timeout), dove.WithLogger(config.Logger))

Expand All @@ -78,18 +98,16 @@ func (g *Gateway) Send(to *gsms.PhoneNumber, message gsms.Message, config *gsms.

// generateSign Generate sign.
// https://help.aliyun.com/document_detail/101343.html
func generateSign(httpMethod, accessKeySecret string, query url.Values) string {
httpMethod = strings.ToUpper(httpMethod)

func (g *Gateway) generateSign(query url.Values) string {
encode := url.QueryEscape(query.Encode())

encode = strings.Replace(encode, "+", "%20", -1)
encode = strings.Replace(encode, "*", "%2A", -1)
encode = strings.Replace(encode, "%7E", "~", -1)

h := hmac.New(sha1.New, []byte(accessKeySecret+"&"))
h := hmac.New(sha1.New, []byte(g.AccessKeySecret+"&"))

h.Write([]byte(fmt.Sprintf("%s&%%2F&%s", httpMethod, encode)))
h.Write([]byte(fmt.Sprintf("%s&%%2F&%s", http.MethodGet, encode)))

return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
4 changes: 3 additions & 1 deletion gateways/aliyun/aliyun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func Test_generateSign(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, generateSign(tt.args.httpMethod, tt.args.accessKeySecret, tt.args.query))
g := &Gateway{AccessKeySecret: tt.args.accessKeySecret}

assert.Equal(t, tt.want, g.generateSign(tt.args.query))
})
}
}
Expand Down
28 changes: 0 additions & 28 deletions gateways/aliyun/common.go

This file was deleted.

203 changes: 203 additions & 0 deletions gateways/qcloud/qcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package qcloud

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/maiqingqiang/gsms"
"github.com/maiqingqiang/gsms/utils"
"github.com/maiqingqiang/gsms/utils/dove"
"net/http"
"time"
)

const NAME = "qcloud"

const EndpointUrl = "https://sms.tencentcloudapi.com"
const EndpointHost = "sms.tencentcloudapi.com"
const EndpointService = "sms"
const EndpointMethod = "SendSms"
const EndpointVersion = "2021-01-11"
const EndpointRegion = "ap-guangzhou"
const Ok = "Ok"

// SendSmsRequest 请求参数 https://cloud.tencent.com/document/api/382/55981
// https://github.com/TencentCloud/signature-process-demo/blob/main/sms/signature-v3/golang/demo.go
type SendSmsRequest struct {
// 下发手机号码,采用 E.164 标准,格式为+[国家或地区码][手机号],单次请求最多支持200个手机号且要求全为境内手机号或全为境外手机号。
// 例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号。
PhoneNumberSet []string `json:"PhoneNumberSet,omitempty"`

// 短信 SdkAppId,在 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 添加应用后生成的实际 SdkAppId,示例如1400006666。
SmsSdkAppId string `json:"SmsSdkAppId,omitempty"`

// 模板 ID,必须填写已审核通过的模板 ID。模板 ID 可登录 [短信控制台](https://console.cloud.tencent.com/smsv2) 查看,若向境外手机号发送短信,仅支持使用国际/港澳台短信模板。
TemplateId string `json:"TemplateId,omitempty"`

// 短信签名内容,使用 UTF-8 编码,必须填写已审核通过的签名,例如:腾讯云,签名信息可登录 [短信控制台](https://console.cloud.tencent.com/smsv2) 查看。
// 注:国内短信为必填参数。
SignName string `json:"SignName,omitempty"`

// 模板参数,若无模板参数,则设置为空。
TemplateParamSet []string `json:"TemplateParamSet,omitempty"`

// 短信码号扩展号,默认未开通,如需开通请联系 [sms helper](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。
ExtendCode string `json:"ExtendCode,omitempty"`

// 用户的 session 内容,可以携带用户侧 ID 等上下文信息,server 会原样返回。
SessionContext string `json:"SessionContext,omitempty"`

// 国内短信无需填写该项;国际/港澳台短信已申请独立 SenderId 需要填写该字段,默认使用公共 SenderId,无需填写该字段。
// 注:月度使用量达到指定量级可申请独立 SenderId 使用,详情请联系 [sms helper](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。
SenderId string `json:"SenderId,omitempty"`
}

type SendSmsResponse struct {
Response *struct {
SendStatusSet []*struct {
SerialNo string `json:"SerialNo"`
PhoneNumber string `json:"PhoneNumber"`
Fee int `json:"Fee"`
SessionContext string `json:"SessionContext"`
Code string `json:"Code"`
Message string `json:"Message"`
IsoCode string `json:"IsoCode"`
} `json:"SendStatusSet"`

Error *struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"Error"`

RequestId string `json:"RequestId"`
} `json:"SendSmsResponse"`
}

var _ gsms.Gateway = (*Gateway)(nil)

type Gateway struct {
SdkAppId string
SecretId string
SecretKey string
SignName string
}

func (g *Gateway) Name() string {
return NAME
}

func (g *Gateway) Send(to *gsms.PhoneNumber, message gsms.Message, config *gsms.Config) error {
phone := fmt.Sprintf("%d", to.Number())
if to.IDDCode() != 0 {
phone = to.UniversalNumber()
}

template, err := message.GetTemplate(g)
if err != nil {
return err
}

data, err := message.GetData(g)
if err != nil {
return err
}

templateParamSet := make([]string, 0, len(data))

for _, s := range data {
templateParamSet = append(templateParamSet, s)
}

p := &SendSmsRequest{
PhoneNumberSet: []string{
phone,
},
SmsSdkAppId: g.SdkAppId,
SignName: g.SignName,
TemplateId: template,
TemplateParamSet: templateParamSet,
}

d := dove.New(dove.WithTimeout(config.Timeout), dove.WithLogger(config.Logger))

timestamp := time.Now().Unix()

payload, _ := json.Marshal(p)

config.Logger.Infof("SendSmsRequest: %s", payload)

header := http.Header{}
header.Add("Authorization", g.generateSign(string(payload), timestamp))
header.Add("Host", EndpointHost)
header.Add("Content-Type", "application/json; charset=utf-8")
header.Add("X-TC-Action", EndpointMethod)
header.Add("X-TC-Region", EndpointRegion)
header.Add("X-TC-Timestamp", fmt.Sprintf("%d", timestamp))
header.Add("X-TC-Version", EndpointVersion)

var response SendSmsResponse

err = d.Request(http.MethodPost, EndpointUrl, header, bytes.NewReader(payload), &response)

if err != nil {
return err
}

if response.Response.Error != nil && response.Response.Error.Code != "" {
return fmt.Errorf("send failed: %+v", response.Response.Error.Message)
}

for _, status := range response.Response.SendStatusSet {
if status.Code != Ok {
return fmt.Errorf("send failed, status message: %s", status.Message)
}
}

return nil
}

// https://github.com/TencentCloud/signature-process-demo/blob/main/sms/signature-v3/golang/demo.go
func (g *Gateway) generateSign(params string, timestamp int64) string {
// step 1: build canonical request string
algorithm := "TC3-HMAC-SHA256"
httpRequestMethod := "POST"
canonicalURI := "/"
canonicalQueryString := ""
canonicalHeaders := "content-type:application/json; charset=utf-8\n" + "host:" + EndpointHost + "\n"
signedHeaders := "content-type;host"
hashedRequestPayload := utils.Sha256Hex(params)
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
httpRequestMethod,
canonicalURI,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload)

// step 2: build string to sign
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, EndpointService)
hashedCanonicalRequest := utils.Sha256Hex(canonicalRequest)
string2sign := fmt.Sprintf("%s\n%d\n%s\n%s",
algorithm,
timestamp,
credentialScope,
hashedCanonicalRequest)

// step 3: sign string
secretDate := utils.HmacSha256(date, "TC3"+g.SecretKey)
secretService := utils.HmacSha256(EndpointService, secretDate)
secretSigning := utils.HmacSha256("tc3_request", secretService)
signature := hex.EncodeToString([]byte(utils.HmacSha256(string2sign, secretSigning)))

// step 4: build authorization
authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
algorithm,
g.SecretId,
credentialScope,
signedHeaders,
signature)

return authorization
}
33 changes: 33 additions & 0 deletions gateways/qcloud/qcloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package qcloud

import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
)

func TestGateway_generateSign(t *testing.T) {
g := &Gateway{
SdkAppId: "SdkAppId",
SecretId: "SecretId",
SecretKey: "SecretKey",
SignName: "gsms",
}

params := &SendSmsRequest{
PhoneNumberSet: []string{"18888888888"},
SmsSdkAppId: g.SdkAppId,
TemplateId: "1111111",
SignName: g.SignName,
TemplateParamSet: []string{"521410", "5"},
}
payload, _ := json.Marshal(params)

sign := g.generateSign(string(payload), 1684225049)

assert.Equal(
t,
"TC3-HMAC-SHA256 Credential=SecretId/2023-05-16/sms/tc3_request, SignedHeaders=content-type;host, Signature=c339e750c3b92ca97783de2c8d00434b12cf8f22c45d421fa8b0609d64e51358",
sign,
)
}
21 changes: 0 additions & 21 deletions gateways/yunpian/common.go

This file was deleted.

Loading

0 comments on commit a1b82a1

Please sign in to comment.