[SMS] Diagnose Kavenegar failures; sanitize API key in URL path
A 404 from Kavenegar means a malformed URL path, and the API key sits in the path unescaped, so a stray space/newline/slash in the saved key breaks it. Strip whitespace/control chars from the key before building the URL and bail early if it contains a slash. Also read and log Kavenegar's response body and return.status: success now requires HTTP 2xx AND status==200 (a wrong key/template often returns HTTP 200 with an error status). Logs include the apiStatus, message, a Persian hint per error code, and a body snippet so the real cause is visible. No schema change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using JobsMedical.Web.Models;
|
using JobsMedical.Web.Models;
|
||||||
|
|
||||||
namespace JobsMedical.Web.Services;
|
namespace JobsMedical.Web.Services;
|
||||||
@@ -27,33 +28,61 @@ public class KavenegarSmsSender : ISmsSender
|
|||||||
public async Task<bool> SendOtpAsync(string phone, string code, AppSetting s, CancellationToken ct = default)
|
public async Task<bool> SendOtpAsync(string phone, string code, AppSetting s, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!s.SmsEnabled || string.IsNullOrWhiteSpace(s.SmsApiKey)) return false;
|
if (!s.SmsEnabled || string.IsNullOrWhiteSpace(s.SmsApiKey)) return false;
|
||||||
|
|
||||||
|
// The API key is part of the URL path; clean it so a stray space/newline/slash
|
||||||
|
// doesn't turn the request into a 404 (a malformed path). It is NOT a query value,
|
||||||
|
// so don't percent-encode it — just strip whitespace/control chars.
|
||||||
|
var apiKey = new string(s.SmsApiKey.Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||||
|
if (apiKey.Contains('/'))
|
||||||
|
{
|
||||||
|
_log.LogWarning("Kavenegar API key looks malformed (contains '/'). Check the value in admin settings.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = _http.CreateClient("sms");
|
var client = _http.CreateClient("sms");
|
||||||
client.Timeout = TimeSpan.FromSeconds(15);
|
client.Timeout = TimeSpan.FromSeconds(15);
|
||||||
string url;
|
string url; string method;
|
||||||
if (!string.IsNullOrWhiteSpace(s.SmsTemplate))
|
if (!string.IsNullOrWhiteSpace(s.SmsTemplate))
|
||||||
{
|
{
|
||||||
|
method = "verify/lookup";
|
||||||
// verify/lookup: template contains %token → the code
|
// verify/lookup: template contains %token → the code
|
||||||
url = $"https://api.kavenegar.com/v1/{s.SmsApiKey}/verify/lookup.json" +
|
url = $"https://api.kavenegar.com/v1/{apiKey}/verify/lookup.json" +
|
||||||
$"?receptor={Uri.EscapeDataString(phone)}&token={Uri.EscapeDataString(code)}" +
|
$"?receptor={Uri.EscapeDataString(phone)}&token={Uri.EscapeDataString(code)}" +
|
||||||
$"&template={Uri.EscapeDataString(s.SmsTemplate)}";
|
$"&template={Uri.EscapeDataString(s.SmsTemplate.Trim())}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
method = "sms/send";
|
||||||
var msg = $"کد ورود همکادر: {code}";
|
var msg = $"کد ورود همکادر: {code}";
|
||||||
url = $"https://api.kavenegar.com/v1/{s.SmsApiKey}/sms/send.json" +
|
url = $"https://api.kavenegar.com/v1/{apiKey}/sms/send.json" +
|
||||||
$"?receptor={Uri.EscapeDataString(phone)}&message={Uri.EscapeDataString(msg)}" +
|
$"?receptor={Uri.EscapeDataString(phone)}&message={Uri.EscapeDataString(msg)}" +
|
||||||
(string.IsNullOrWhiteSpace(s.SmsSender) ? "" : $"&sender={Uri.EscapeDataString(s.SmsSender)}");
|
(string.IsNullOrWhiteSpace(s.SmsSender) ? "" : $"&sender={Uri.EscapeDataString(s.SmsSender.Trim())}");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var resp = await client.GetAsync(url, ct);
|
using var resp = await client.GetAsync(url, ct);
|
||||||
if (!resp.IsSuccessStatusCode)
|
var body = await resp.Content.ReadAsStringAsync(ct);
|
||||||
{
|
var (apiStatus, apiMessage) = ParseKavenegar(body);
|
||||||
_log.LogWarning("Kavenegar OTP HTTP {Status} for {Phone}", (int)resp.StatusCode, phone);
|
|
||||||
return false;
|
// Kavenegar success = HTTP 2xx AND return.status == 200. A wrong key/template
|
||||||
}
|
// often comes back as HTTP 200 with an error status, so check both.
|
||||||
return true;
|
if (resp.IsSuccessStatusCode && apiStatus == 200) return true;
|
||||||
|
|
||||||
|
string hint = (int)resp.StatusCode == 404
|
||||||
|
? " — HTTP 404 معمولاً یعنی کلید API نادرست/خراب است یا متد اشتباه (مسیر آدرس معتبر نیست)."
|
||||||
|
: apiStatus switch
|
||||||
|
{
|
||||||
|
401 or 402 => " — کلید API نامعتبر یا حساب غیرفعال است.",
|
||||||
|
407 or 431 or 432 => " — متن/تمپلیت تأیید نشده یا نام تمپلیت اشتباه است.",
|
||||||
|
411 or 412 => " — شماره گیرنده یا فرستنده نامعتبر است.",
|
||||||
|
418 => " — اعتبار حساب کافی نیست.",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
_log.LogWarning(
|
||||||
|
"Kavenegar OTP failed for {Phone} via {Method}: HTTP {Http}, apiStatus={ApiStatus}, message={Message}{Hint} | body: {Body}",
|
||||||
|
phone, method, (int)resp.StatusCode, apiStatus, apiMessage, hint, Truncate(body, 300));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -61,4 +90,25 @@ public class KavenegarSmsSender : ISmsSender
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Parse Kavenegar's { "return": { "status": N, "message": "…" } } envelope.</summary>
|
||||||
|
private static (int? status, string? message) ParseKavenegar(string body)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(body)) return (null, null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
if (doc.RootElement.TryGetProperty("return", out var ret))
|
||||||
|
{
|
||||||
|
int? status = ret.TryGetProperty("status", out var st) && st.TryGetInt32(out var n) ? n : null;
|
||||||
|
string? msg = ret.TryGetProperty("message", out var m) ? m.GetString() : null;
|
||||||
|
return (status, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException) { /* not JSON (e.g. a 404 HTML page) — fall through */ }
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string s, int max) =>
|
||||||
|
string.IsNullOrEmpty(s) ? "" : (s.Length <= max ? s : s[..max] + "…");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user