एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

सामान्य "क्लाइंट-सर्वर" आर्किटेक्चर के विपरीत, विकेंद्रीकृत अनुप्रयोगों की विशेषता यह है:

  • उपयोगकर्ता लॉगिन और पासवर्ड के साथ डेटाबेस संग्रहीत करने की कोई आवश्यकता नहीं है। एक्सेस जानकारी विशेष रूप से उपयोगकर्ताओं द्वारा स्वयं संग्रहीत की जाती है, और उनकी प्रामाणिकता की पुष्टि प्रोटोकॉल स्तर पर होती है।
  • सर्वर का उपयोग करने की कोई आवश्यकता नहीं है. एप्लिकेशन लॉजिक को ब्लॉकचेन नेटवर्क पर निष्पादित किया जा सकता है, जहां आवश्यक मात्रा में डेटा संग्रहीत करना संभव है।

उपयोगकर्ता कुंजियों के लिए 2 अपेक्षाकृत सुरक्षित भंडारण हैं - हार्डवेयर वॉलेट और ब्राउज़र एक्सटेंशन। हार्डवेयर वॉलेट अधिकतर बेहद सुरक्षित होते हैं, लेकिन उपयोग में कठिन होते हैं और मुफ़्त से बहुत दूर होते हैं, लेकिन ब्राउज़र एक्सटेंशन सुरक्षा और उपयोग में आसानी का सही संयोजन हैं, और अंतिम उपयोगकर्ताओं के लिए पूरी तरह से मुफ़्त भी हो सकते हैं।

इस सब को ध्यान में रखते हुए, हम सबसे सुरक्षित एक्सटेंशन बनाना चाहते थे जो लेनदेन और हस्ताक्षर के साथ काम करने के लिए एक सरल एपीआई प्रदान करके विकेंद्रीकृत अनुप्रयोगों के विकास को सरल बनाता है।
इस अनुभव के बारे में हम आपको नीचे बताएंगे.

लेख में कोड उदाहरणों और स्क्रीनशॉट के साथ ब्राउज़र एक्सटेंशन लिखने के बारे में चरण-दर-चरण निर्देश होंगे। आप इसमें सभी कोड पा सकते हैं खजाने. प्रत्येक प्रतिबद्धता तार्किक रूप से इस आलेख के एक भाग से मेल खाती है।

ब्राउज़र एक्सटेंशन का संक्षिप्त इतिहास

ब्राउज़र एक्सटेंशन काफी समय से मौजूद हैं। वे 1999 में इंटरनेट एक्सप्लोरर में और 2004 में फ़ायरफ़ॉक्स में दिखाई दिए। हालाँकि, बहुत लंबे समय तक एक्सटेंशन के लिए कोई एक मानक नहीं था।

हम कह सकते हैं कि यह Google Chrome के चौथे संस्करण में एक्सटेंशन के साथ दिखाई दिया। बेशक, तब कोई विशिष्टता नहीं थी, लेकिन यह क्रोम एपीआई था जो इसका आधार बन गया: अधिकांश ब्राउज़र बाजार पर विजय प्राप्त करने और एक अंतर्निहित एप्लिकेशन स्टोर होने के बाद, क्रोम ने वास्तव में ब्राउज़र एक्सटेंशन के लिए मानक निर्धारित किया।

मोज़िला का अपना मानक था, लेकिन क्रोम एक्सटेंशन की लोकप्रियता को देखते हुए, कंपनी ने एक संगत एपीआई बनाने का फैसला किया। 2015 में, मोज़िला की पहल पर, क्रॉस-ब्राउज़र एक्सटेंशन विनिर्देशों पर काम करने के लिए वर्ल्ड वाइड वेब कंसोर्टियम (W3C) के भीतर एक विशेष समूह बनाया गया था।

क्रोम के लिए मौजूदा एपीआई एक्सटेंशन को आधार के रूप में लिया गया। कार्य Microsoft के समर्थन से किया गया (Google ने मानक के विकास में भाग लेने से इनकार कर दिया), और परिणामस्वरूप एक मसौदा सामने आया विनिर्देश.

औपचारिक रूप से, विनिर्देश एज, फ़ायरफ़ॉक्स और ओपेरा द्वारा समर्थित है (ध्यान दें कि क्रोम इस सूची में नहीं है)। लेकिन वास्तव में, मानक काफी हद तक क्रोम के साथ संगत है, क्योंकि यह वास्तव में इसके एक्सटेंशन के आधार पर लिखा गया है। आप WebExtensions API के बारे में अधिक पढ़ सकते हैं यहां.

विस्तार संरचना

एक्सटेंशन के लिए आवश्यक एकमात्र फ़ाइल मेनिफेस्ट (manifest.json) है। यह विस्तार का "प्रवेश बिंदु" भी है।

घोषणापत्र

विनिर्देश के अनुसार, मेनिफेस्ट फ़ाइल एक वैध JSON फ़ाइल है। किस ब्राउज़र में कौन सी कुंजियाँ समर्थित हैं, इसकी जानकारी के साथ मेनिफेस्ट कुंजियों का पूरा विवरण देखा जा सकता है यहां.

जो कुंजियाँ विनिर्देश में नहीं हैं उन्हें "अनदेखा" किया जा सकता है (क्रोम और फ़ायरफ़ॉक्स दोनों त्रुटियों की रिपोर्ट करते हैं, लेकिन एक्सटेंशन काम करना जारी रखते हैं)।

और मैं कुछ बिंदुओं पर ध्यान आकर्षित करना चाहूंगा।

  1. पृष्ठभूमि - एक ऑब्जेक्ट जिसमें निम्नलिखित फ़ील्ड शामिल हैं:
    1. लिपियों - स्क्रिप्ट की एक श्रृंखला जिसे पृष्ठभूमि संदर्भ में निष्पादित किया जाएगा (हम इसके बारे में थोड़ी देर बाद बात करेंगे);
    2. पृष्ठ - खाली पेज में निष्पादित होने वाली स्क्रिप्ट के बजाय, आप सामग्री के साथ html निर्दिष्ट कर सकते हैं। इस मामले में, स्क्रिप्ट फ़ील्ड को अनदेखा कर दिया जाएगा, और स्क्रिप्ट को सामग्री पृष्ठ में डालने की आवश्यकता होगी;
    3. दृढ़ - एक बाइनरी फ़्लैग, यदि निर्दिष्ट नहीं है, तो ब्राउज़र पृष्ठभूमि प्रक्रिया को "मार" देगा जब उसे लगेगा कि वह कुछ नहीं कर रहा है, और यदि आवश्यक हो तो उसे पुनः आरंभ करेगा। अन्यथा, पेज केवल ब्राउज़र बंद होने पर ही अनलोड किया जाएगा। फ़ायरफ़ॉक्स में समर्थित नहीं है.
  2. content_scripts - वस्तुओं की एक श्रृंखला जो आपको अलग-अलग स्क्रिप्ट को अलग-अलग वेब पेजों पर लोड करने की अनुमति देती है। प्रत्येक ऑब्जेक्ट में निम्नलिखित महत्वपूर्ण फ़ील्ड होते हैं:
    1. मैच - पैटर्न यूआरएल, जो यह निर्धारित करता है कि किसी विशेष सामग्री स्क्रिप्ट को शामिल किया जाएगा या नहीं।
    2. js - स्क्रिप्ट की एक सूची जो इस मैच में लोड की जाएगी;
    3. बहिष्कृत_मिलान - क्षेत्र से बाहर रखा गया है match वे URL जो इस फ़ील्ड से मेल खाते हैं.
  3. पृष्ठ_कार्रवाई - वास्तव में एक ऑब्जेक्ट है जो ब्राउज़र में एड्रेस बार के बगल में प्रदर्शित होने वाले आइकन और उसके साथ इंटरेक्शन के लिए जिम्मेदार है। यह आपको एक पॉपअप विंडो प्रदर्शित करने की भी अनुमति देता है, जिसे आपके स्वयं के HTML, CSS और JS का उपयोग करके परिभाषित किया गया है।
    1. default_popup - पॉपअप इंटरफ़ेस के साथ HTML फ़ाइल का पथ, इसमें CSS और JS शामिल हो सकते हैं।
  4. अनुमतियाँ - विस्तार अधिकारों के प्रबंधन के लिए एक सरणी। अधिकार तीन प्रकार के होते हैं जिनका विस्तार से वर्णन किया गया है यहां
  5. वेब_सुलभ_संसाधन - एक्सटेंशन संसाधन जो एक वेब पेज अनुरोध कर सकता है, उदाहरण के लिए, छवियां, जेएस, सीएसएस, एचटीएमएल फाइलें।
  6. बाह्य रूप से_कनेक्ट करने योग्य - यहां आप वेब पेजों के अन्य एक्सटेंशन और डोमेन की आईडी स्पष्ट रूप से निर्दिष्ट कर सकते हैं जिनसे आप कनेक्ट हो सकते हैं। एक डोमेन द्वितीय स्तर या उच्चतर हो सकता है। फ़ायरफ़ॉक्स में काम नहीं करता.

निष्पादन प्रसंग

एक्सटेंशन में तीन कोड निष्पादन संदर्भ हैं, अर्थात, एप्लिकेशन में ब्राउज़र एपीआई तक पहुंच के विभिन्न स्तरों के साथ तीन भाग होते हैं।

विस्तार प्रसंग

अधिकांश एपीआई यहां उपलब्ध है। इस संदर्भ में वे "जीवित" हैं:

  1. पृष्ठभूमि पृष्ठ - एक्सटेंशन का "बैकएंड" भाग। फ़ाइल को "पृष्ठभूमि" कुंजी का उपयोग करके मैनिफ़ेस्ट में निर्दिष्ट किया गया है।
  2. पॉपअप पेज - एक पॉपअप पेज जो एक्सटेंशन आइकन पर क्लिक करने पर दिखाई देता है। घोषणापत्र में browser_action -> default_popup.
  3. कस्टम पेज - विस्तार पृष्ठ, दृश्य के एक अलग टैब में "जीवित"। chrome-extension://<id_расширения>/customPage.html.

यह संदर्भ ब्राउज़र विंडो और टैब से स्वतंत्र रूप से मौजूद है। पृष्ठभूमि पृष्ठ एक ही प्रतिलिपि में मौजूद है और हमेशा काम करता है (अपवाद ईवेंट पृष्ठ है, जब पृष्ठभूमि स्क्रिप्ट किसी ईवेंट द्वारा लॉन्च की जाती है और इसके निष्पादन के बाद "मर जाती है")। पॉपअप पेज पॉपअप विंडो खुली होने पर मौजूद रहता है, और कस्टम पेज — जबकि इसके साथ वाला टैब खुला है। इस संदर्भ से अन्य टैब और उनकी सामग्री तक कोई पहुंच नहीं है।

सामग्री स्क्रिप्ट संदर्भ

सामग्री स्क्रिप्ट फ़ाइल प्रत्येक ब्राउज़र टैब के साथ लॉन्च की जाती है। इसकी एक्सटेंशन के एपीआई के हिस्से और वेब पेज के DOM ट्री तक पहुंच है। यह सामग्री स्क्रिप्ट हैं जो पेज के साथ इंटरेक्शन के लिए ज़िम्मेदार हैं। DOM ट्री में हेरफेर करने वाले एक्सटेंशन सामग्री स्क्रिप्ट में ऐसा करते हैं - उदाहरण के लिए, विज्ञापन अवरोधक या अनुवादक। साथ ही, सामग्री स्क्रिप्ट मानक के माध्यम से पृष्ठ के साथ संचार कर सकती है postMessage.

वेब पेज संदर्भ

यह वास्तविक वेब पेज ही है. इसका एक्सटेंशन से कोई लेना-देना नहीं है और इसकी वहां तक ​​पहुंच नहीं है, सिवाय उन मामलों के जहां इस पेज का डोमेन मेनिफेस्ट में स्पष्ट रूप से इंगित नहीं किया गया है (नीचे इस पर अधिक जानकारी दी गई है)।

संदेश सेवा

एप्लिकेशन के विभिन्न हिस्सों को एक दूसरे के साथ संदेशों का आदान-प्रदान करना होगा। इसके लिए एक एपीआई है runtime.sendMessage एक संदेश भेजने के लिए background и tabs.sendMessage किसी पेज पर संदेश भेजने के लिए (यदि उपलब्ध हो तो सामग्री स्क्रिप्ट, पॉपअप या वेब पेज)। externally_connectable). Chrome API एक्सेस करते समय नीचे एक उदाहरण दिया गया है।

// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};

// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);

// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))

// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)

// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
    {currentWindow: true, active : true},
    function(tabArray){
      tabArray.forEach(tab => console.log(tab.id))
    }
)

पूर्ण संचार के लिए, आप इसके माध्यम से कनेक्शन बना सकते हैं runtime.connect. जवाब में हमें प्राप्त होगा runtime.Port, जिसके खुले रहने पर आप कितने भी संदेश भेज सकते हैं। उदाहरण के लिए, ग्राहक पक्ष पर, contentscript, यह इस तरह दिख रहा है:

// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
    if (msg.question === "Who's there?")
        port.postMessage({answer: "Madame"});
    else if (msg.question === "Madame who?")
        port.postMessage({answer: "Madame... Bovary"});

सर्वर या पृष्ठभूमि:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
chrome.runtime.onConnect.addListener(function(port) {
    console.assert(port.name === "knockknock");
    port.onMessage.addListener(function(msg) {
        if (msg.joke === "Knock knock")
            port.postMessage({question: "Who's there?"});
        else if (msg.answer === "Madame")
            port.postMessage({question: "Madame who?"});
        else if (msg.answer === "Madame... Bovary")
            port.postMessage({question: "I don't get it."});
    });
});

// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
    ...
});

एक आयोजन भी है onDisconnect और विधि disconnect.

अनुप्रयोग आरेख

आइए एक ब्राउज़र एक्सटेंशन बनाएं जो निजी कुंजी संग्रहीत करता है, सार्वजनिक जानकारी (पता, सार्वजनिक कुंजी पृष्ठ के साथ संचार करता है) तक पहुंच प्रदान करता है और तीसरे पक्ष के अनुप्रयोगों को लेनदेन के लिए हस्ताक्षर का अनुरोध करने की अनुमति देता है।

एप्लीकेशन का विकास

हमारे एप्लिकेशन को उपयोगकर्ता के साथ इंटरैक्ट करना होगा और पेज को कॉल करने के तरीकों (उदाहरण के लिए, लेनदेन पर हस्ताक्षर करने के लिए) के लिए एपीआई प्रदान करना होगा। एक से ही काम चलाओ contentscript काम नहीं करेगा, क्योंकि इसकी पहुंच केवल DOM तक है, लेकिन पेज के JS तक नहीं। के माध्यम से कनेक्ट करें runtime.connect हम ऐसा नहीं कर सकते, क्योंकि सभी डोमेन पर एपीआई की आवश्यकता होती है, और केवल विशिष्ट डोमेन को ही मेनिफेस्ट में निर्दिष्ट किया जा सकता है। परिणामस्वरूप, आरेख इस तरह दिखेगा:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

एक और स्क्रिप्ट होगी - inpage, जिसे हम पेज में इंजेक्ट करेंगे। यह अपने संदर्भ में चलेगा और एक्सटेंशन के साथ काम करने के लिए एक एपीआई प्रदान करेगा।

शुरू

सभी ब्राउज़र एक्सटेंशन कोड यहां उपलब्ध हैं GitHub. विवरण के दौरान कमिट के लिंक होंगे।

आइए घोषणापत्र से शुरू करें:

{
  // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
  "name": "Signer",
  "description": "Extension demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Скрипты, которые будут исполнятся в background, их может быть несколько
  "background": {
    "scripts": ["background.js"]
  },

  // Какой html использовать для popup
  "browser_action": {
    "default_title": "My Extension",
    "default_popup": "popup.html"
  },

  // Контент скрипты.
  // У нас один объект: для всех url начинающихся с http или https мы запускаем
  // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Разрешен доступ к localStorage и idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
  "web_accessible_resources": ["inpage.js"]
}

खाली बैकग्राउंड.जेएस, पॉपअप.जेएस, इनपेज.जेएस और कंटेंटस्क्रिप्ट.जेएस बनाएं। हम Popup.html जोड़ते हैं - और हमारा एप्लिकेशन पहले से ही Google Chrome में लोड किया जा सकता है और सुनिश्चित कर सकता है कि यह काम करता है।

इसे वेरिफाई करने के लिए आप कोड ले सकते हैं अत:. हमने जो किया उसके अलावा, लिंक ने वेबपैक का उपयोग करके प्रोजेक्ट की असेंबली को कॉन्फ़िगर किया। ब्राउज़र में एक एप्लिकेशन जोड़ने के लिए, क्रोम://एक्सटेंशन में आपको लोड अनपैक्ड और संबंधित एक्सटेंशन वाले फ़ोल्डर का चयन करना होगा - हमारे मामले में जिला।

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

अब हमारा एक्सटेंशन स्थापित है और काम कर रहा है। आप विभिन्न संदर्भों के लिए डेवलपर टूल निम्नानुसार चला सकते हैं:

पॉपअप ->

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

सामग्री स्क्रिप्ट कंसोल तक पहुंच उस पृष्ठ के कंसोल के माध्यम से ही की जाती है जिस पर इसे लॉन्च किया गया है।एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

संदेश सेवा

इसलिए, हमें दो संचार चैनल स्थापित करने की आवश्यकता है: इनपेज <-> बैकग्राउंड और पॉपअप <-> बैकग्राउंड। बेशक, आप केवल पोर्ट पर संदेश भेज सकते हैं और अपने स्वयं के प्रोटोकॉल का आविष्कार कर सकते हैं, लेकिन मैं उस दृष्टिकोण को पसंद करता हूं जो मैंने मेटामास्क ओपन सोर्स प्रोजेक्ट में देखा था।

यह एथेरियम नेटवर्क के साथ काम करने के लिए एक ब्राउज़र एक्सटेंशन है। इसमें एप्लिकेशन के विभिन्न भाग dnode लाइब्रेरी का उपयोग करके RPC के माध्यम से संचार करते हैं। यदि आप इसे ट्रांसपोर्ट के रूप में नोडज स्ट्रीम प्रदान करते हैं (अर्थात् एक ऑब्जेक्ट जो समान इंटरफ़ेस लागू करता है) तो यह आपको एक एक्सचेंज को बहुत तेज़ी से और आसानी से व्यवस्थित करने की अनुमति देता है:

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

अब हम एक एप्लीकेशन क्लास बनाएंगे। यह पॉपअप और वेब पेज के लिए एपीआई ऑब्जेक्ट बनाएगा, और उनके लिए एक डीनोड बनाएगा:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Возвращает объект API для ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Возвращает объет API для страницы
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Подключает popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(remote)
        })
    }

    // Подключает страницу
    connectPage(connectionStream, origin){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(origin);
            console.log(remote)
        })
    }
}

यहां और नीचे, वैश्विक क्रोम ऑब्जेक्ट के बजाय, हम एक्सटेंशनएपीआई का उपयोग करते हैं, जो Google के ब्राउज़र में क्रोम और अन्य में ब्राउज़र तक पहुंचता है। यह क्रॉस-ब्राउज़र संगतता के लिए किया जाता है, लेकिन इस आलेख के प्रयोजनों के लिए कोई केवल 'chrome.runtime.connect' का उपयोग कर सकता है।

आइए पृष्ठभूमि स्क्रिप्ट में एक एप्लिकेशन इंस्टेंस बनाएं:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const app = new SignerApp();

// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

चूँकि dnode स्ट्रीम के साथ काम करता है, और हमें एक पोर्ट प्राप्त होता है, एक एडाप्टर क्लास की आवश्यकता होती है। इसे पठनीय-स्ट्रीम लाइब्रेरी का उपयोग करके बनाया गया है, जो ब्राउज़र में नोडज स्ट्रीम लागू करता है:

import {Duplex} from 'readable-stream';

export class PortStream extends Duplex{
    constructor(port){
        super({objectMode: true});
        this._port = port;
        port.onMessage.addListener(this._onMessage.bind(this));
        port.onDisconnect.addListener(this._onDisconnect.bind(this))
    }

    _onMessage(msg) {
        if (Buffer.isBuffer(msg)) {
            delete msg._isBuffer;
            const data = new Buffer(msg);
            this.push(data)
        } else {
            this.push(msg)
        }
    }

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

    _write(msg, encoding, cb) {
        try {
            if (Buffer.isBuffer(msg)) {
                const data = msg.toJSON();
                data._isBuffer = true;
                this._port.postMessage(data)
            } else {
                this._port.postMessage(msg)
            }
        } catch (err) {
            return cb(new Error('PortStream - disconnected'))
        }
        cb()
    }
}

अब आइए यूआई में एक कनेक्शन बनाएं:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Делаем объект API доступным из консоли
    if (DEV_MODE){
        global.background = background;
    }
}

फिर हम सामग्री स्क्रिप्ट में कनेक्शन बनाते हैं:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import PostMessageStream from 'post-message-stream';

setupConnection();
injectScript();

function setupConnection(){
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // inject in-page script
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection failed.', e);
    }
}

चूँकि हमें एपीआई की आवश्यकता सामग्री स्क्रिप्ट में नहीं, बल्कि सीधे पृष्ठ पर होती है, हम दो काम करते हैं:

  1. हम दो धाराएँ बनाते हैं। एक - पेज की ओर, पोस्टमैसेज के ऊपर। इसके लिए हम इसका इस्तेमाल करते हैं यह पैकेज मेटामास्क के रचनाकारों से। दूसरी धारा प्राप्त पोर्ट पर बैकग्राउंड करना है runtime.connect. आइए उन्हें खरीदें. अब पेज के बैकग्राउंड में एक स्ट्रीम होगी।
  2. स्क्रिप्ट को DOM में इंजेक्ट करें। स्क्रिप्ट डाउनलोड करें (मैनिफ़ेस्ट में इस तक पहुंच की अनुमति दी गई थी) और एक टैग बनाएं script इसके अंदर की सामग्री के साथ:

import PostMessageStream from 'post-message-stream';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";

setupConnection();
injectScript();

function setupConnection(){
    // Стрим к бекграунду
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    // Стрим к странице
    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // inject in-page script
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection failed.', e);
    }
}

अब हम इनपेज में एक एपीआई ऑब्जेक्ट बनाते हैं और इसे ग्लोबल पर सेट करते हैं:

import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Стрим к контентскрипту
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    // Получаем объект API
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Доступ через window
    global.SignerApp = pageApi;
}

हम तैयार हैं पेज और यूआई के लिए अलग एपीआई के साथ रिमोट प्रोसीजर कॉल (आरपीसी)।. किसी नए पेज को बैकग्राउंड से कनेक्ट करते समय हम यह देख सकते हैं:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

खाली एपीआई और मूल. पेज साइड पर, हम हैलो फ़ंक्शन को इस प्रकार कॉल कर सकते हैं:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

आधुनिक जेएस में कॉलबैक फ़ंक्शंस के साथ काम करना बुरा व्यवहार है, तो आइए एक डीनोड बनाने के लिए एक छोटा सहायक लिखें जो आपको एपीआई ऑब्जेक्ट को यूटिल्स में पास करने की अनुमति देता है।

एपीआई ऑब्जेक्ट अब इस तरह दिखेंगे:

export class SignerApp {

    popupApi() {
        return {
            hello: async () => "world"
        }
    }

...

}

इस प्रकार रिमोट से वस्तु प्राप्त करना:

import {cbToPromise, transformMethods} from "../../src/utils/setupDnode";

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // С помощью утилит меняем все callback на promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

और कॉलिंग फ़ंक्शंस एक वादा लौटाता है:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

अतुल्यकालिक कार्यों वाला संस्करण उपलब्ध है यहां.

कुल मिलाकर, आरपीसी और स्ट्रीम दृष्टिकोण काफी लचीला लगता है: हम स्टीम मल्टीप्लेक्सिंग का उपयोग कर सकते हैं और विभिन्न कार्यों के लिए कई अलग-अलग एपीआई बना सकते हैं। सिद्धांत रूप में, डीनोड का उपयोग कहीं भी किया जा सकता है, मुख्य बात यह है कि परिवहन को नोडज स्ट्रीम के रूप में लपेटना है।

एक विकल्प JSON प्रारूप है, जो JSON RPC 2 प्रोटोकॉल को लागू करता है। हालाँकि, यह विशिष्ट ट्रांसपोर्ट (TCP और HTTP(S)) के साथ काम करता है, जो हमारे मामले में लागू नहीं है।

आंतरिक स्थिति और लोकलस्टोरेज

हमें एप्लिकेशन की आंतरिक स्थिति को संग्रहीत करने की आवश्यकता होगी - कम से कम हस्ताक्षर कुंजी। हम एप्लिकेशन में एक स्थिति और पॉपअप एपीआई में इसे बदलने के तरीकों को आसानी से जोड़ सकते हैं:

import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(){
        this.store = {
            keys: [],
        };
    }

    addKey(key){
        this.store.keys.push(key)
    }

    removeKey(index){
        this.store.keys.splice(index,1)
    }

    popupApi(){
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index)
        }
    }

    ...

} 

पृष्ठभूमि में, हम सब कुछ एक फ़ंक्शन में लपेटेंगे और एप्लिकेशन ऑब्जेक्ट को विंडो में लिखेंगे ताकि हम कंसोल से इसके साथ काम कर सकें:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const app = new SignerApp();

    if (DEV_MODE) {
        global.app = app;
    }

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

आइए यूआई कंसोल से कुछ कुंजियाँ जोड़ें और देखें कि स्थिति के साथ क्या होता है:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

स्थिति को लगातार बनाए रखने की आवश्यकता है ताकि पुनरारंभ करते समय चाबियाँ खो न जाएं।

हम इसे हर बदलाव के साथ ओवरराइट करते हुए लोकलस्टोरेज में स्टोर करेंगे। इसके बाद, यूआई के लिए इस तक पहुंच भी आवश्यक होगी, और मैं परिवर्तनों की सदस्यता भी लेना चाहूंगा। इसके आधार पर, एक अवलोकन योग्य भंडारण बनाना और उसके परिवर्तनों की सदस्यता लेना सुविधाजनक होगा।

हम mobx लाइब्रेरी का उपयोग करेंगे (https://github.com/mobxjs/mobx). चुनाव उसी पर पड़ा क्योंकि मुझे इसके साथ काम नहीं करना था, लेकिन मैं वास्तव में इसका अध्ययन करना चाहता था।

आइए प्रारंभिक स्थिति का आरंभीकरण जोड़ें और स्टोर को अवलोकन योग्य बनाएं:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(initState = {}) {
        // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Методы, которые меняют observable принято оборачивать декоратором
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

    @action
    removeKey(index) {
        this.store.keys.splice(index, 1)
    }

    ...

}

"अंडर द हुड" mobx ने सभी स्टोर फ़ील्ड को प्रॉक्सी से बदल दिया है और उन पर आने वाली सभी कॉल को इंटरसेप्ट कर लेता है। इन संदेशों की सदस्यता लेना संभव होगा।

नीचे मैं अक्सर "जब बदल रहा हूँ" शब्द का उपयोग करूँगा, हालाँकि यह पूरी तरह से सही नहीं है। Mobx फ़ील्ड तक पहुंच को ट्रैक करता है। लाइब्रेरी द्वारा बनाए गए प्रॉक्सी ऑब्जेक्ट के गेटर्स और सेटर्स का उपयोग किया जाता है।

एक्शन डेकोरेटर दो उद्देश्यों की पूर्ति करते हैं:

  1. EnforceActions फ़्लैग के साथ सख्त मोड में, mobx सीधे राज्य को बदलने पर रोक लगाता है। कड़ी परिस्थितियों में काम करना अच्छा अभ्यास माना जाता है।
  2. भले ही कोई फ़ंक्शन कई बार स्थिति बदलता है - उदाहरण के लिए, हम कोड की कई पंक्तियों में कई फ़ील्ड बदलते हैं - पर्यवेक्षकों को केवल इसके पूरा होने पर ही सूचित किया जाता है। यह फ़्रंटएंड के लिए विशेष रूप से महत्वपूर्ण है, जहां अनावश्यक स्थिति अपडेट से तत्वों का अनावश्यक प्रतिपादन होता है। हमारे मामले में, न तो पहला और न ही दूसरा विशेष रूप से प्रासंगिक है, लेकिन हम सर्वोत्तम प्रथाओं का पालन करेंगे। डेकोरेटर्स को उन सभी कार्यों से जोड़ने की प्रथा है जो प्रेक्षित क्षेत्रों की स्थिति को बदलते हैं।

पृष्ठभूमि में हम लोकलस्टोरेज में इनिशियलाइज़ेशन और स्टेट को सेव करेंगे:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу 'store'
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Setup state persistence

    // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера
    const localStorageReaction = reaction(
        () => toJS(app.store), // Функция-селектор данных
        saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор
    );

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

यहां प्रतिक्रिया फ़ंक्शन दिलचस्प है। इसके दो तर्क हैं:

  1. डेटा चयनकर्ता.
  2. एक हैंडलर जिसे हर बार परिवर्तन होने पर इस डेटा के साथ बुलाया जाएगा।

रिडक्स के विपरीत, जहां हम स्पष्ट रूप से राज्य को एक तर्क के रूप में प्राप्त करते हैं, mobx याद रखता है कि हम चयनकर्ता के अंदर किन वेधशालाओं तक पहुंचते हैं, और केवल हैंडलर को कॉल करते हैं जब वे बदलते हैं।

यह समझना महत्वपूर्ण है कि mobx कैसे तय करता है कि हम किन वेधशालाओं की सदस्यता लेते हैं। अगर मैंने इस तरह कोड में एक चयनकर्ता लिखा है() => app.store, तो प्रतिक्रिया कभी नहीं बुलाई जाएगी, क्योंकि भंडारण स्वयं देखने योग्य नहीं है, केवल उसके क्षेत्र हैं।

अगर मैं इसे इस तरह लिखूं () => app.store.keys, तो फिर कुछ नहीं होगा, क्योंकि सरणी तत्वों को जोड़ने/हटाने पर, इसका संदर्भ नहीं बदलेगा।

Mobx पहली बार एक चयनकर्ता के रूप में कार्य करता है और केवल उन अवलोकनों का ट्रैक रखता है जिन तक हमने पहुंच बनाई है। यह प्रॉक्सी गेटर्स के माध्यम से किया जाता है। इसलिए, यहां बिल्ट-इन फ़ंक्शन का उपयोग किया जाता है toJS. यह मूल फ़ील्ड के साथ प्रतिस्थापित सभी प्रॉक्सी के साथ एक नई वस्तु लौटाता है। निष्पादन के दौरान, यह ऑब्जेक्ट के सभी फ़ील्ड को पढ़ता है - इसलिए गेटर्स ट्रिगर हो जाते हैं।

पॉपअप कंसोल में हम फिर से कई कुंजियाँ जोड़ेंगे। इस बार वे लोकलस्टोरेज में भी समाप्त हो गए:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

जब पृष्ठभूमि पृष्ठ पुनः लोड किया जाता है, तो जानकारी यथावत बनी रहती है।

इस बिंदु तक के सभी एप्लिकेशन कोड देखे जा सकते हैं यहां.

निजी चाबियों का सुरक्षित भंडारण

निजी कुंजियों को स्पष्ट टेक्स्ट में संग्रहीत करना असुरक्षित है: इस बात की संभावना हमेशा बनी रहती है कि आपको हैक कर लिया जाएगा, आपके कंप्यूटर तक पहुंच प्राप्त कर ली जाएगी, इत्यादि। इसलिए, लोकलस्टोरेज में हम चाबियों को पासवर्ड-एन्क्रिप्टेड रूप में संग्रहीत करेंगे।

अधिक सुरक्षा के लिए, हम एप्लिकेशन में एक लॉक स्थिति जोड़ देंगे, जिसमें कुंजियों तक बिल्कुल भी पहुंच नहीं होगी। हम टाइमआउट के कारण एक्सटेंशन को स्वचालित रूप से लॉक स्थिति में स्थानांतरित कर देंगे।

Mobx आपको डेटा का केवल एक न्यूनतम सेट संग्रहीत करने की अनुमति देता है, और बाकी की गणना स्वचालित रूप से इसके आधार पर की जाती है। ये तथाकथित परिकलित गुण हैं। उनकी तुलना डेटाबेस में दृश्यों से की जा सकती है:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
            password: null,
            vault: initState.vault,

            // Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Инициализация пустого хранилища новым паролем
    @action
    initVault(password){
        this.store.vault = SignerApp._encryptVault([], password)
    }
    @action
    lock() {
        this.store.password = null
    }
    @action
    unlock(password) {
        this._checkPassword(password);
        this.store.password = password
    }
    @action
    addKey(key) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
    }
    @action
    removeKey(index) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault([
                ...this.store.keys.slice(0, index),
                ...this.store.keys.slice(index + 1)
            ],
            this.store.password
        )
    }

    ... // код подключения и api

    // private
    _checkPassword(password) {
        SignerApp._decryptVault(this.store.vault, password);
    }

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App is locked')
        }
    }

    // Методы для шифровки/дешифровки хранилища
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Vault not initialized')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Wrong password')
        }
    }
}

अब हम केवल एन्क्रिप्टेड कुंजी और पासवर्ड संग्रहीत करते हैं। बाकी सभी चीज़ों की गणना की जाती है। हम राज्य से पासवर्ड हटाकर लॉक स्थिति में स्थानांतरण करते हैं। सार्वजनिक एपीआई में अब भंडारण आरंभ करने की एक विधि है।

एन्क्रिप्शन के लिए लिखा गया क्रिप्टो-जेएस का उपयोग करने वाली उपयोगिताएँ:

import CryptoJS from 'crypto-js'

// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
function strengthenPassword(pass, rounds = 5000) {
    while (rounds-- > 0){
        pass = CryptoJS.SHA256(pass).toString()
    }
    return pass
}

export function encrypt(str, pass){
    const strongPass = strengthenPassword(pass);
    return CryptoJS.AES.encrypt(str, strongPass).toString()
}

export function decrypt(str, pass){
    const strongPass = strengthenPassword(pass)
    const decrypted = CryptoJS.AES.decrypt(str, strongPass);
    return decrypted.toString(CryptoJS.enc.Utf8)
}

ब्राउज़र में एक निष्क्रिय एपीआई है जिसके माध्यम से आप किसी ईवेंट की सदस्यता ले सकते हैं - स्थिति में परिवर्तन। राज्य, तदनुसार, हो सकता है idle, active и locked. निष्क्रिय के लिए आप एक टाइमआउट सेट कर सकते हैं, और लॉक तब सेट किया जाता है जब ओएस स्वयं अवरुद्ध हो जाता है। हम लोकलस्टोरेज में सेव करने के लिए चयनकर्ता को भी बदल देंगे:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';
const IDLE_INTERVAL = 30;

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Таймаут бездействия, когда сработает событие
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Connect to other contexts
    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

इस चरण से पहले का कोड है यहां.

लेन-देन

तो, हम सबसे महत्वपूर्ण बात पर आते हैं: ब्लॉकचेन पर लेनदेन बनाना और हस्ताक्षर करना। हम WAVES ब्लॉकचेन और लाइब्रेरी का उपयोग करेंगे तरंग-लेन-देन.

सबसे पहले, आइए राज्य में उन संदेशों की एक श्रृंखला जोड़ें जिन पर हस्ताक्षर करने की आवश्यकता है, फिर एक नया संदेश जोड़ने, हस्ताक्षर की पुष्टि करने और इनकार करने के तरीके जोड़ें:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

    @action
    newMessage(data, origin) {
        // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
        const message = observable.object({
            id: uuid(), // Идентификатор, используюю uuid
            origin, // Origin будем впоследствии показывать в интерфейсе
            data, //
            status: 'new', // Статусов будет четыре: new, signed, rejected и failed
            timestamp: Date.now()
        });
        console.log(`new message: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, //Будем обсервить статус сообщеня
                (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('User rejected message'));
                            break;
                        case 'failed':
                            reject(new Error(message.err.message));
                            break;
                        default:
                            return
                    }
                    reaction.dispose()
                }
            )
        })
    }
    @action
    approve(id, keyIndex = 0) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        try {
            message.data = signTx(message.data, this.store.keys[keyIndex]);
            message.status = 'signed'
        } catch (e) {
            message.err = {
                stack: e.stack,
                message: e.message
            };
            message.status = 'failed'
            throw e
        }
    }
    @action
    reject(id) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        message.status = 'rejected'
    }

    ...
}

जब हमें कोई नया संदेश मिलता है तो हम उसमें मेटाडेटा जोड़ते हैं, जोड़ते हैं observable और जोड़ें store.messages.

यदि आप नहीं करते हैं observable मैन्युअल रूप से, फिर सरणी में संदेश जोड़ते समय mobx स्वयं ऐसा करेगा। हालाँकि, यह एक नई वस्तु बनाएगा जिसका हमारे पास कोई संदर्भ नहीं होगा, लेकिन हमें अगले चरण के लिए इसकी आवश्यकता होगी।

इसके बाद, हम एक वादा लौटाते हैं जो संदेश की स्थिति बदलने पर हल हो जाता है। स्थिति की निगरानी प्रतिक्रिया द्वारा की जाती है, जो स्थिति बदलने पर "खुद को मार डालेगी"।

विधि कोड approve и reject बहुत सरल: यदि आवश्यक हो तो हस्ताक्षर करने के बाद हम बस संदेश की स्थिति बदल देते हैं।

हम यूआई एपीआई में स्वीकृत और अस्वीकार करते हैं, पेज एपीआई में नया संदेश डालते हैं:

export class SignerApp {
    ...
    popupApi() {
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index),

            lock: async () => this.lock(),
            unlock: async (password) => this.unlock(password),
            initVault: async (password) => this.initVault(password),

            approve: async (id, keyIndex) => this.approve(id, keyIndex),
            reject: async (id) => this.reject(id)
        }
    }

    pageApi(origin) {
        return {
            signTransaction: async (txParams) => this.newMessage(txParams, origin)
        }
    }

    ...
}

आइए अब एक्सटेंशन के साथ लेनदेन पर हस्ताक्षर करने का प्रयास करें:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

सामान्य तौर पर, सब कुछ तैयार है, जो कुछ बचा है सरल यूआई जोड़ें.

UI

इंटरफ़ेस को एप्लिकेशन स्थिति तक पहुंच की आवश्यकता है। यूआई पक्ष पर हम करेंगे observable राज्य बनाएं और एपीआई में एक फ़ंक्शन जोड़ें जो इस स्थिति को बदल देगा। आइए जोड़ें observable पृष्ठभूमि से प्राप्त एपीआई ऑब्जेक्ट के लिए:

import {observable} from 'mobx'
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode";
import {initApp} from "./ui/index";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi() {
    // Подключаемся к порту, создаем из него стрим
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Создаем пустой observable для состояния background'a
    let backgroundState = observable.object({});
    const api = {
        //Отдаем бекграунду функцию, которая будет обновлять observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Делаем RPC объект
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Добавляем в background observable со стейтом
    background.state = backgroundState;

    if (DEV_MODE) {
        global.background = background;
    }

    // Запуск интерфейса
    await initApp(background)
}

अंत में हम एप्लिकेशन इंटरफ़ेस प्रस्तुत करना शुरू करते हैं। यह एक रिएक्ट एप्लीकेशन है. बैकग्राउंड ऑब्जेक्ट को केवल प्रॉप्स का उपयोग करके पास किया जाता है। बेशक, तरीकों के लिए एक अलग सेवा और राज्य के लिए एक स्टोर बनाना सही होगा, लेकिन इस लेख के प्रयोजनों के लिए यह पर्याप्त है:

import {render} from 'react-dom'
import App from './App'
import React from "react";

// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
    render(
        <App background={background}/>,
        document.getElementById('app-content')
    );
}

mobx के साथ डेटा बदलने पर रेंडरिंग शुरू करना बहुत आसान है। हम बस ऑब्जर्वर डेकोरेटर को पैकेज से लटकाते हैं mobx-प्रतिक्रिया घटक पर, और जब घटक द्वारा संदर्भित कोई भी अवलोकन बदल जाता है तो रेंडर स्वचालित रूप से कॉल किया जाएगा। आपको किसी मैपस्टेटटूप्रॉप्स या रिडक्स की तरह कनेक्ट करने की आवश्यकता नहीं है। सब कुछ सीधे बॉक्स से बाहर काम करता है:

import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'

@observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {

    // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
    // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
    render() {
        const {keys, messages, initialized, locked} = this.props.background.state;
        const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;

        return <Fragment>
            {!initialized
                ?
                <Init onInit={initVault}/>
                :
                locked
                    ?
                    <Unlock onUnlock={unlock}/>
                    :
                    messages.length > 0
                        ?
                        <Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
                        :
                        <Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
            }
            <div>
                {!locked && <button onClick={() => lock()}>Lock App</button>}
                {initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
            </div>
        </Fragment>
    }
}

शेष घटकों को कोड में देखा जा सकता है यूआई फ़ोल्डर में.

अब एप्लिकेशन क्लास में आपको यूआई के लिए एक राज्य चयनकर्ता बनाना होगा और यूआई में बदलाव होने पर उसे सूचित करना होगा। ऐसा करने के लिए, आइए एक विधि जोड़ें getState и reactionकॉलिंग remote.updateState:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

    // public
    getState() {
        return {
            keys: this.store.keys,
            messages: this.store.newMessages,
            initialized: this.store.initialized,
            locked: this.store.locked
        }
    }

    ...

    //
    connectPopup(connectionStream) {
        const api = this.popupApi();
        const dnode = setupDnode(connectionStream, api);

        dnode.once('remote', (remote) => {
            // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
                // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
                {fireImmediately: true, delay: 500}
            );
            // Удалим подписку при отключении клиента
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

कोई वस्तु प्राप्त करते समय remote बनाया गया है reaction यूआई पक्ष पर फ़ंक्शन को कॉल करने वाली स्थिति को बदलने के लिए।

अंतिम स्पर्श एक्सटेंशन आइकन पर नए संदेशों का प्रदर्शन जोड़ना है:

function setupApp() {
...

    // Reaction на выставление текста беджа.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

तो, आवेदन तैयार है. वेब पेज लेनदेन के लिए हस्ताक्षर का अनुरोध कर सकते हैं:

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

एक सुरक्षित ब्राउज़र एक्सटेंशन लिखना

कोड यहां उपलब्ध है लिंक.

निष्कर्ष

यदि आपने लेख को अंत तक पढ़ा है, लेकिन अभी भी प्रश्न हैं, तो आप उनसे पूछ सकते हैं विस्तार के साथ भंडार. वहां आपको प्रत्येक निर्दिष्ट चरण के लिए कमिट भी मिलेंगे।

और यदि आप वास्तविक एक्सटेंशन के लिए कोड देखने में रुचि रखते हैं, तो आप इसे पा सकते हैं यहां.

कोड, रिपॉजिटरी और नौकरी विवरण सिमरेल

स्रोत: www.habr.com

एक टिप्पणी जोड़ें