mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
test capcha
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,9 @@ namespace amnezia
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
ApiCaptchaRequiredError = 1117,
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiRateLimitError = 1119,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
186
client/ui/qml/Controls2/CaptchaDialogType.qml
Normal file
186
client/ui/qml/Controls2/CaptchaDialogType.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user