test capcha

This commit is contained in:
Pavel Yaumenau
2026-04-21 16:55:47 +03:00
parent 650c1c6ebb
commit 8327de66dd
7 changed files with 375 additions and 12 deletions

View File

@@ -30,6 +30,15 @@ namespace
return value.isString() ? value.toString().trimmed() : QString();
}
QString apiErrorTokenFromJson(const QJsonObject &jsonObj)
{
const QString message = apiErrorMessageFromJson(jsonObj);
if (!message.isEmpty()) {
return message;
}
return jsonObj.value(QStringLiteral("error")).toString().trimmed();
}
QString escapeUnicode(const QString &input)
{
QString output;
@@ -137,15 +146,13 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodePaymentRequired = 402;
const int httpStatusCodeTooManyRequests = 429;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError;
}
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError;
@@ -163,32 +170,59 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeConflict) {
int status = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (status < 0) {
status = httpStatusCode;
}
if (status == httpStatusCodeTooManyRequests) {
return amnezia::ErrorCode::ApiRateLimitError;
}
if (status == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
}
return amnezia::ErrorCode::ApiConfigLimitError;
}
if (httpStatusFromBody == httpStatusCodeNotFound) {
if (status == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
if (status == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
if (status == httpStatusCodeUnprocessableEntity) {
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
if (status == httpStatusCodePaymentRequired) {
const QString errorToken = apiErrorTokenFromJson(jsonObj);
if (errorToken.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaInvalidError;
}
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|| errorToken.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|| errorToken.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRequiredError;
}
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
if (replyError == QNetworkReply::NoError && status > 0 && status < 400) {
return amnezia::ErrorCode::NoError;
}
if (status >= 400) {
return amnezia::ErrorCode::ApiConfigDownloadError;
}
}
qDebug() << "something went wrong";
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
}
qDebug() << "something went wrong" << replyErrorString;
return amnezia::ErrorCode::ApiConfigDownloadError;
}

View File

@@ -216,6 +216,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
if (errorCode) {
responseBody = decryptionResult.decryptedBody;
return errorCode;
}
@@ -263,7 +264,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish();
return;
}
@@ -434,6 +435,18 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
}
} else {
// Plaintext JSON error (e.g. HTTP 402 CAPTCHA) is not encrypted — do not treat as proxy failure.
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
const QJsonObject jsonObj = jsonDoc.object();
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))) {
return false;
}
const QString err = jsonObj.value(QStringLiteral("error")).toString();
if (err.contains(QLatin1String("captcha"), Qt::CaseInsensitive) || err == QLatin1String("rate_limit_exceeded")) {
return false;
}
}
qDebug() << "failed to decrypt the data";
return true;
}

View File

@@ -126,6 +126,9 @@ namespace amnezia
ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116,
ApiCaptchaRequiredError = 1117,
ApiCaptchaInvalidError = 1118,
ApiRateLimitError = 1119,
// QFile errors
OpenError = 1200,

View File

@@ -858,6 +858,28 @@ bool ApiConfigsController::importFreeFromGateway()
m_serversModel->addServer(serverConfig);
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
return true;
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
QString captchaId = jsonObj.value("captcha_id").toString();
QString captchaImage = jsonObj.value("captcha_image").toString();
QString hint = jsonObj.value("hint").toString(tr("Please solve the CAPTCHA to continue"));
m_captchaState.apiPayload = apiPayload;
m_captchaState.endpoint = QString("%1v1/config");
m_captchaState.serviceProtocol = gatewayRequestData.serviceProtocol;
m_captchaState.openvpnPrivKey = protocolData.certRequest.privKey;
m_captchaState.wireguardClientPrivKey = protocolData.wireGuardClientPrivKey;
m_captchaState.wireguardClientPubKey = protocolData.wireGuardClientPubKey;
m_captchaState.xrayUuid = protocolData.xrayUuid;
m_captchaState.isPending = true;
emit captchaRequired(captchaId, captchaImage, hint);
return false;
}
emit errorOccurred(errorCode);
return false;
} else {
emit errorOccurred(errorCode);
return false;
@@ -1276,3 +1298,71 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}
void ApiConfigsController::onCaptchaSolved(const QString &captchaId, const QString &solution)
{
if (!m_captchaState.isPending) {
emit errorOccurred(ErrorCode::InternalError);
return;
}
m_captchaState.isPending = false;
QJsonObject apiPayload = m_captchaState.apiPayload;
apiPayload.insert("captcha_id", captchaId);
apiPayload.insert("captcha_solution", solution);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(m_captchaState.endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::ApiCaptchaInvalidError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
QString newCaptchaId = jsonObj.value("captcha_id").toString();
QString newCaptchaImage = jsonObj.value("captcha_image").toString();
QString hint = jsonObj.value("hint").toString(tr("Invalid CAPTCHA. Please try again"));
m_captchaState.apiPayload = apiPayload;
m_captchaState.isPending = true;
emit captchaRequired(newCaptchaId, newCaptchaImage, hint);
return;
}
emit errorOccurred(errorCode);
return;
}
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
// Reconstruct ProtocolData from saved state
ProtocolData protocolData;
protocolData.certRequest.privKey = m_captchaState.openvpnPrivKey;
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
protocolData.xrayUuid = m_captchaState.xrayUuid;
QJsonObject serverConfig;
errorCode = fillServerConfig(m_captchaState.serviceProtocol, protocolData, responseBody, serverConfig);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonObject apiConfig = serverConfig.value(configKey::apiConfig).toObject();
apiConfig.insert(configKey::userCountryCode, m_apiServicesModel->getCountryCode());
apiConfig.insert(configKey::serviceType, m_apiServicesModel->getSelectedServiceType());
apiConfig.insert(configKey::serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol());
serverConfig.insert(configKey::apiConfig, apiConfig);
QJsonObject authData = serverConfig.value(configKey::authData).toObject();
authData.insert(QStringLiteral("captcha_solution"), solution);
serverConfig.insert(configKey::authData, authData);
m_serversModel->addServer(serverConfig);
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
}

View File

@@ -47,6 +47,8 @@ public slots:
void setCurrentProtocol(const QString &protocolName);
bool isVlessProtocol();
void onCaptchaSolved(const QString &captchaId, const QString &solution);
signals:
void errorOccurred(ErrorCode errorCode);
void trialEmailError(const QString &message);
@@ -59,6 +61,7 @@ signals:
void updateServerFromApiFinished();
void vpnKeyExportReady();
void captchaRequired(const QString &captchaId, const QString &captchaImageBase64, const QString &hint);
private:
QList<QString> getQrCodes();
@@ -77,6 +80,18 @@ private:
QSharedPointer<ApiSubscriptionPlansModel> m_subscriptionPlansModel;
QSharedPointer<ApiBenefitsModel> m_benefitsModel;
// CAPTCHA handling state
struct CaptchaState {
QJsonObject apiPayload;
QString endpoint;
QString serviceProtocol;
QString openvpnPrivKey;
QString wireguardClientPrivKey;
QString wireguardClientPubKey;
QString xrayUuid;
bool isPending = false;
} m_captchaState;
};
#endif

View File

@@ -0,0 +1,186 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "TextTypes"
import "../Config"
Popup {
id: root
property string captchaId
property string captchaImageBase64
property string hint: "Please solve the CAPTCHA to continue"
signal captchaSolved(string captchaId, string solution)
leftMargin: 25
rightMargin: 25
bottomMargin: 70 + SettingsController.safeAreaBottomMargin
width: parent.width - leftMargin - rightMargin
anchors.centerIn: parent
modal: true
closePolicy: Popup.NoAutoClose
Overlay.modal: Rectangle {
color: AmneziaStyle.color.translucentMidnightBlack
}
onOpened: {
timer.start()
solutionInput.text = ""
solutionInput.focus = true
}
onClosed: {
FocusController.dropRootObject(root)
}
background: Rectangle {
anchors.fill: parent
color: "white"
radius: 4
}
Timer {
id: timer
interval: 200
onTriggered: {
FocusController.pushRootObject(root)
FocusController.setFocusItem(solutionInput)
}
repeat: false
running: true
}
contentItem: Item {
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
anchors.fill: parent
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 16
anchors.bottomMargin: 16
spacing: 12
CaptionTextType {
text: qsTr("CAPTCHA Verification")
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
}
ParagraphTextType {
text: hint
Layout.fillWidth: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 200
color: AmneziaStyle.color.lightGray
radius: 4
Image {
id: captchaImage
anchors.centerIn: parent
cache: false
Component.onCompleted: {
if (captchaImageBase64 !== "") {
source = "data:image/png;base64," + captchaImageBase64
}
}
Connections {
target: root
function onCaptchaImageBase64Changed() {
captchaImage.source = "data:image/png;base64," + root.captchaImageBase64
}
}
}
BusyIndicator {
anchors.centerIn: parent
running: captchaImage.status === Image.Loading
}
}
ParagraphTextType {
text: qsTr("Enter the numbers from the image:")
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
}
TextField {
id: solutionInput
Layout.fillWidth: true
implicitHeight: 40
placeholderText: qsTr("Enter CAPTCHA solution")
background: Rectangle {
border.color: AmneziaStyle.color.charcoalGray
border.width: 1
radius: 4
color: "white"
}
onAccepted: {
if (solutionInput.text.trim() !== "") {
root.captchaSolved(root.captchaId, solutionInput.text.trim())
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
BasicButtonType {
id: submitButton
Layout.fillWidth: true
implicitHeight: 40
text: qsTr("Submit")
onClicked: {
if (solutionInput.text.trim() !== "") {
root.captchaSolved(root.captchaId, solutionInput.text.trim())
}
}
}
BasicButtonType {
id: cancelButton
Layout.fillWidth: true
implicitHeight: 40
text: qsTr("Cancel")
defaultColor: AmneziaStyle.color.lightGray
hoveredColor: AmneziaStyle.color.charcoalGray
textColor: AmneziaStyle.color.midnightBlack
onClicked: {
root.close()
}
}
}
}
}
}

View File

@@ -201,6 +201,21 @@ Window {
}
}
Item {
objectName: "captchaDialogItem"
anchors.fill: parent
CaptchaDialogType {
id: captchaDialog
onCaptchaSolved: function(captchaId, solution) {
captchaDialog.close()
ApiConfigsController.onCaptchaSolved(captchaId, solution)
}
}
}
Item {
objectName: "privateKeyPassphraseDrawerItem"
@@ -306,6 +321,13 @@ Window {
function onSubscriptionExpiredOnServer() {
subscriptionExpiredDrawer.openTriggered()
}
function onCaptchaRequired(captchaId, captchaImageBase64, hint) {
captchaDialog.captchaId = captchaId
captchaDialog.captchaImageBase64 = captchaImageBase64
captchaDialog.hint = hint
captchaDialog.open()
}
}
Connections {