feat(auth): 登录注册 - 添加短信通知与组合验证支持
This commit is contained in:
1 parent
6c013ebf7e
commit
bc0d109428
55 files changed
+4013
-103
No files matched your search
@@ -0,0 +1,15 @@
|
||||
# Local transport extension
|
||||
|
||||
Upstream: `github.com/casdoor/go-sms-sender`, tag `v0.25.0`, commit
|
||||
`af78ac0e42f4` (2024-11-29), Apache-2.0. The upstream Go source is retained;
|
||||
this module is pinned using the parent module's local `replace` directive.
|
||||
|
||||
Canvas additions: `safe_http.go` exposes a context-bound, single-recipient,
|
||||
single-attempt send with structured acceptance identifiers. It uses the upstream
|
||||
SDKs for signing and payload construction, injects the host's outbound transport,
|
||||
and strictly checks the raw provider response. Unknown results are never retried.
|
||||
`tencent.go` additionally rejects missing response/status objects before dereferencing.
|
||||
|
||||
Only Aliyun and Tencent have the safe transport capability. Other upstream
|
||||
providers are not exposed by Canvas. No delivery-receipt capability is claimed.
|
||||
Before upgrading, run the host's SMS transport contract tests.
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
)
|
||||
|
||||
type AliyunClient struct {
|
||||
template string
|
||||
sign string
|
||||
core *dysmsapi.Client
|
||||
}
|
||||
|
||||
type AliyunResult struct {
|
||||
RequestId string
|
||||
Message string
|
||||
}
|
||||
|
||||
func GetAliyunClient(accessId string, accessKey string, sign string, template string) (*AliyunClient, error) {
|
||||
region := "cn-hangzhou"
|
||||
client, err := dysmsapi.NewClientWithAccessKey(region, accessId, accessKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliyunClient := &AliyunClient{
|
||||
template: template,
|
||||
core: client,
|
||||
sign: sign,
|
||||
}
|
||||
|
||||
return aliyunClient, nil
|
||||
}
|
||||
|
||||
func (c *AliyunClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
requestParam, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumbers = strings.Join(targetPhoneNumber, ",")
|
||||
request.TemplateCode = c.template
|
||||
request.TemplateParam = string(requestParam)
|
||||
request.SignName = c.sign
|
||||
|
||||
response, err := c.core.SendSms(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
aliyunResult := AliyunResult{}
|
||||
err = json.Unmarshal(response.GetHttpContentBytes(), &aliyunResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if aliyunResult.Message != "" {
|
||||
return fmt.Errorf(aliyunResult.Message)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sns"
|
||||
"github.com/aws/aws-sdk-go/service/sns/snsiface"
|
||||
)
|
||||
|
||||
type AmazonSNSClient struct {
|
||||
svc snsiface.SNSAPI
|
||||
template string
|
||||
}
|
||||
|
||||
func GetAmazonSNSClient(accessKeyID string, secretAccessKey string, template string, region []string) (*AmazonSNSClient, error) {
|
||||
if len(region) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: region")
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region[0]),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := sns.New(sess)
|
||||
|
||||
snsClient := &AmazonSNSClient{
|
||||
svc: svc,
|
||||
template: template,
|
||||
}
|
||||
|
||||
return snsClient, nil
|
||||
}
|
||||
|
||||
func (a *AmazonSNSClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
bodyContent := fmt.Sprintf(a.template, code)
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
messageAttributes := make(map[string]*sns.MessageAttributeValue)
|
||||
for k, v := range param {
|
||||
messageAttributes[k] = &sns.MessageAttributeValue{
|
||||
DataType: aws.String("String"),
|
||||
StringValue: aws.String(v),
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(targetPhoneNumber); i++ {
|
||||
_, err := a.svc.Publish(&sns.PublishInput{
|
||||
Message: &bodyContent,
|
||||
PhoneNumber: &targetPhoneNumber[i],
|
||||
MessageAttributes: messageAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ACSClient struct {
|
||||
AccessToken string
|
||||
Endpoint string
|
||||
Message string
|
||||
Sender string
|
||||
}
|
||||
|
||||
type reqBody struct {
|
||||
From string `json:"from"`
|
||||
Message string `json:"message"`
|
||||
SMSRecipients []smsRecipient `json:"smsRecipients"`
|
||||
}
|
||||
|
||||
type smsRecipient struct {
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
func GetACSClient(accessToken string, message string, other []string) (*ACSClient, error) {
|
||||
if len(other) < 2 {
|
||||
return nil, fmt.Errorf("missing parameter: endpoint or sender")
|
||||
}
|
||||
|
||||
acsClient := &ACSClient{
|
||||
AccessToken: accessToken,
|
||||
Endpoint: other[0],
|
||||
Message: message,
|
||||
Sender: other[1],
|
||||
}
|
||||
|
||||
return acsClient, nil
|
||||
}
|
||||
|
||||
func (a *ACSClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
reqBody := &reqBody{
|
||||
From: a.Sender,
|
||||
Message: a.Message,
|
||||
SMSRecipients: make([]smsRecipient, 0),
|
||||
}
|
||||
for _, mobile := range targetPhoneNumber {
|
||||
reqBody.SMSRecipients = append(reqBody.SMSRecipients, smsRecipient{To: mobile})
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/sms?api-version=2021-03-07", a.Endpoint)
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
requestBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request body: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+a.AccessToken)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending request: %w", err)
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/baidubce/bce-sdk-go/services/sms"
|
||||
"github.com/baidubce/bce-sdk-go/services/sms/api"
|
||||
)
|
||||
|
||||
type BaiduClient struct {
|
||||
sign string
|
||||
template string
|
||||
core *sms.Client
|
||||
}
|
||||
|
||||
func GetBceClient(accessId, accessKey, sign, template string, endpoint []string) (*BaiduClient, error) {
|
||||
if len(endpoint) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: endpoint")
|
||||
}
|
||||
|
||||
client, err := sms.NewClient(accessId, accessKey, endpoint[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bceClient := &BaiduClient{
|
||||
sign: sign,
|
||||
template: template,
|
||||
core: client,
|
||||
}
|
||||
|
||||
return bceClient, nil
|
||||
}
|
||||
|
||||
func (c *BaiduClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
contentMap := make(map[string]interface{})
|
||||
contentMap["code"] = code
|
||||
|
||||
sendSmsArgs := &api.SendSmsArgs{
|
||||
Mobile: strings.Join(targetPhoneNumber, ","),
|
||||
SignatureId: c.sign,
|
||||
Template: c.template,
|
||||
ContentVar: contentMap,
|
||||
}
|
||||
|
||||
_, err := c.core.SendSms(sendSmsArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
Twilio = "Twilio SMS"
|
||||
AmazonSNS = "Amazon SNS"
|
||||
AzureACS = "Azure ACS"
|
||||
Msg91 = "Msg91 SMS"
|
||||
GCCPAY = "GCCPAY SMS"
|
||||
Infobip = "Infobip SMS"
|
||||
SUBMAIL = "SUBMAIL SMS"
|
||||
SmsBao = "SmsBao SMS"
|
||||
Aliyun = "Aliyun SMS"
|
||||
TencentCloud = "Tencent Cloud SMS"
|
||||
BaiduCloud = "Baidu Cloud SMS"
|
||||
VolcEngine = "Volc Engine SMS"
|
||||
HuaweiCloud = "Huawei Cloud SMS"
|
||||
UCloud = "UCloud SMS"
|
||||
Huyi = "Huyi SMS"
|
||||
MockSms = "Mock SMS"
|
||||
Netgsm = "Netgsm SMS"
|
||||
OsonSms = "OSON SMS"
|
||||
UniSms = "Uni SMS"
|
||||
)
|
||||
|
||||
type SmsClient interface {
|
||||
SendMessage(param map[string]string, targetPhoneNumber ...string) error
|
||||
}
|
||||
|
||||
func NewSmsClient(provider string, accessId string, accessKey string, sign string, template string, other ...string) (SmsClient, error) {
|
||||
switch provider {
|
||||
case Twilio:
|
||||
return GetTwilioClient(accessId, accessKey, template)
|
||||
case AmazonSNS:
|
||||
return GetAmazonSNSClient(accessId, accessKey, template, other)
|
||||
case AzureACS:
|
||||
return GetACSClient(accessKey, template, other)
|
||||
case Msg91:
|
||||
return GetMsg91Client(accessId, accessKey, template)
|
||||
case GCCPAY:
|
||||
return GetGCCPAYClient(accessId, accessKey, template)
|
||||
case Infobip:
|
||||
return GetInfobipClient(accessId, accessKey, template, other)
|
||||
case SUBMAIL:
|
||||
return GetSubmailClient(accessId, accessKey, template)
|
||||
case SmsBao:
|
||||
return GetSmsbaoClient(accessId, accessKey, sign, template, other)
|
||||
case Aliyun:
|
||||
return GetAliyunClient(accessId, accessKey, sign, template)
|
||||
case TencentCloud:
|
||||
return GetTencentClient(accessId, accessKey, sign, template, other)
|
||||
case BaiduCloud:
|
||||
return GetBceClient(accessId, accessKey, sign, template, other)
|
||||
case VolcEngine:
|
||||
return GetVolcClient(accessId, accessKey, sign, template, other)
|
||||
case HuaweiCloud:
|
||||
return GetHuaweiClient(accessId, accessKey, sign, template, other)
|
||||
case UCloud:
|
||||
return GetUcloudClient(accessId, accessKey, sign, template, other)
|
||||
case Huyi:
|
||||
return GetHuyiClient(accessId, accessKey, template)
|
||||
case Netgsm:
|
||||
return GetNetgsmClient(accessId, accessKey, sign, template)
|
||||
case MockSms:
|
||||
return NewMocker(accessId, accessKey, sign, template, other)
|
||||
case OsonSms:
|
||||
return GetOsonClient(accessId, accessKey, sign, template)
|
||||
case UniSms:
|
||||
return GetUnismsClient(accessId, accessKey, sign, template)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", provider)
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GCCPAYClient struct {
|
||||
clientname string
|
||||
secret string
|
||||
template string
|
||||
}
|
||||
|
||||
type params struct {
|
||||
Mobile string `json:"mobile"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateParams map[string]string `json:"template_params"`
|
||||
}
|
||||
|
||||
func GetGCCPAYClient(clientname string, secret string, template string) (*GCCPAYClient, error) {
|
||||
gccPayClient := &GCCPAYClient{
|
||||
clientname: clientname,
|
||||
secret: secret,
|
||||
template: template,
|
||||
}
|
||||
|
||||
return gccPayClient, nil
|
||||
}
|
||||
|
||||
func RandStringBytesCrypto(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func Md5(str string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(str))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (c *GCCPAYClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
_, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
reqParams := make(map[string]params)
|
||||
|
||||
for _, mobile := range targetPhoneNumber {
|
||||
if strings.HasPrefix(mobile, "+") {
|
||||
mobile = mobile[1:]
|
||||
}
|
||||
randomString, err := RandStringBytesCrypto(16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMS key generation failed")
|
||||
}
|
||||
|
||||
reqParams[randomString] = params{
|
||||
Mobile: mobile,
|
||||
TemplateCode: c.template,
|
||||
TemplateParams: param,
|
||||
}
|
||||
}
|
||||
|
||||
requestBody := new(bytes.Buffer)
|
||||
err := json.NewEncoder(requestBody).Encode(reqParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMS sending failed")
|
||||
}
|
||||
|
||||
// sign
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
sign := Md5(fmt.Sprintf("%s%d%s", c.clientname, timestamp, c.secret))
|
||||
|
||||
reqUrl := "https://smscenter.sgate.sa/api/v1/client/sendSms"
|
||||
|
||||
// send request
|
||||
req, _ := http.NewRequest("POST", reqUrl, requestBody)
|
||||
req.Header.Set("clientname", c.clientname)
|
||||
req.Header.Set("timestamp", fmt.Sprintf("%d", timestamp))
|
||||
req.Header.Set("sign", sign)
|
||||
req.Header.Set("content-type", "application/json;")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
module github.com/casdoor/go-sms-sender
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/baidubce/bce-sdk-go v0.9.156
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744
|
||||
github.com/twilio/twilio-go v1.13.0
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\""
|
||||
AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\""
|
||||
)
|
||||
|
||||
type HuaweiClient struct {
|
||||
accessId string
|
||||
accessKey string
|
||||
sign string
|
||||
template string
|
||||
apiAddress string
|
||||
sender string
|
||||
}
|
||||
|
||||
func GetHuaweiClient(accessId string, accessKey string, sign string, template string, other []string) (*HuaweiClient, error) {
|
||||
if len(other) < 2 {
|
||||
return nil, fmt.Errorf("missing parameter: apiAddress or sender")
|
||||
}
|
||||
|
||||
apiAddress := fmt.Sprintf("%s/sms/batchSendSms/v1", other[0])
|
||||
|
||||
huaweiClient := &HuaweiClient{
|
||||
accessId: accessId,
|
||||
accessKey: accessKey,
|
||||
sign: sign,
|
||||
template: template,
|
||||
apiAddress: apiAddress,
|
||||
sender: other[1],
|
||||
}
|
||||
|
||||
return huaweiClient, nil
|
||||
}
|
||||
|
||||
// SendMessage https://support.huaweicloud.com/intl/en-us/devg-msgsms/sms_04_0012.html
|
||||
func (c *HuaweiClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
phoneNumbers := strings.Join(targetPhoneNumber, ",")
|
||||
templateParas := fmt.Sprintf("[\"%s\"]", code)
|
||||
|
||||
body := buildRequestBody(c.sender, phoneNumbers, c.template, templateParas, "", c.sign)
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
headers["Authorization"] = AUTH_HEADER_VALUE
|
||||
headers["X-WSSE"] = buildWsseHeader(c.accessId, c.accessKey)
|
||||
|
||||
_, err := post(c.apiAddress, []byte(body), headers)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildRequestBody(sender, receiver, templateId, templateParas, statusCallBack, signature string) string {
|
||||
param := "from=" + url.QueryEscape(sender) + "&to=" + url.QueryEscape(receiver) + "&templateId=" + url.QueryEscape(templateId)
|
||||
if templateParas != "" {
|
||||
param += "&templateParas=" + url.QueryEscape(templateParas)
|
||||
}
|
||||
if statusCallBack != "" {
|
||||
param += "&statusCallback=" + url.QueryEscape(statusCallBack)
|
||||
}
|
||||
if signature != "" {
|
||||
param += "&signature=" + url.QueryEscape(signature)
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
func buildWsseHeader(appKey, appSecret string) string {
|
||||
cTime := time.Now().Format("2006-01-02T15:04:05Z")
|
||||
nonce := uuid.New().String()
|
||||
nonce = strings.ReplaceAll(nonce, "-", "")
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(nonce + cTime + appSecret))
|
||||
passwordDigestBase64Str := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return fmt.Sprintf(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, cTime)
|
||||
}
|
||||
|
||||
func post(url string, param []byte, headers map[string]string) (string, error) {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(param))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for key, header := range headers {
|
||||
req.Header.Set(key, header)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HuyiClient struct {
|
||||
appId string
|
||||
appKey string
|
||||
template string
|
||||
}
|
||||
|
||||
func GetHuyiClient(appId string, appKey string, template string) (*HuyiClient, error) {
|
||||
return &HuyiClient{
|
||||
appId: appId,
|
||||
appKey: appKey,
|
||||
template: template,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetMd5String(s string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(s))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (hc *HuyiClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missin parer: trgetPhoneNumber")
|
||||
}
|
||||
|
||||
_now := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
smsContent := fmt.Sprintf(hc.template, code)
|
||||
v := url.Values{}
|
||||
v.Set("account", hc.appId)
|
||||
v.Set("content", smsContent)
|
||||
v.Set("time", _now)
|
||||
passwordStr := hc.appId + hc.appKey + "%s" + smsContent + _now
|
||||
for _, mobile := range targetPhoneNumber {
|
||||
password := fmt.Sprintf(passwordStr, mobile)
|
||||
v.Set("password", GetMd5String(password))
|
||||
v.Set("mobile", mobile)
|
||||
|
||||
body := strings.NewReader(v.Encode()) // encode form data
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("POST", "http://106.ihuyi.com/webservice/sms.php?method=Submit&format=json", body)
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
|
||||
resp, err := client.Do(req) // request remote
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close() // ! close ReadCloser
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InfobipClient struct {
|
||||
baseUrl string
|
||||
sender string
|
||||
apiKey string
|
||||
template string
|
||||
}
|
||||
|
||||
type InfobipConfigService struct {
|
||||
baseUrl string
|
||||
sender string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
type SmsService struct {
|
||||
configService InfobipConfigService
|
||||
}
|
||||
|
||||
type MessageData struct {
|
||||
Messages []Message `json:"messages"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
From string `json:"from"`
|
||||
Destinations []Destination `json:"destinations"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Destination struct {
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
func GetInfobipClient(sender string, apiKey string, template string, baseUrl []string) (*InfobipClient, error) {
|
||||
if len(baseUrl) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: baseUrl")
|
||||
}
|
||||
|
||||
infobipClient := &InfobipClient{
|
||||
baseUrl: baseUrl[0],
|
||||
sender: sender,
|
||||
apiKey: apiKey,
|
||||
template: template,
|
||||
}
|
||||
|
||||
return infobipClient, nil
|
||||
}
|
||||
|
||||
func (c *InfobipClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missin parer: trgetPhoneNumber")
|
||||
}
|
||||
|
||||
mobile := targetPhoneNumber[0]
|
||||
|
||||
if strings.HasPrefix(mobile, "0") {
|
||||
mobile = "886" + mobile[1:]
|
||||
}
|
||||
if strings.HasPrefix(mobile, "+") {
|
||||
mobile = mobile[1:]
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s/sms/2/text/advanced", c.baseUrl)
|
||||
text := fmt.Sprintf(c.template, code)
|
||||
|
||||
messageData := MessageData{
|
||||
Messages: []Message{
|
||||
{
|
||||
From: c.sender,
|
||||
Destinations: []Destination{
|
||||
{
|
||||
To: mobile,
|
||||
},
|
||||
},
|
||||
Text: text,
|
||||
},
|
||||
},
|
||||
}
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("App %s", c.apiKey),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
messageDataBytes, _ := json.Marshal(messageData)
|
||||
req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(messageDataBytes))
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
type Mocker struct{}
|
||||
|
||||
var _ SmsClient = &Mocker{}
|
||||
|
||||
func NewMocker(accessId, accessKey, sign, templateId string, smsAccount []string) (*Mocker, error) {
|
||||
return &Mocker{}, nil
|
||||
}
|
||||
|
||||
func (m *Mocker) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
return nil
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Msg91Client struct {
|
||||
authKey string
|
||||
senderId string
|
||||
templateId string
|
||||
}
|
||||
|
||||
func GetMsg91Client(senderId string, authKey string, templateId string) (*Msg91Client, error) {
|
||||
msg91Client := &Msg91Client{
|
||||
authKey: authKey,
|
||||
senderId: senderId,
|
||||
templateId: templateId,
|
||||
}
|
||||
|
||||
return msg91Client, nil
|
||||
}
|
||||
|
||||
func (m *Msg91Client) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
url := "https://control.msg91.com/api/v5/flow/"
|
||||
|
||||
for _, mobile := range targetPhoneNumber {
|
||||
if strings.HasPrefix(mobile, "+") {
|
||||
mobile = mobile[1:]
|
||||
}
|
||||
|
||||
payload, err := buildPayload(m.templateId, m.senderId, "0", mobile, param)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMS build payload failed: %v", err)
|
||||
}
|
||||
|
||||
err = postMsg91SendRequest(url, strings.NewReader(payload), m.authKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send message failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPayload(templateId, senderId, shortURL, mobiles string, variables map[string]string) (string, error) {
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
payload["template_id"] = templateId
|
||||
payload["sender"] = senderId
|
||||
payload["short_url"] = shortURL
|
||||
payload["mobiles"] = mobiles
|
||||
|
||||
for k, v := range variables {
|
||||
payload[k] = v
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
func postMsg91SendRequest(url string, payload io.Reader, authKey string) error {
|
||||
req, _ := http.NewRequest("POST", url, payload)
|
||||
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("content-type", "application/json")
|
||||
req.Header.Add("authkey", authKey)
|
||||
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
|
||||
err := res.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type NetgsmClient struct {
|
||||
accessId string
|
||||
accessKey string
|
||||
sign string
|
||||
template string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type NetgsmResponse struct {
|
||||
Code string `xml:"main>code"`
|
||||
JobID string `xml:"main>jobID"`
|
||||
Error string `xml:"main>error"`
|
||||
}
|
||||
|
||||
func GetNetgsmClient(accessId, accessKey, sign, template string) (*NetgsmClient, error) {
|
||||
return &NetgsmClient{
|
||||
accessId: accessId,
|
||||
accessKey: accessKey,
|
||||
sign: sign,
|
||||
template: template,
|
||||
httpClient: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *NetgsmClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
for _, phoneNumber := range targetPhoneNumber {
|
||||
data := fmt.Sprintf(`
|
||||
<mainbody>
|
||||
<header>
|
||||
<usercode>%s</usercode>
|
||||
<password>%s</password>
|
||||
<msgheader>%s</msgheader>
|
||||
</header>
|
||||
<body>
|
||||
<msg>
|
||||
<![CDATA[%s]]>
|
||||
</msg>
|
||||
<no>%s</no>
|
||||
</body>
|
||||
</mainbody>`, c.accessId, c.accessKey, c.sign, c.template, phoneNumber)
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/xml",
|
||||
}
|
||||
|
||||
respBody, err := c.postXML("https://api.netgsm.com.tr/sms/send/otp", data, headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var netgsmResponse NetgsmResponse
|
||||
if err := xml.Unmarshal([]byte(respBody), &netgsmResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if netgsmResponse.Code != "0" {
|
||||
return errors.New(netgsmResponse.Error)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetgsmClient) postXML(url, xmlData string, headers map[string]string) (string, error) {
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(xmlData)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OsonClient struct {
|
||||
Endpoint string
|
||||
SenderID string
|
||||
SecretAccessHash string
|
||||
Sign string
|
||||
Message string
|
||||
}
|
||||
|
||||
type OsonResponse struct {
|
||||
Status string // ok
|
||||
Timestamp time.Time // 2017-07-07 16:58:12
|
||||
TxnId string // f89xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxe0b
|
||||
MsgId uint // 40127
|
||||
SmscMsgId string // 45f22479
|
||||
SmscMsgStatus string // success
|
||||
SmscMsgParts string // 1
|
||||
}
|
||||
|
||||
func GetOsonClient(senderId, secretAccessHash, sign, message string) (*OsonClient, error) {
|
||||
return &OsonClient{
|
||||
Endpoint: "https://api.osonsms.com/sendsms_v1.php",
|
||||
SenderID: senderId,
|
||||
SecretAccessHash: secretAccessHash,
|
||||
Sign: sign,
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *OsonClient) SendMessage(param map[string]string, targetPhoneNumber ...string) (err error) {
|
||||
// Init http client for make request to sms center. Set a timeout of 25+
|
||||
// seconds to ensure that the response from the SMS center has been
|
||||
// processed.
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
}
|
||||
|
||||
if c.Message == "" {
|
||||
c.Message = fmt.Sprintf("Hello. Your authorization code: %s", param["code"])
|
||||
} else {
|
||||
c.Message += param["code"]
|
||||
}
|
||||
|
||||
txnID := uuid.New()
|
||||
buildStrHash := strings.Join([]string{txnID.String(), c.SenderID, c.Sign, targetPhoneNumber[0], c.SecretAccessHash}, ";")
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(buildStrHash))
|
||||
bs := hash.Sum(nil)
|
||||
strHash := fmt.Sprintf("%x", bs)
|
||||
|
||||
urlLink, err := url.Parse(c.Endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urlParams := url.Values{}
|
||||
urlParams.Add("from", c.Sign)
|
||||
urlParams.Add("phone_number", targetPhoneNumber[0])
|
||||
urlParams.Add("msg", c.Message)
|
||||
urlParams.Add("str_hash", strHash)
|
||||
urlParams.Add("txn_id", txnID.String())
|
||||
urlParams.Add("login", c.SenderID)
|
||||
|
||||
urlLink.RawQuery = urlParams.Encode()
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, urlLink.String(), nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resultBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result OsonResponse
|
||||
if err = json.Unmarshal(resultBytes, &result); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if result.Status != "ok" {
|
||||
return fmt.Errorf("sms service returned error status not 200: Status Code: %d Error: %s", resp.StatusCode, string(resultBytes))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// SendResult describes provider acceptance, not handset delivery.
|
||||
type SendResult struct {
|
||||
State, RequestID, MessageID, Code string
|
||||
}
|
||||
|
||||
var ErrSendUnknown = errors.New("sms acceptance unknown")
|
||||
var ErrSendRejected = errors.New("sms rejected")
|
||||
|
||||
type singleSendTransport struct {
|
||||
ctx context.Context
|
||||
base http.RoundTripper
|
||||
host string
|
||||
attempted atomic.Bool
|
||||
body []byte
|
||||
status int
|
||||
}
|
||||
|
||||
func (t *singleSendTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Scheme != "https" || req.URL.Host != t.host || t.attempted.Swap(true) {
|
||||
return nil, ErrSendUnknown
|
||||
}
|
||||
response, err := t.base.RoundTrip(req.Clone(t.ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.body, t.status = body, response.StatusCode
|
||||
response.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// SendMessageContext requires a fresh client per call. It deliberately disables
|
||||
// implicit SDK retries and validates acceptance independently of SendMessage.
|
||||
func SendMessageContext(ctx context.Context, client SmsClient, transport http.RoundTripper, params map[string]string, phone string) (SendResult, error) {
|
||||
result := SendResult{State: "unknown", Code: "acceptance_unknown"}
|
||||
if transport == nil || phone == "" {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
t := &singleSendTransport{ctx: ctx, base: transport}
|
||||
switch c := client.(type) {
|
||||
case *AliyunClient:
|
||||
t.host = "dysmsapi.aliyuncs.com"
|
||||
c.core.GetConfig().AutoRetry = false
|
||||
c.core.SetTransport(t)
|
||||
case *TencentClient:
|
||||
t.host = "sms.tencentcloudapi.com"
|
||||
c.core.WithHttpTransport(t)
|
||||
default:
|
||||
return result, ErrSendRejected
|
||||
}
|
||||
// Upstream errors may contain credentials/phone numbers and are not exposed.
|
||||
_ = client.SendMessage(params, phone)
|
||||
if t.status < 200 || t.status >= 300 {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
switch client.(type) {
|
||||
case *AliyunClient:
|
||||
var body struct{ Code, RequestId, BizId string }
|
||||
if json.Unmarshal(t.body, &body) != nil {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
result.RequestID = body.RequestId
|
||||
if body.Code != "" && body.Code != "OK" {
|
||||
result.State, result.Code = "rejected", body.Code
|
||||
return result, ErrSendRejected
|
||||
}
|
||||
if body.Code != "OK" || body.BizId == "" || body.RequestId == "" {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
result.MessageID = body.BizId
|
||||
case *TencentClient:
|
||||
var body struct {
|
||||
Response struct {
|
||||
RequestId string
|
||||
Error *struct{ Code string }
|
||||
SendStatusSet []struct{ Code, SerialNo string }
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(t.body, &body) != nil {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
result.RequestID = body.Response.RequestId
|
||||
if body.Response.Error != nil && body.Response.Error.Code != "" {
|
||||
result.State, result.Code = "rejected", body.Response.Error.Code
|
||||
return result, ErrSendRejected
|
||||
}
|
||||
if len(body.Response.SendStatusSet) != 1 {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
status := body.Response.SendStatusSet[0]
|
||||
if status.Code != "" && status.Code != "Ok" {
|
||||
result.State, result.Code = "rejected", status.Code
|
||||
return result, ErrSendRejected
|
||||
}
|
||||
if status.Code != "Ok" || status.SerialNo == "" || result.RequestID == "" {
|
||||
return result, ErrSendUnknown
|
||||
}
|
||||
result.MessageID = status.SerialNo
|
||||
}
|
||||
result.State, result.Code = "accepted", ""
|
||||
return result, nil
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SmsBaoClient struct {
|
||||
username string
|
||||
apikey string
|
||||
sign string
|
||||
template string
|
||||
goodsid string
|
||||
}
|
||||
|
||||
func GetSmsbaoClient(username string, apikey string, sign string, template string, other []string) (*SmsBaoClient, error) {
|
||||
var goodsid string
|
||||
if len(other) == 0 {
|
||||
goodsid = ""
|
||||
} else {
|
||||
goodsid = other[0]
|
||||
}
|
||||
return &SmsBaoClient{
|
||||
username: username,
|
||||
apikey: apikey,
|
||||
sign: sign,
|
||||
template: template,
|
||||
goodsid: goodsid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *SmsBaoClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
smsContent := url.QueryEscape("【" + c.sign + "】" + fmt.Sprintf(c.template, code))
|
||||
for _, mobile := range targetPhoneNumber {
|
||||
if strings.HasPrefix(mobile, "+86") {
|
||||
mobile = mobile[3:]
|
||||
} else if strings.HasPrefix(mobile, "+") {
|
||||
return fmt.Errorf("unsupported country code")
|
||||
}
|
||||
// https://api.smsbao.com/sms?u=USERNAME&p=PASSWORD&g=GOODSID&m=PHONE&c=CONTENT
|
||||
url := fmt.Sprintf("https://api.smsbao.com/sms?u=%s&p=%s&g=%s&m=%s&c=%s", c.username, c.apikey, c.goodsid, mobile, smsContent)
|
||||
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch string(body) {
|
||||
case "30":
|
||||
return fmt.Errorf("password error")
|
||||
case "40":
|
||||
return fmt.Errorf("account not exist")
|
||||
case "41":
|
||||
return fmt.Errorf("overdue account")
|
||||
case "43":
|
||||
return fmt.Errorf("IP address limit")
|
||||
case "50":
|
||||
return fmt.Errorf("content contain forbidden words")
|
||||
case "51":
|
||||
return fmt.Errorf("phone number incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SubmailClient struct {
|
||||
api string
|
||||
appid string
|
||||
signature string
|
||||
project string
|
||||
}
|
||||
|
||||
type SubmailResult struct {
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func buildSubmailPostdata(param map[string]string, appid string, signature string, project string, targetPhoneNumber []string) (map[string]string, error) {
|
||||
multi := make([]map[string]interface{}, 0, 32)
|
||||
|
||||
for _, phoneNumber := range targetPhoneNumber[0:] {
|
||||
multi = append(multi, map[string]interface{}{
|
||||
"to": phoneNumber,
|
||||
"vars": param,
|
||||
})
|
||||
}
|
||||
|
||||
m, err := json.Marshal(multi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
postdata := make(map[string]string)
|
||||
postdata["appid"] = appid
|
||||
postdata["signature"] = signature
|
||||
postdata["project"] = project
|
||||
postdata["multi"] = string(m)
|
||||
return postdata, nil
|
||||
}
|
||||
|
||||
func GetSubmailClient(appid string, signature string, project string) (*SubmailClient, error) {
|
||||
submailClient := &SubmailClient{
|
||||
api: "https://api-v4.mysubmail.com/sms/multixsend",
|
||||
appid: appid,
|
||||
signature: signature,
|
||||
project: project,
|
||||
}
|
||||
return submailClient, nil
|
||||
}
|
||||
|
||||
func (c *SubmailClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
postdata, err := buildSubmailPostdata(param, c.appid, c.signature, c.project, targetPhoneNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
for key, val := range postdata {
|
||||
err = writer.WriteField(key, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
contentType := writer.FormDataContentType()
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(c.api, contentType, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleSubmailResult(result)
|
||||
}
|
||||
|
||||
func handleSubmailResult(result []byte) error {
|
||||
var submailSuccessResult []SubmailResult
|
||||
err := json.Unmarshal(result, &submailSuccessResult)
|
||||
if err != nil {
|
||||
var submailErrorResult SubmailResult
|
||||
err = json.Unmarshal(result, &submailErrorResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if submailErrorResult.Msg != "" {
|
||||
return fmt.Errorf(submailErrorResult.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
errMsgs := []string{}
|
||||
for _, submailResult := range submailSuccessResult {
|
||||
if submailResult.Status != "success" {
|
||||
errMsg := fmt.Sprintf("%s, %d, %s", submailResult.Status, submailResult.Code, submailResult.Msg)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf(strings.Join(errMsgs, "|"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
|
||||
)
|
||||
|
||||
type TencentClient struct {
|
||||
core *sms.Client
|
||||
appId string
|
||||
sign string
|
||||
template string
|
||||
}
|
||||
|
||||
func GetTencentClient(accessId string, accessKey string, sign string, templateId string, appId []string) (*TencentClient, error) {
|
||||
if len(appId) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: appId")
|
||||
}
|
||||
|
||||
credential := common.NewCredential(accessId, accessKey)
|
||||
config := profile.NewClientProfile()
|
||||
config.HttpProfile.ReqMethod = "POST"
|
||||
|
||||
region := "ap-guangzhou"
|
||||
client, err := sms.NewClient(credential, region, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tencentClient := &TencentClient{
|
||||
core: client,
|
||||
appId: appId[0],
|
||||
sign: sign,
|
||||
template: templateId,
|
||||
}
|
||||
|
||||
return tencentClient, nil
|
||||
}
|
||||
|
||||
func (c *TencentClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
var paramArray []string
|
||||
index := 0
|
||||
for {
|
||||
value := param[strconv.Itoa(index)]
|
||||
if len(value) == 0 {
|
||||
break
|
||||
}
|
||||
paramArray = append(paramArray, value)
|
||||
index++
|
||||
}
|
||||
|
||||
request := sms.NewSendSmsRequest()
|
||||
request.SmsSdkAppId = common.StringPtr(c.appId)
|
||||
request.SignName = common.StringPtr(c.sign)
|
||||
request.TemplateParamSet = common.StringPtrs(paramArray)
|
||||
request.TemplateId = common.StringPtr(c.template)
|
||||
request.PhoneNumberSet = common.StringPtrs(targetPhoneNumber)
|
||||
|
||||
response, err := c.core.SendSms(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response == nil || response.Response == nil || len(response.Response.SendStatusSet) == 0 || response.Response.SendStatusSet[0] == nil || response.Response.SendStatusSet[0].Code == nil {
|
||||
return fmt.Errorf("missing send status")
|
||||
}
|
||||
if *response.Response.SendStatusSet[0].Code != "Ok" {
|
||||
return fmt.Errorf("sms rejected")
|
||||
}
|
||||
return err
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/twilio/twilio-go"
|
||||
openapi "github.com/twilio/twilio-go/rest/api/v2010"
|
||||
)
|
||||
|
||||
type TwilioClient struct {
|
||||
template string
|
||||
core *twilio.RestClient
|
||||
}
|
||||
|
||||
func GetTwilioClient(accessId string, accessKey string, template string) (*TwilioClient, error) {
|
||||
client := twilio.NewRestClientWithParams(twilio.ClientParams{
|
||||
Username: accessId,
|
||||
Password: accessKey,
|
||||
})
|
||||
|
||||
twilioClient := &TwilioClient{
|
||||
core: client,
|
||||
template: template,
|
||||
}
|
||||
|
||||
return twilioClient, nil
|
||||
}
|
||||
|
||||
// SendMessage targetPhoneNumber[0] is the sender's number, so targetPhoneNumber should have at least two parameters
|
||||
func (c *TwilioClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
bodyContent := fmt.Sprintf(c.template, code)
|
||||
|
||||
if len(targetPhoneNumber) < 2 {
|
||||
return fmt.Errorf("bad parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
params := &openapi.CreateMessageParams{}
|
||||
params.SetFrom(targetPhoneNumber[0])
|
||||
params.SetBody(bodyContent)
|
||||
|
||||
for i := 1; i < len(targetPhoneNumber); i++ {
|
||||
params.SetTo(targetPhoneNumber[i])
|
||||
_, err := c.core.Api.CreateMessage(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ucloud/ucloud-sdk-go/services/usms"
|
||||
"github.com/ucloud/ucloud-sdk-go/ucloud"
|
||||
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
|
||||
"github.com/ucloud/ucloud-sdk-go/ucloud/config"
|
||||
)
|
||||
|
||||
type UcloudClient struct {
|
||||
core *usms.USMSClient
|
||||
ProjectId string
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
Sign string
|
||||
Template string
|
||||
}
|
||||
|
||||
func GetUcloudClient(publicKey string, privateKey string, sign string, template string, projectId []string) (*UcloudClient, error) {
|
||||
if len(projectId) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: projectId")
|
||||
}
|
||||
|
||||
cfg := config.NewConfig()
|
||||
cfg.ProjectId = projectId[0]
|
||||
credential := auth.NewCredential()
|
||||
credential.PublicKey = publicKey
|
||||
credential.PrivateKey = privateKey
|
||||
|
||||
client := usms.NewClient(&cfg, &credential)
|
||||
|
||||
ucloudClient := &UcloudClient{
|
||||
core: client,
|
||||
ProjectId: projectId[0],
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
Sign: sign,
|
||||
Template: template,
|
||||
}
|
||||
|
||||
return ucloudClient, nil
|
||||
}
|
||||
|
||||
func (c *UcloudClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
code, ok := param["code"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing parameter: code")
|
||||
}
|
||||
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
req := c.core.NewSendUSMSMessageRequest()
|
||||
req.SigContent = ucloud.String(c.Sign)
|
||||
req.TemplateId = ucloud.String(c.Template)
|
||||
req.PhoneNumbers = targetPhoneNumber
|
||||
req.TemplateParams = []string{code}
|
||||
response, err := c.core.SendUSMSMessage(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.RetCode != 0 {
|
||||
return fmt.Errorf(response.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
unisms "github.com/apistd/uni-go-sdk/sms"
|
||||
)
|
||||
|
||||
type UnismsClient struct {
|
||||
core *unisms.UniSMSClient
|
||||
sign string
|
||||
template string
|
||||
}
|
||||
|
||||
func GetUnismsClient(accessId string, accessKey string, signature string, templateId string) (*UnismsClient, error) {
|
||||
client := unisms.NewClient(accessId, accessKey)
|
||||
|
||||
// Check the correctness of the accessId and accessKey
|
||||
msg := unisms.BuildMessage()
|
||||
msg.SetTo("test")
|
||||
msg.SetTemplateId("pub_verif_register") // free template
|
||||
_, err := client.Send(msg)
|
||||
if strings.Contains(err.Error(), "[104111] InvalidAccessKeyId") {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unismsClient := &UnismsClient{
|
||||
core: client,
|
||||
sign: signature,
|
||||
template: templateId,
|
||||
}
|
||||
|
||||
return unismsClient, nil
|
||||
}
|
||||
|
||||
func (c *UnismsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
msg := unisms.BuildMessage()
|
||||
msg.SetTo(targetPhoneNumber...)
|
||||
msg.SetSignature(c.sign)
|
||||
msg.SetTemplateId(c.template)
|
||||
|
||||
resp, err := c.core.Send(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Code != "0" {
|
||||
return errors.New(resp.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package go_sms_sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/volcengine/volc-sdk-golang/service/sms"
|
||||
)
|
||||
|
||||
type VolcClient struct {
|
||||
core *sms.SMS
|
||||
sign string
|
||||
template string
|
||||
smsAccount string
|
||||
}
|
||||
|
||||
func GetVolcClient(accessId, accessKey, sign, templateId string, smsAccount []string) (*VolcClient, error) {
|
||||
if len(smsAccount) == 0 {
|
||||
return nil, fmt.Errorf("missing parameter: smsAccount")
|
||||
}
|
||||
|
||||
client := sms.NewInstance()
|
||||
client.Client.SetAccessKey(accessId)
|
||||
client.Client.SetSecretKey(accessKey)
|
||||
|
||||
volcClient := &VolcClient{
|
||||
core: client,
|
||||
sign: sign,
|
||||
template: templateId,
|
||||
smsAccount: smsAccount[0],
|
||||
}
|
||||
|
||||
return volcClient, nil
|
||||
}
|
||||
|
||||
func (c *VolcClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
requestParam, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &sms.SmsRequest{
|
||||
SmsAccount: c.smsAccount,
|
||||
Sign: c.sign,
|
||||
TemplateID: c.template,
|
||||
TemplateParam: string(requestParam),
|
||||
PhoneNumbers: strings.Join(targetPhoneNumber, ","),
|
||||
}
|
||||
|
||||
resp, statusCode, err := c.core.Send(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send message failed, error: %q", err.Error())
|
||||
}
|
||||
if statusCode < 200 || statusCode > 299 {
|
||||
return fmt.Errorf("send message failed, statusCode: %d", statusCode)
|
||||
}
|
||||
if resp.ResponseMetadata.Error != nil {
|
||||
return fmt.Errorf("send message failed, code: %q, message: %q", resp.ResponseMetadata.Error.Code, resp.ResponseMetadata.Error.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user