You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
6.5 KiB

<?php
namespace App\Services\Sms;
use App\Models\SmsVerification;
use App\Support\SmsConfig;
use Illuminate\Support\Facades\Http;
class TencentCloudSmsSender implements SmsSender
{
private const ACTION = 'SendSms';
private const VERSION = '2021-01-11';
private const SERVICE = 'sms';
private const ALGORITHM = 'TC3-HMAC-SHA256';
public function sendVerification(SmsVerification $record): SmsSendResult
{
try {
SmsConfig::assertReadyForRealSending();
} catch (\Throwable $e) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'CONFIG_INCOMPLETE',
$e->getMessage(),
['error' => 'config_incomplete']
);
}
$endpoint = (string) config('sms.tencentcloud.endpoint');
$region = (string) config('sms.tencentcloud.region');
$secretId = (string) config('sms.tencentcloud.secret_id');
$secretKey = (string) config('sms.tencentcloud.secret_key');
$timeout = (int) config('sms.timeout', 5);
$payload = [
'PhoneNumberSet' => ['+86'.$record->mobile],
'SmsSdkAppId' => (string) config('sms.tencentcloud.sdk_app_id'),
'SignName' => (string) config('sms.tencentcloud.sign_name'),
'TemplateId' => (string) config('sms.tencentcloud.template_id'),
'TemplateParamSet' => [
(string) $record->code,
(string) data_get($record->request_payload_json, 'ttl_minutes', '5'),
],
];
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($body === false) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'PAYLOAD_ENCODE_FAILED',
'Tencent Cloud SMS payload encode failed.',
['error' => 'payload_encode_failed']
);
}
$timestamp = time();
$headers = $this->buildHeaders($endpoint, $region, $secretId, $secretKey, $timestamp, $body);
try {
$response = Http::timeout($timeout)
->withHeaders($headers)
->withBody($body, 'application/json; charset=utf-8')
->post('https://'.$endpoint);
} catch (\Throwable $e) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'NETWORK_ERROR',
$e->getMessage(),
['error' => 'network_error']
);
}
$json = $response->json();
if (! is_array($json)) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'INVALID_RESPONSE',
'Tencent Cloud SMS returned a non-json response.',
['http_status' => $response->status()]
);
}
$tencentResponse = data_get($json, 'Response', []);
$requestId = data_get($tencentResponse, 'RequestId');
if (data_get($tencentResponse, 'Error.Code')) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
is_string($requestId) ? $requestId : null,
(string) data_get($tencentResponse, 'Error.Code'),
(string) data_get($tencentResponse, 'Error.Message'),
$this->responseSummary($json)
);
}
$status = data_get($tencentResponse, 'SendStatusSet.0', []);
$providerCode = (string) data_get($status, 'Code', '');
$providerMessage = (string) data_get($status, 'Message', '');
if ($response->successful() && $providerCode === 'Ok') {
return SmsSendResult::success(
SmsVerification::PROVIDER_TENCENTCLOUD,
is_string($requestId) ? $requestId : null,
$providerCode,
$providerMessage,
$this->responseSummary($json)
);
}
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
is_string($requestId) ? $requestId : null,
$providerCode !== '' ? $providerCode : 'HTTP_'.$response->status(),
$providerMessage !== '' ? $providerMessage : 'Tencent Cloud SMS send failed.',
$this->responseSummary($json)
);
}
/**
* @return array<string, string>
*/
private function buildHeaders(
string $host,
string $region,
string $secretId,
string $secretKey,
int $timestamp,
string $payload,
): array {
$date = gmdate('Y-m-d', $timestamp);
$canonicalHeaders = "content-type:application/json; charset=utf-8\nhost:{$host}\n";
$signedHeaders = 'content-type;host';
$canonicalRequest = implode("\n", [
'POST',
'/',
'',
$canonicalHeaders,
$signedHeaders,
hash('sha256', $payload),
]);
$credentialScope = $date.'/'.self::SERVICE.'/tc3_request';
$stringToSign = implode("\n", [
self::ALGORITHM,
(string) $timestamp,
$credentialScope,
hash('sha256', $canonicalRequest),
]);
$secretDate = hash_hmac('sha256', $date, 'TC3'.$secretKey, true);
$secretService = hash_hmac('sha256', self::SERVICE, $secretDate, true);
$secretSigning = hash_hmac('sha256', 'tc3_request', $secretService, true);
$signature = hash_hmac('sha256', $stringToSign, $secretSigning);
$authorization = self::ALGORITHM
.' Credential='.$secretId.'/'.$credentialScope
.', SignedHeaders='.$signedHeaders
.', Signature='.$signature;
return [
'Authorization' => $authorization,
'Content-Type' => 'application/json; charset=utf-8',
'Host' => $host,
'X-TC-Action' => self::ACTION,
'X-TC-Region' => $region,
'X-TC-Timestamp' => (string) $timestamp,
'X-TC-Version' => self::VERSION,
];
}
/**
* @param array<string, mixed> $response
* @return array<string, mixed>
*/
private function responseSummary(array $response): array
{
return [
'request_id' => data_get($response, 'Response.RequestId'),
'error' => data_get($response, 'Response.Error'),
'send_status_set' => data_get($response, 'Response.SendStatusSet'),
];
}
}