Menu
FR
AdGuard VPN Blog L'Extension de navigateur AdGuard VPN a migré sur Manifest V3

L'Extension de navigateur AdGuard VPN a migré sur Manifest V3

Manifest V3, la nouvelle version de l'API Chrome, est devenue une pierre tombale pour de nombreux développeurs. Elle apporte des modifications importantes aux règles que les extensions de navigateur doivent respecter. Ces changements entraînent non seulement des désagréments, mais aussi une perte partielle de fonctionnalité.

En août 2022, AdGuard a lancé la première extension de navigateur bloquant les publicités, basée sur Manifest V3. Le développement a été difficile : nous avons rencontré de nombreux problèmes en cours de route. Cependant, nous avons alors prouvé que les bloqueurs de publicité avaient un avenir, même dans la réalité de Manifest V3.

Bien que Google ait prolongé la durée de vie des extensions Manifest V2 jusqu'à au moins janvier 2024, nous n'avons pas reporté le développement de l'extension de navigateur AdGuard VPN qui fonctionne selon les règles de Manifest V3, et nous sommes prêts à vous la présenter maintenant.

L'Extension de navigateur AdGuard VPN MV3

Avec notre expérience dans le développement d'AdGuard MV3, nous ne nous attendions pas du tout à ce que le travail sur AdGuard VPN MV3 soit une promenade tranquille, et nous avions raison. Parlons des problèmes que nous avons rencontrés et de la façon dont nous les avons résolus.

Le service worker s'endort

La version précédente de Manifest disposait d'une page d'arrière-plan qui permettait à l'extension de s'exécuter une fois après l'installation ou à l'ouverture d'un navigateur, et de continuer à travailler en arrière-plan. Dans Manifest V3, la page d'arrière-plan est remplacée par le service worker.

Il permet aux extensions de s'exécuter en arrière-plan uniquement lorsque l'utilisateur en a besoin. L'idée de Google Chrome est de réduire considérablement la consommation des ressources du système. Et pour les extensions de navigateur peu utilisées, cela fonctionne. Mais si une extension doit aider l'utilisateur en permanence, comme AdGuard VPN, un agent de service qui s'endort et se réveille fréquemment est plus susceptible d'augmenter la charge sur le système que de la réduire.

Et ce n'est pas tout. Chrome surveille de près le travailleur de service. Ainsi, si l'extension n'effectue aucun appel à l'API ou n'enregistre aucun événement dans les 30 secondes pour empêcher le service worker de s'endormir, le navigateur arrêtera tout simplement le script.

L'arrêt du service worker menace l'extension de navigateur AdGuard VPN avec de nombreuses erreurs. Par exemple, elle ne recevra pas les dernières informations sur les emplacements disponibles, le trafic libre restant, les services exclus, les autres appareils de l'utilisateur connectés au compte et les primes disponibles ; elle ne pourra pas mettre à jour les clés d'accès et l'icône de la barre d'état avec l'état de l'extension.

Solution

Nous avons mentionné plus haut que les événements et les appels d'API empêchent le travailleur de service d'entrer en mode hibernation. Il peut s'agir

  • de messages provenant d'une fenêtre contextuelle, d'une page d'options ou d'un script de contenu d'une extension
  • d'une demande d'authentification par proxy
  • l'ouverture d'une fenêtre contextuelle d'une extension
  • l'ouverture d'un nouvel onglet de navigateur, l'actualisation d'un onglet existant ou le passage d'une fenêtre de navigateur à une autre
  • le choix d'une action dans le menu contextuel de la page (ajout du site web aux exclusions, modification du mode d'exclusion)
  • le déclenchement de la minuterie définie via l'API Alarme (plus d'informations à ce sujet ultérieurement).

En résumé, le fonctionnement est le suivant : le service worker enregistre l'auditeur d'événements et le navigateur enregistre l'événement. Lorsque l'événement se produit, le navigateur vérifie s'il a une extension qui doit être réveillée en réponse à cet événement. Si c'est le cas, il réveille l'agent de service de l'extension et transmet l'événement à l'auditeur.

Dans l'environnement Manifest V2, l'emplacement de l'écouteur d'événements dans le code importait peu : son enregistrement pouvait se faire de manière asynchrone, car la page d'arrière-plan était toujours en cours d'exécution et n'était jamais redémarrée.

Dans Manifest V3, le service worker est redémarré lorsqu'un événement se produit. Dans ce cas, pour que l'événement atteigne l'auditeur, il doit être enregistré au niveau supérieur du code (de manière synchrone).

Dans l'exemple ci-dessous, vous pouvez voir comment l'écouteur pour les messages popup, la page d'options et les scripts de contenu est maintenant défini :

// add the listener for messages from popup, options page and userscripts
chrome.runtime.onMessage.addListener((event) => console.log(event));

Cet exemple illustre la manière dont le récepteur des événements d'action du menu contextuel est défini :

// add the listener for context menu actions
chrome.contextMenus.onClicked.addListener((event) => console.log(event));

Ceci montre comment définir l'écouteur pour les événements d'action de l'onglet :

// add the listener for tab updated
chrome.tabs.onUpdated.addListener((event) => console.log(event));
// add the listener for tab activated
chrome.tabs.onActivated.addListener((event) => console.log(event));
// add the listener for tab activated from another window
chrome.windows.onFocusChanged.addListener((event) => console.log(event));

La nouvelle Alarm API, apparue dans Manifest V3 en plus de setInterval et setTimeout, permet également d'éviter certaines erreurs dues à l'endormissement du service worker.

/**
* Creates an alarm with the specified name and delay
* @param alarmName
* @param delay in ms
*/
const createAlarm = (alarmName: string, delay: number) => {
   chrome.alarms.create(alarmName, { when: Date.now() + delay });
};

/**
* Creates a periodic alarm with the specified name and interval
* @param alarmName
* @param interval in minutes!
*/
const createPeriodicAlarm = (alarmName: string, interval: number) => {
   chrome.alarms.create(alarmName, { periodInMinutes: interval });
};

En utilisant Alarm API

/**
* setTimeout implementation
* @param callback
* @param timeout in ms
*/
setTimeout = (callback: () => void, timeout: number): number => {
   const timerId = this.generateId();
   alarmApi.createAlarm(`${timerId}`, timeout);
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

/**
* setInterval implementation
* @param callback
* @param interval in ms
*/
setInterval = (callback: () => void, interval: number): number => {
   const timerId = this.generateId();
   alarmApi.createPeriodicAlarm(`${timerId}`, this.convertMsToMin(interval));
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

Implémentation d'un minuteur pour MV3

Cependant, Alarm API comporte une limitation assez gênante : il est impossible de définir une minuterie qui fonctionne en 3-5-10 secondes, mais seulement en une minute ou plus.

Réveil du service worker

Lorsque le service worker passe en mode de veille prolongée, il décharge les données qui étaient stockées dans la RAM. Ainsi, après son réveil, l'extension met un certain temps à initialiser les modules : 1,5 à 2 secondes en moyenne, ce qui est inexcusable. Comme vous pouvez le constater, il n'y a pas eu de problème avec la page d'arrière-plan, car elle n'a été rechargée qu'au redémarrage du navigateur.

Solution

Nous avons ajouté un module pour stocker chaque changement de statut à partir de la RAM. Le dernier statut sauvegardé est utilisé par AdGuard VPN lors du réveil de l'agent de service pour une récupération rapide. Cela permet de réduire le délai de redémarrage de l'agent de service à 150-200 ms.

interface StateStorageInterface {
   // Gets the value for the specified key from the session storage
   getItem<T>(key: StorageKey): T;


   // Sets the value for the specified key in the session storage
   setItem<T>(key: StorageKey, value: T): void;


   // Initializes the storage by loading the data from the session storage,
   // or creating a new storage with the default data if none exists.
   init(): Promise<void>;
}

Interface du référentiel des statuts d'extension

Pour mettre en œuvre cette solution, il a fallu déplacer les champs modifiables des classes vers le magasin des statuts. À leur place, nous avons ajouté des getters et des setters aux classes.

Par exemple, si l'API de repli ressemblait à ceci :

export class FallbackApi {
   fallbackInfo: FallbackInfo;

   private setFallbackInfo(fallbackInfo: FallbackInfo) {
       this.fallbackInfo = fallbackInfo;
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
       }

       return this.fallbackInfo;
   }
}

Maintenant, son apparence a changé :

export class FallbackApi {
   private get fallbackInfo(): FallbackInfo {
       return sessionState.getItem(StorageKey.FallbackInfo);
   }

   private set fallbackInfo(value: FallbackInfo) {
       sessionState.setItem(StorageKey.FallbackInfo, value);
   }

   private static needsUpdate(fallbackInfo: FallbackInfo): boolean {
       return fallbackInfo.expiresInMs < Date.now();
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (this.fallbackInfo && FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
           if (this.fallbackInfo) {
               return this.fallbackInfo;
           }
       }

       return this.fallbackInfo;
   }
}

Voici encore des exemples:

class Endpoints implements EndpointsInterface {
   vpnInfo?: VpnExtensionInfoInterface;
}

Ajout d'un statut à la classe Endpoints dans Manifest V2

class Endpoints implements EndpointsInterface {
   #state: EndpointsState | undefined;

   private get vpnInfo(): VpnExtensionInfoInterface | null {
       return this.state.vpnInfo;
   }

   private set vpnInfo(vpnInfo: VpnExtensionInfoInterface | null) {
       this.state.vpnInfo = vpnInfo;
       sessionState.setItem(StorageKey.Endpoints, this.state);
   }

   public get state(): EndpointsState {
       if (!this.#state) {
           throw new Error('Endpoints API is not initialized');
       }

       return this.#state;
   }

   public set state(value: EndpointsState) {
       this.#state = value;
   }
}

Ajout d'un statut à la classe Endpoints dans Manifest V3

export class Credentials implements CredentialsInterface {
   vpnToken: VpnTokenData | null;

   vpnCredentials: CredentialsDataInterface | null;
}

Ajout d'un statut à la classe Credentials dans Manifest V2

export class Credentials implements CredentialsInterface {
   state: CredentialsState;

   saveCredentialsState = () => {
       sessionState.setItem(StorageKey.CredentialsState, this.state);
   };

   get vpnToken() {
       return this.state.vpnToken;
   }

   set vpnToken(vpnToken: VpnTokenData | null) {
       this.state.vpnToken = vpnToken;
       this.saveCredentialsState();
   }

   get vpnCredentials() {
       return this.state.vpnCredentials;
   }

   set vpnCredentials(vpnCredentials: CredentialsDataInterface | null) {
       this.state.vpnCredentials = vpnCredentials;
       this.saveCredentialsState();
   }
}

Ajout d'un statut à la classe Credentials dans Manifest V3

Implémentation de NetworkConnectionObserver

Le NetworkConnectionObserver est une classe qui surveille le statut du réseau (en ligne/hors ligne). Elle est utilisée lorsqu'une application ne reçoit pas de réponse du serveur et permet d'identifier la cause du problème : pas de connexion Internet de la part de l'utilisateur ou un problème du côté du serveur. De plus, NetworkConnectionObserver surveille la transition de l'utilisateur de offline à online. Dans ce cas, les droits d'accès sont vérifiés et la connexion est rétablie si elle a été établie avant que l'utilisateur ne soit hors ligne.

Dans MV2, le NetworkConnectionObserver est implémenté de manière très simple. Il écoute l'événement online et fait appel à la callback qui lui a été transmise.

class NetworkConnectionObserver {
   constructor(callback: () => Promise<void>) {
       window.addEventListener('online', callback);
   }
}

Dans MV3, le service worker n'a pas accès à window, ce qui complique la mise en œuvre. Une solution consisterait à utiliser un document hors écran. Cela créerait une page invisible ayant accès à window et document et la possibilité d'y effectuer des actions qui ne sont pas disponibles pour le travailleur de service.

Cependant, Google promet qu'à l'avenir, le document hors écran s'endormira après une période d'inactivité, comme le travailleur de service. De cette façon, le NetworkConnectionObserver ne sera pas capable d'écouter l'événement online/offline tout le temps, ce qui conduira à des problèmes divers.

Maintenant, nous devons utiliser une solution moins subtile : toutes les demi-secondes, nous vérifions l'état de navigator.online et appelons callback s'il est passé de offline à online.

/**
* Starts checking if the network connection is online at a specified time interval.
*/
private startCheckIsOnline() {
   setInterval(() => {
       this.setIsOnline(navigator.onLine);
   }, this.CHECK_ONLINE_INTERVAL_MS);
}

/**
* Calls handler if network connection becomes online and sets isOnline value.
*/
private setIsOnline(isOnline: boolean) {
   if (isOnline && !this.isOnline) {
       this.onlineHandler(); // callback
   }
   this.isOnline = isOnline;
}

Problèmes liés à la prise en charge de Websocket

Désormais, dans Manifest V3, les messages reçus par l'extension via une connexion websocket ne réveilleront pas le service worker inactif. Par conséquent, l'extension peut manquer le message concernant la nécessité de mettre à jour les clés d'accès au serveur backend et peut ne pas être en mesure de gérer les messages d'erreur concernant l'atteinte de la limite de trafic, la limite des appareils connectés et les domaines non routables.

Solution

Le premier réflexe était d'aller à l'encontre de la vision de Chrome et de faire un service de travailleur qui ne s'arrête pas. C'est d'ailleurs ce que nous avons fait. Mais ensuite, nous avons décidé d'essayer de respecter les règles de Manifest V3 et de nous connecter au websocket uniquement lorsque le service worker est éveillé.

Malgré la solution trouvée, nous espérons vraiment que Google résoudra le problème que nous avons signalé et ajoutera la possibilité de prolonger la durée de vie du service worker avec des messages reçus via la connexion websocket.

Conclusion

Manifest V3 est loin d'être l'environnement le plus intelligent et le plus stable. Néanmoins, vous pouvez déjà installer l'extension AdGuard VPN MV3 et l'essayer. Comme toujours, nous comptons beaucoup sur vos commentaires : si vous trouvez un bug, merci de le signaler sur GitHub.

Vous avez aimé cet article ?

AdGuard VPN
pour Windows

Utilisez un navigateur ou une application de votre choix et ne vous souciez plus jamais de votre anonymat. Le monde entier est à la portée de votre main avec AdGuard VPN.
En savoir plus
Télécharger
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Mac

En deux clics, séléctionnez une ville de votre choix — nous offrons 65+ localisations — et vos données seront cachées des regards indiscrets des sociétés globales et des états.
En savoir plus
Télécharger
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour iOS

Renforcez votre protection en ligne en l'emportant avec vous partout où vous allez. Profitez de vos séries et émissions préférées avec AdGuard VPN !
En savoir plus
App Store
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Android

Restez anonyme où que vous alliez avec AdGuard VPN ! Des dizaines d'emplacements, une connexion rapide et fiable - le tout dans votre poche.
En savoir plus
Google Play
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation
Télécharger
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Chrome

Cachez votre véritable emplacement et émergez d'un autre endroit dans le monde — accédez à n'importe quel contenu sans restrictions et préservez votre anonymat sur le web.
En savoir plus
Installer
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Edge

Rendez-vous dans un autre pays en un seul clic, gagnez l'accès au contenu restreint, faites de votre surfing du web une expérience extra sécurisée et anonyme.
En savoir plus
Installer
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Firefox

Protégez votre confidentialité, dissimulez votre véritable localisation, décidez où vous avez besoin du VPN et où non !
En savoir plus
Installer
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation

AdGuard VPN
pour Opera

Soyez un ninja dans votre navigateur Opera : déplacez-vous rapidement dans n'importe quelle partie du monde et restez inaperçu.
En savoir plus
Installer
En téléchargeant le programme, vous acceptez les termes des Conditions générales d'utilisation
AdGuard VPN
téléchargement en cours
Cliquez le bouton indiqué par la flèche pour déclencher l'installation.
Analyser pour installer AdGuard VPN sur votre appareil mobile