mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
fixed open QR code screen & fix iOS scanner
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
@property (nonatomic) QRCodeReader* qrCodeReader;
|
@property (nonatomic) QRCodeReader* qrCodeReader;
|
||||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||||
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
||||||
|
@property (nonatomic) dispatch_queue_t sessionQueue;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@
|
|||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
|
|
||||||
_captureSession = nil;
|
_captureSession = nil;
|
||||||
|
if (!_sessionQueue) {
|
||||||
|
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setQrCodeReader: (QRCodeReader*)value {
|
- (void)setQrCodeReader: (QRCodeReader*)value {
|
||||||
@@ -48,9 +52,10 @@
|
|||||||
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
|
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
|
||||||
[_captureSession addOutput:capturedMetadataOutput];
|
[_captureSession addOutput:capturedMetadataOutput];
|
||||||
|
|
||||||
dispatch_queue_t dispatchQueue;
|
if (!_sessionQueue) {
|
||||||
dispatchQueue = dispatch_queue_create("myQueue", NULL);
|
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||||
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue];
|
}
|
||||||
|
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: _sessionQueue];
|
||||||
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
||||||
|
|
||||||
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
|
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
|
||||||
@@ -69,7 +74,10 @@
|
|||||||
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
|
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
|
||||||
[layer addSublayer: _videoPreviewPlayer];
|
[layer addSublayer: _videoPreviewPlayer];
|
||||||
|
|
||||||
[_captureSession startRunning];
|
AVCaptureSession *session = _captureSession;
|
||||||
|
dispatch_async(_sessionQueue, ^{
|
||||||
|
[session startRunning];
|
||||||
|
});
|
||||||
|
|
||||||
NSLog(@"[QRCodeReader] startReading OK frame=(%.1f,%.1f,%.1f,%.1f) statusBar=%.1f",
|
NSLog(@"[QRCodeReader] startReading OK frame=(%.1f,%.1f,%.1f,%.1f) statusBar=%.1f",
|
||||||
cameraCGRect.origin.x, cameraCGRect.origin.y, cameraCGRect.size.width, cameraCGRect.size.height, statusBarHeight);
|
cameraCGRect.origin.x, cameraCGRect.origin.y, cameraCGRect.size.width, cameraCGRect.size.height, statusBarHeight);
|
||||||
@@ -79,7 +87,10 @@
|
|||||||
|
|
||||||
- (void)stopReading {
|
- (void)stopReading {
|
||||||
if (_captureSession) {
|
if (_captureSession) {
|
||||||
[_captureSession stopRunning];
|
AVCaptureSession *session = _captureSession;
|
||||||
|
dispatch_async(_sessionQueue, ^{
|
||||||
|
[session stopRunning];
|
||||||
|
});
|
||||||
_captureSession = nil;
|
_captureSession = nil;
|
||||||
}
|
}
|
||||||
if (_videoPreviewPlayer) {
|
if (_videoPreviewPlayer) {
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr auto kGenerateQrPath = "%1api/v1/generate_qr";
|
constexpr auto kGenerateQrPath = "%1api/v1/generate_qr";
|
||||||
constexpr auto kScanQrPath = "%1api/v1/scan_qr";
|
constexpr auto kScanQrPath = "%1api/v1/scan_qr";
|
||||||
|
constexpr auto kGatewayProbePath = "%1v1/news";
|
||||||
constexpr int kPairingRetryMaxAttempts = 3;
|
constexpr int kPairingRetryMaxAttempts = 3;
|
||||||
|
constexpr int kGatewayProbeTimeoutMsecs = 3000;
|
||||||
|
|
||||||
bool isPairingRetriableError(ErrorCode code)
|
bool isPairingRetriableError(ErrorCode code)
|
||||||
{
|
{
|
||||||
@@ -282,6 +284,26 @@ void PairingUiController::setPhoneBusy(bool busy)
|
|||||||
emit phonePairingBusyChanged();
|
emit phonePairingBusyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PairingUiController::canOpenTvQrPairingPage()
|
||||||
|
{
|
||||||
|
if (!m_appSettingsRepository) {
|
||||||
|
emit errorOccurred(ErrorCode::InternalError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool isTestPurchase = false;
|
||||||
|
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||||
|
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), kGatewayProbeTimeoutMsecs,
|
||||||
|
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||||
|
QByteArray responseBody;
|
||||||
|
const ErrorCode err = gatewayController.post(QString::fromLatin1(kGatewayProbePath), QJsonObject {}, responseBody);
|
||||||
|
if (err != ErrorCode::NoError) {
|
||||||
|
emit errorOccurred(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void PairingUiController::resetTvQrDisplay()
|
void PairingUiController::resetTvQrDisplay()
|
||||||
{
|
{
|
||||||
m_tvQrCodes.clear();
|
m_tvQrCodes.clear();
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
/** Fast preflight before opening receive QR page; emits errorOccurred on failure. */
|
||||||
|
bool canOpenTvQrPairingPage();
|
||||||
void startTvQrSession();
|
void startTvQrSession();
|
||||||
void cancelTvQrSession();
|
void cancelTvQrSession();
|
||||||
/** TV receive + phone send: call when leaving QR pairing (back / pop) so long-poll state does not stick. */
|
/** TV receive + phone send: call when leaving QR pairing (back / pop) so long-poll state does not stick. */
|
||||||
|
|||||||
@@ -33,12 +33,15 @@ PageType {
|
|||||||
target: PairingUiController
|
target: PairingUiController
|
||||||
|
|
||||||
function onPhonePairingSucceeded() {
|
function onPhonePairingSucceeded() {
|
||||||
|
const serverIndex = ServersUiController.getProcessedServerIndex()
|
||||||
|
if (serverIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SubscriptionUiController.getAccountInfo(serverIndex, true)
|
||||||
|
SubscriptionUiController.updateApiDevicesModel()
|
||||||
if (!root.visible) {
|
if (!root.visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const serverIndex = ServersUiController.getProcessedServerIndex()
|
|
||||||
SubscriptionUiController.getAccountInfo(serverIndex, true)
|
|
||||||
SubscriptionUiController.updateApiDevicesModel()
|
|
||||||
const label = PairingUiController.lastSuccessfulPhonePairingDisplayName
|
const label = PairingUiController.lastSuccessfulPhonePairingDisplayName
|
||||||
if (label.length > 0) {
|
if (label.length > 0) {
|
||||||
PageController.showNotificationMessage(
|
PageController.showNotificationMessage(
|
||||||
|
|||||||
@@ -353,7 +353,12 @@ PageType {
|
|||||||
property string imageSource: "qrc:/images/controls/folder-search-2.svg"
|
property string imageSource: "qrc:/images/controls/folder-search-2.svg"
|
||||||
property bool isVisible: true
|
property bool isVisible: true
|
||||||
property var handler: function() {
|
property var handler: function() {
|
||||||
PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive)
|
PageController.showBusyIndicator(true)
|
||||||
|
var result = PairingUiController.canOpenTvQrPairingPage()
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
if (result) {
|
||||||
|
PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+77
-12
@@ -32,6 +32,7 @@ var (
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session)
|
requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session)
|
||||||
sessions = map[string]*pairingSession{}
|
sessions = map[string]*pairingSession{}
|
||||||
|
issued = map[string]issuedConfigInfo{} // installation_uuid -> issued config info shown in /v1/account_info
|
||||||
|
|
||||||
// Configured from flags / env in main().
|
// Configured from flags / env in main().
|
||||||
pairingSessionTTL = 30 * time.Second
|
pairingSessionTTL = 30 * time.Second
|
||||||
@@ -70,11 +71,31 @@ type pairingResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type pairingSession struct {
|
type pairingSession struct {
|
||||||
QRUUID string
|
QRUUID string
|
||||||
ExpiresAt time.Time
|
DesktopInstallUUID string
|
||||||
Done chan struct{}
|
DesktopOSVersion string
|
||||||
Result *pairingResult
|
ExpiresAt time.Time
|
||||||
Completed bool
|
Done chan struct{}
|
||||||
|
Result *pairingResult
|
||||||
|
Completed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type issuedConfigInfo struct {
|
||||||
|
InstallationUUID string `json:"installation_uuid"`
|
||||||
|
WorkerLastUpdated string `json:"worker_last_updated"`
|
||||||
|
LastDownloaded string `json:"last_downloaded"`
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
OSVersion string `json:"os_version"`
|
||||||
|
ServerCountryCode string `json:"server_country_code"`
|
||||||
|
ServerCountryName string `json:"server_country_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringFromMap(m map[string]any, key string) string {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
v, _ := m[key].(string)
|
||||||
|
return strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||||
@@ -184,9 +205,11 @@ func handleGenerateQR(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session := &pairingSession{
|
session := &pairingSession{
|
||||||
QRUUID: req.QRUUID,
|
QRUUID: req.QRUUID,
|
||||||
ExpiresAt: time.Now().Add(pairingSessionTTL),
|
DesktopInstallUUID: req.InstallationUUID,
|
||||||
Done: make(chan struct{}),
|
DesktopOSVersion: req.OSVersion,
|
||||||
|
ExpiresAt: time.Now().Add(pairingSessionTTL),
|
||||||
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -276,12 +299,44 @@ func handleScanQR(w http.ResponseWriter, r *http.Request) {
|
|||||||
ServiceInfo: req.ServiceInfo,
|
ServiceInfo: req.ServiceInfo,
|
||||||
SupportedProto: req.SupportedProto,
|
SupportedProto: req.SupportedProto,
|
||||||
}
|
}
|
||||||
|
nowISO := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
countryCode := stringFromMap(req.ServiceInfo, "server_country_code")
|
||||||
|
if countryCode == "" {
|
||||||
|
countryCode = stringFromMap(req.ServiceInfo, "country_code")
|
||||||
|
}
|
||||||
|
if countryCode == "" {
|
||||||
|
countryCode = "ZZ"
|
||||||
|
}
|
||||||
|
countryName := stringFromMap(req.ServiceInfo, "server_country_name")
|
||||||
|
if countryName == "" {
|
||||||
|
countryName = stringFromMap(req.ServiceInfo, "country_name")
|
||||||
|
}
|
||||||
|
if countryName == "" {
|
||||||
|
countryName = "Mock Country"
|
||||||
|
}
|
||||||
|
desktopUUID := strings.TrimSpace(session.DesktopInstallUUID)
|
||||||
|
if desktopUUID == "" {
|
||||||
|
desktopUUID = strings.TrimSpace(req.InstallationUUID)
|
||||||
|
}
|
||||||
|
desktopOS := strings.TrimSpace(session.DesktopOSVersion)
|
||||||
|
if desktopOS == "" {
|
||||||
|
desktopOS = strings.TrimSpace(req.OSVersion)
|
||||||
|
}
|
||||||
|
issued[desktopUUID] = issuedConfigInfo{
|
||||||
|
InstallationUUID: desktopUUID,
|
||||||
|
WorkerLastUpdated: nowISO,
|
||||||
|
LastDownloaded: nowISO,
|
||||||
|
SourceType: "gateway_account",
|
||||||
|
OSVersion: desktopOS,
|
||||||
|
ServerCountryCode: countryCode,
|
||||||
|
ServerCountryName: countryName,
|
||||||
|
}
|
||||||
session.Completed = true
|
session.Completed = true
|
||||||
close(session.Done)
|
close(session.Done)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
log.Printf("pairing COMPLETED uuid=%s phone_install=%s config_len=%d proto_count=%d",
|
log.Printf("pairing COMPLETED uuid=%s phone_install=%s desktop_install=%s config_len=%d proto_count=%d",
|
||||||
shortID(req.QRUUID), shortID(req.InstallationUUID), len(req.Config), len(req.SupportedProto))
|
shortID(req.QRUUID), shortID(req.InstallationUUID), shortID(desktopUUID), len(req.Config), len(req.SupportedProto))
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"message": "OK"})
|
writeJSON(w, http.StatusOK, map[string]string{"message": "OK"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,17 +497,27 @@ func handleAccountInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
drainBody(r)
|
drainBody(r)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
issuedConfigs := make([]issuedConfigInfo, 0, len(issued))
|
||||||
|
for _, cfg := range issued {
|
||||||
|
issuedConfigs = append(issuedConfigs, cfg)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
sort.Slice(issuedConfigs, func(i, j int) bool {
|
||||||
|
return issuedConfigs[i].InstallationUUID < issuedConfigs[j].InstallationUUID
|
||||||
|
})
|
||||||
|
|
||||||
// Keys match client/core/utils/constants/apiKeys.h (snake_case).
|
// Keys match client/core/utils/constants/apiKeys.h (snake_case).
|
||||||
endDate := time.Now().UTC().AddDate(1, 0, 0).Format(time.RFC3339)
|
endDate := time.Now().UTC().AddDate(1, 0, 0).Format(time.RFC3339)
|
||||||
resp := map[string]any{
|
resp := map[string]any{
|
||||||
"active_device_count": 1,
|
"active_device_count": len(issuedConfigs),
|
||||||
"max_device_count": 5,
|
"max_device_count": 5,
|
||||||
"subscription_end_date": endDate,
|
"subscription_end_date": endDate,
|
||||||
"subscription_description": "Local mock (tools/local_gateway)",
|
"subscription_description": "Local mock (tools/local_gateway)",
|
||||||
"is_renewal_available": false,
|
"is_renewal_available": false,
|
||||||
"supported_protocols": []string{"awg", "vless"},
|
"supported_protocols": []string{"awg", "vless"},
|
||||||
"available_countries": []any{},
|
"available_countries": []any{},
|
||||||
"issued_configs": []any{},
|
"issued_configs": issuedConfigs,
|
||||||
"support_info": map[string]any{
|
"support_info": map[string]any{
|
||||||
"telegram": "amnezia_support",
|
"telegram": "amnezia_support",
|
||||||
"email": "support@example.com",
|
"email": "support@example.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user