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


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


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

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


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


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


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


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


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


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


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


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


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


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


घोषणापत्र


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


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


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


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

निष्पादन का संदर्भ


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


विस्तार का संदर्भ


अधिकांश API यहां उपलब्ध हैं। इस संदर्भ में, "लाइव":


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

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


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


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


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


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


संदेश सेवा


एप्लिकेशन के विभिन्न हिस्सों को एक दूसरे के साथ संदेशों का आदान-प्रदान करना चाहिए। ऐसा करने के लिए, एक background मैसेज भेजने के लिए runtime.sendMessage API है और एक पेज (कंटेंट स्क्रिप्ट, पॉपअप या वेब पेज पर अगर externally_connectable मौजूद है) के लिए एक मैसेज भेजने के लिए tabs.sendMessage । 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"] } 

खाली पृष्ठभूमि बनाएँ। js, popup.js, inpage.js और contentcript.js। Popup.html जोड़ें - और हमारा एप्लिकेशन पहले से ही Google Chrome में डाउनलोड किया जा सकता है और यह सुनिश्चित कर सकता है कि यह काम करता है।


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



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


पॉपअप ->



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


संदेश सेवा


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


यह Ethereum नेटवर्क के साथ काम करने के लिए एक ब्राउज़र एक्सटेंशन है। इसमें, एप्लिकेशन के विभिन्न भाग 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))) }) 

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


 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) }) } } 

इसके बाद, वैश्विक क्रोम ऑब्जेक्ट के बजाय, हम extentionApi का उपयोग करते हैं, जो 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() } } 

अब UI में एक कनेक्शन बनाएं:


 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); } } 

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


  1. हम दो धाराएँ बनाते हैं। एक पृष्ठ की ओर है, पोस्टमेसेज के ऊपर। इसके लिए हम मेटामास्क के रचनाकारों से इस पैकेज का उपयोग करते हैं । दूसरी धारा runtime.connect से प्राप्त पोर्ट के शीर्ष पर पृष्ठभूमि के लिए है। runtime.connect । उन्हें पिप करें। अब पृष्ठ की पृष्ठभूमि में एक धारा होगी।
  2. डोम में स्क्रिप्ट इंजेक्ट करें। हम स्क्रिप्ट को पंप करते हैं (इसका उपयोग प्रकट में अनुमति दी गई थी) और अंदर इसकी सामग्री के साथ एक 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; } 

हम पृष्ठ और UI के लिए एक अलग API के साथ रिमोट प्रक्रिया कॉल (RPC) के लिए तैयार हैं। एक नए पृष्ठ को पृष्ठभूमि से जोड़ते समय, हम इसे देख सकते हैं:



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



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


API ऑब्जेक्ट अब इस तरह दिखाई देंगे:


 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)) }) }); 

एक फ़ंक्शन कॉल एक वादा लौटाता है:



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


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


एक विकल्प 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) } } } 

UI कंसोल से कुछ चाबियाँ जोड़ें और देखें कि राज्य के साथ क्या हुआ:



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


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


हम 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 खेतों तक पहुंच को ट्रैक करता है। पुस्तकालय बनाने वाले छद्म वस्तुओं के गेटर्स और सेटर का उपयोग किया जाता है।


एक्शन डेकोरेटर दो उद्देश्यों की सेवा करते हैं:


  1. फ्लैग एनफोर्समेंट के साथ सख्त मोड में 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. एक हैंडलर जिसे इस डेटा में हर बार बदलाव के साथ बुलाया जाएगा।

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


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


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


पहली बार, Mobx एक चयनकर्ता का कार्य करता है और केवल उन अवलोकनों की निगरानी करता है जिनकी हमारे पास पहुँच है। यह प्रॉक्सी गेटर्स के माध्यम से किया जाता है। toJS . , . – , .


popup . localStorage:



background- .


.



: , , . localStorage .


locked, . locked .


Mobx , . — computed properties. view :


 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') } } } 

. . locked . API .


rypto-js :


 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 API, — . , , idle , active locked . idle , locked , . localStorage:


 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 waves-transactions .


, , — , :


 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 messages. , , .


, . reaction, "" .


approve reject : , , .


Approve reject API UI, newMessage — API :


 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 .


UI


. UI observable API , . observable API, background:


 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) } 

. react-. Background- props. , , store , :


 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 . observer mobx-react , observable, . mapStateToProps connect, redux. " ":


 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> } } 

UI .


UI UI. 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 , UI.


— :


 function setupApp() { ... // Reaction    . reaction( () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '', text => extensionApi.browserAction.setBadgeText({text}), {fireImmediately: true} ); ... } 

, . - :




.


निष्कर्ष


, , . .


, .


, siemarell

Source: https://habr.com/ru/post/hi451796/


All Articles