هڪ محفوظ برائوزر واڌارو لکڻ

هڪ محفوظ برائوزر واڌارو لکڻ

عام "ڪلائنٽ-سرور" فن تعمير جي برعڪس، غير مرڪزي ايپليڪيشنون خاصيتون آهن:

  • صارف لاگ ان ۽ پاسورڊ سان گڏ ڊيٽابيس کي ذخيرو ڪرڻ جي ڪا ضرورت ناهي. رسائي جي معلومات خاص طور تي صارفين طرفان محفوظ ڪئي وئي آهي، ۽ انهن جي صداقت جي تصديق پروٽوڪول سطح تي ٿيندي آهي.
  • سرور استعمال ڪرڻ جي ڪا ضرورت ناهي. ايپليڪيشن منطق کي بلاڪچين نيٽ ورڪ تي عمل ڪري سگهجي ٿو، جتي اهو ممڪن آهي ته ڊيٽا جي گهربل مقدار کي ذخيرو ڪرڻ لاء.

هتي آهن 2 نسبتا محفوظ اسٽوريج لاءِ صارف چابيون - هارڊويئر والٽ ۽ برائوزر ايڪسٽينشن. هارڊويئر والٽ گهڻو ڪري انتهائي محفوظ آهن، پر استعمال ڪرڻ مشڪل ۽ مفت کان پري آهن، پر برائوزر ايڪسٽينشنز سيڪيورٽي ۽ استعمال ۾ آسانيءَ جو ڀرپور ميلاپ آهن، ۽ آخر استعمال ڪندڙن لاءِ پڻ مڪمل طور تي مفت ٿي سگهن ٿا.

انهن سڀني ڳالهين کي نظر ۾ رکندي، اسان چاهيون ٿا ته سڀ کان وڌيڪ محفوظ توسيع جيڪا غير مرڪزي ايپليڪيشنن جي ترقي کي آسان بڻائي ٿي، ٽرانزيڪشن ۽ دستخطن سان ڪم ڪرڻ لاءِ هڪ سادي API مهيا ڪندي.
اسان توهان کي هن تجربي بابت هيٺ ٻڌائينداسين.

مضمون ۾ براؤزر جي واڌ کي ڪيئن لکجي، ڪوڊ مثالن ۽ اسڪرين شاٽس سان گڏ قدم قدم جي هدايتن تي مشتمل هوندو. توھان سڀ ڪوڊ ڳولي سگھوٿا ان ۾ ذخيرو. هر ڪمٽ منطقي طور تي هن مضمون جي هڪ حصي سان ملندو آهي.

برائوزر ايڪسٽينشن جي مختصر تاريخ

برائوزر جي توسيع هڪ ڊگهي وقت جي چوڌاري آهن. اهي 1999 ۾ واپس انٽرنيٽ ايڪسپلورر ۾ ظاهر ٿيا، فائر فاکس ۾ 2004 ۾. تنهن هوندي به، هڪ تمام ڊگهي وقت لاء ڪو به معيار نه هو توسيع لاء.

اسان اهو چئي سگهون ٿا ته اهو گوگل ڪروم جي چوٿين ورزن ۾ ايڪسٽينشن سان گڏ ظاهر ٿيو. يقينا، اتي ڪا وضاحت نه هئي، پر اهو هو Chrome API جيڪو ان جو بنياد بڻجي ويو: اڪثر برائوزر مارڪيٽ کي فتح ڪرڻ ۽ هڪ بلٽ ان ايپليڪيشن اسٽور هجڻ سان، ڪروم اصل ۾ برائوزر جي واڌارن لاء معيار مقرر ڪيو.

موزيلا جو پنهنجو معيار هو، پر ڪروم ايڪسٽينشن جي مقبوليت کي ڏسندي، ڪمپني هڪ مطابقت رکندڙ API ٺاهڻ جو فيصلو ڪيو. 2015 ۾، Mozilla جي شروعات تي، ورلڊ وائڊ ويب ڪنسورشيم (W3C) جي اندر هڪ خاص گروپ ٺاهيو ويو جيڪو ڪراس برائوزر جي توسيع جي وضاحتن تي ڪم ڪرڻ لاءِ.

ڪروم لاءِ موجوده API واڌارن کي بنياد طور ورتو ويو. ڪم Microsoft جي حمايت سان ڪيو ويو (گوگل معيار جي ترقي ۾ حصو وٺڻ کان انڪار ڪيو)، ۽ نتيجي ۾ هڪ مسودو ظاهر ٿيو. وضاحتون.

رسمي طور تي، وضاحت جي حمايت ڪئي وئي آهي ايج، فائر فاکس ۽ اوپيرا (نوٽ ڪريو ته ڪروم هن فهرست تي ناهي). پر حقيقت ۾، معيار گهڻو ڪري ڪروم سان مطابقت رکي ٿو، ڇاڪاڻ ته اهو اصل ۾ ان جي توسيع جي بنياد تي لکيو ويو آهي. توھان وڌيڪ پڙھي سگھوٿا WebExtensions API بابت هتي.

واڌ جي جوڙجڪ

صرف فائل جيڪا واڌ لاءِ گهربل آهي منشور آهي (manifest.json). اهو پڻ آهي "داخلا پوائنٽ" توسيع ڏانهن.

ظاهر

وضاحت جي مطابق، ظاهر ڪيل فائل هڪ صحيح JSON فائل آهي. پڌري ڪنجين جو مڪمل تفصيل معلومات سان گڏ ڪھڙين ڪنجين کي سپورٽ ڪيو ويو آھي جنھن ۾ برائوزر ڏسي سگھجي ٿو هتي.

ڪنجيون جيڪي وضاحتن ۾ نه آهن "شايد" کي نظرانداز ڪيو وڃي (ٻئي ڪروم ۽ فائر فاڪس رپورٽ ۾ غلطيون، پر واڌايون ڪم ڪرڻ جاري رکن ٿيون).

۽ مان ڪجهه نقطن ڏانهن ڌيان ڏيڻ چاهيان ٿو.

  1. پس منظر - ھڪڙو اعتراض جنھن ۾ ھيٺيون فيلڊ شامل آھن:
    1. لکتون - اسڪرپٽ جو هڪ سلسلو جيڪو پس منظر جي حوالي سان عمل ڪيو ويندو (اسان ان بابت ٿوري دير بعد ڳالهائينداسين)؛
    2. صفحو - اسڪرپٽس جي بدران جيڪي خالي پيج تي عمل ڪيا ويندا، توھان وضاحت ڪري سگھوٿا html مواد سان. انهي صورت ۾، اسڪرپٽ فيلڊ کي نظر انداز ڪيو ويندو، ۽ اسڪرپٽ کي مواد جي صفحي ۾ داخل ڪرڻ جي ضرورت پوندي.
    3. مسلسل - هڪ بائنري پرچم، جيڪڏهن بيان نه ڪيو ويو آهي، برائوزر پس منظر واري عمل کي "ماريندو" جڏهن اهو سمجهي ٿو ته اهو ڪجهه نه ڪري رهيو آهي، ۽ جيڪڏهن ضروري هجي ته ان کي ٻيهر شروع ڪريو. ٻي صورت ۾، صفحو صرف ان لوڊ ڪيو ويندو جڏهن برائوزر بند ڪيو ويندو. فائر فاکس ۾ سپورٽ ناهي.
  2. content_scripts - شين جي هڪ صف جيڪا توهان کي مختلف ويب صفحن تي مختلف اسڪرپٽ لوڊ ڪرڻ جي اجازت ڏئي ٿي. هر اعتراض هيٺ ڏنل اهم شعبن تي مشتمل آهي:
    1. ملي - نموني url، جيڪو اهو طئي ڪري ٿو ته ڇا هڪ خاص مواد اسڪرپٽ شامل ڪيو ويندو يا نه.
    2. js - لکت جي هڪ فهرست جيڪا هن ميچ ۾ لوڊ ڪئي ويندي؛
    3. exclude_maches - فيلڊ مان نڪتل match URLs جيڪي هن فيلڊ سان ملن ٿا.
  3. صفحو_عمل - اصل ۾ ھڪڙو اعتراض آھي جيڪو آئڪن لاء ذميوار آھي جيڪو برائوزر ۾ ايڊريس بار جي اڳيان ڏيکاريل آھي ۽ ان سان رابطي ۾ آھي. اهو پڻ توهان کي هڪ پاپ اپ ونڊو ڏيکارڻ جي اجازت ڏئي ٿو، جيڪا توهان جي پنهنجي HTML، CSS ۽ JS استعمال ڪندي وضاحت ڪئي وئي آهي.
    1. default_popup - پاپ اپ انٽرفيس سان HTML فائل ڏانهن رستو، شايد CSS ۽ JS تي مشتمل هجي.
  4. اجازت - توسيع جي حقن جي انتظام لاءِ هڪ صف. حق جا 3 قسم آهن، جن کي تفصيل سان بيان ڪيو ويو آهي هتي
  5. web_accessible_resources - توسيع وسيلا جيڪي ويب پيج درخواست ڪري سگھن ٿا، مثال طور، تصويرون، JS، CSS، HTML فائلون.
  6. externally_connectable — هتي توهان واضح طور تي ٻين ايڪسٽينشنن جي IDs ۽ ويب صفحن جي ڊومينز کي واضح ڪري سگھو ٿا جن مان توهان ڳنڍي سگهو ٿا. هڪ ڊومين ٻئي سطح يا اعلي ٿي سگهي ٿو. فائر فاکس ۾ ڪم نٿو ڪري.

عملدرآمد جي حوالي سان

ايڪسٽينشن ۾ ٽي ڪوڊ ايگزيڪيوشن ڪنٽسٽس آهن، يعني ايپليڪيشن ۾ ٽن حصن تي مشتمل آهي برائوزر API تائين رسائي جي مختلف سطحن سان.

توسيع جي حوالي سان

گهڻو ڪري API موجود آهي هتي. ان سلسلي ۾ اهي "رهن ٿا":

  1. پس منظر صفحو - واڌ جو "پسند" حصو. فائل "پس منظر" کي استعمال ڪندي مينيفيسٽ ۾ بيان ڪئي وئي آھي.
  2. پاپ اپ صفحو - هڪ پاپ اپ صفحو جيڪو ظاهر ٿئي ٿو جڏهن توهان ايڪسٽينشن آئڪن تي ڪلڪ ڪندا آهيو. منشور ۾ browser_action -> default_popup.
  3. حسب ضرورت صفحو - توسيع صفحو، "رهجي" ڏسڻ جي هڪ الڳ ٽيب ۾ chrome-extension://<id_расширения>/customPage.html.

هي حوالو برائوزر ونڊوز ۽ ٽيب کان آزاد طور تي موجود آهي. پس منظر صفحو هڪ واحد ڪاپي ۾ موجود آهي ۽ هميشه ڪم ڪري ٿو (استثنائي واقعي واري صفحي جو آهي، جڏهن پس منظر اسڪرپٽ هڪ واقعي جي ذريعي شروع ڪئي وئي آهي ۽ ان جي عمل کان پوء "مرندو" آهي). پاپ اپ صفحو موجود آهي جڏهن پاپ اپ ونڊو کليل آهي، ۽ حسب ضرورت صفحو - جڏهن ته ان سان گڏ ٽيب کليل آهي. ان حوالي سان ٻين ٽيب ۽ انهن جي مواد تائين رسائي نه آهي.

مواد اسڪرپٽ جي حوالي سان

مواد اسڪرپٽ فائل هر برائوزر ٽيب سان گڏ شروع ڪئي وئي آهي. ان کي ايڪسٽينشن جي API جو حصو ۽ ويب پيج جي DOM وڻ تائين رسائي آهي. اهو مواد اسڪرپٽ آهي جيڪو صفحي سان رابطي لاء ذميوار آهي. ايڪسٽينشنون جيڪي ڊوم ٽري کي هٿي وٺن ٿيون اهي ڪنٽينٽ اسڪرپٽس ۾ ڪن ٿيون - مثال طور، اشتهار بلاڪ ڪندڙ يا مترجم. انهي سان گڏ، مواد اسڪرپٽ معياري ذريعي صفحي سان رابطو ڪري سگھن ٿا postMessage.

ويب پيج جي حوالي سان

اهو ئي اصل ويب پيج آهي. ان جو ايڪسٽينشن سان ڪو به واسطو نه آهي ۽ ان کي اتي پهچ نه آهي، سواءِ انهن صورتن جي جتي هن صفحي جو ڊومين واضح طور پڌرنامي ۾ ظاهر نه ڪيو ويو آهي (هن تي وڌيڪ هيٺ ڏنل).

پيغام جو تبادلو

ايپليڪيشن جا مختلف حصا هڪ ٻئي سان پيغامن جي مٽا سٽا ڪرڻ گهرجن. ھن لاء ھڪڙو API آھي 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.

ايپليڪيشن خاڪو

اچو ته هڪ برائوزر ايڪسٽينشن ٺاهيون جيڪو خانگي چاٻين کي محفوظ ڪري ٿو، عوامي معلومات تائين رسائي فراهم ڪري ٿو (پتو، عوامي ڪيئي صفحي سان رابطو ڪري ٿو ۽ ٽئين پارٽي جي ايپليڪيشنن کي اجازت ڏئي ٿو ته اهي ٽرانزيڪشن لاءِ دستخط جي درخواست ڪن.

ايپليڪيشن ڊولپمينٽ

اسان جي ائپليڪيشن کي لازمي طور تي صارف سان رابطو ڪرڻ گهرجي ۽ هڪ API سان گڏ صفحي کي مهيا ڪرڻ گهرجي طريقن کي ڪال ڪرڻ لاء (مثال طور، ٽرانزيڪشن تي دستخط ڪرڻ). صرف هڪ سان ٺاهيو contentscript ڪم نه ڪندو، ڇاڪاڻ ته ان کي صرف DOM تائين رسائي آهي، پر صفحي جي JS تائين نه. ذريعي ڳنڍيو runtime.connect اسان نٿا ڪري سگھون، ڇاڪاڻ ته API سڀني ڊومينز تي گهربل آهي، ۽ صرف مخصوص کي بيان ڪري سگهجي ٿو. نتيجي طور، ڊراگرام هن طرح نظر ايندو:

هڪ محفوظ برائوزر واڌارو لکڻ

اتي هڪ ٻيو اسڪرپٽ هوندو - inpage، جنهن کي اسان صفحي ۾ داخل ڪنداسين. اهو ان جي حوالي سان هلندو ۽ ايڪسٽينشن سان ڪم ڪرڻ لاءِ هڪ API مهيا ڪندو.

شروعات

سڀ برائوزر ايڪسٽينشن ڪوڊ موجود آهي 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"]
}

خالي background.js، popup.js، inpage.js ۽ contentscript.js ٺاهيو. اسان شامل ڪيو popup.html - ۽ اسان جي ايپليڪيشن اڳ ۾ ئي گوگل ڪروم ۾ لوڊ ٿي سگهي ٿي ۽ پڪ ڪريو ته اهو ڪم ڪري ٿو.

انهي جي تصديق ڪرڻ لاء، توهان ڪوڊ وٺي سگهو ٿا هتي کان. ان کان علاوه جيڪو اسان ڪيو، لنڪ ويب پيڪ استعمال ڪندي منصوبي جي اسيمبلي کي ترتيب ڏني. برائوزر ۾ ايپليڪيشن شامل ڪرڻ لاءِ، chrome://extensions ۾، توھان کي چونڊڻو پوندو لوڊ unpacked ۽ لاڳاپيل ايڪسٽينشن سان فولڊر - اسان جي صورت ۾ dist.

هڪ محفوظ برائوزر واڌارو لکڻ

ھاڻي اسان جي ايڪسٽينشن انسٽال ٿيل آھي ۽ ڪم ڪري رھي آھي. توھان ھلي سگھوٿا ڊولپر جا اوزار مختلف حوالن لاءِ ھيٺ ڏنل:

پاپ اپ ->

هڪ محفوظ برائوزر واڌارو لکڻ

مواد اسڪرپٽ ڪنسول تائين رسائي ان صفحي جي ڪنسول ذريعي ڪئي ويندي آهي جنهن تي اهو شروع ڪيو ويو آهي.هڪ محفوظ برائوزر واڌارو لکڻ

پيغام جو تبادلو

تنهن ڪري، اسان کي ٻه مواصلاتي چينل قائم ڪرڻ جي ضرورت آهي: inpage <-> پس منظر ۽ پاپ اپ <-> پس منظر. توهان، يقينا، صرف بندرگاهن ڏانهن پيغام موڪلي سگهو ٿا ۽ پنهنجو پروٽوڪول ايجاد ڪري سگهو ٿا، پر مان ان طريقي کي ترجيح ڏيان ٿو جيڪو مون ميٽاماسڪ اوپن سورس پروجيڪٽ ۾ ڏٺو.

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

هاڻي اسان هڪ ايپليڪيشن ڪلاس ٺاهينداسين. اهو پاپ اپ ۽ ويب پيج لاءِ API شيون ٺاهيندو، ۽ انهن لاءِ ڊنوڊ ٺاهيندو:

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

هتي ۽ هيٺ، گلوبل ڪروم اعتراض جي بدران، اسان استعمال ڪريون ٿا ايڪسٽينشن ايپ، جيڪو گوگل جي برائوزر ۾ ڪروم تائين رسائي ٿو ۽ ٻين ۾ برائوزر. اهو ڪراس-براؤزر مطابقت لاءِ ڪيو ويو آهي، پر هن مضمون جي مقصدن لاءِ، توهان صرف '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)
    }
}

جيئن ته ڊنوڊ اسٽريمز سان ڪم ڪري ٿو، ۽ اسان کي هڪ بندرگاهه ملي ٿو، هڪ اڊاپٽر ڪلاس جي ضرورت آهي. اهو استعمال ڪيو ويو آهي پڙهڻ لائق-اسٽريم لائبريري، جيڪو برائوزر ۾ نوڊج اسٽريمز کي لاڳو ڪري ٿو:

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. اسان ٻه وهڪرو ٺاهيندا آهيون. ھڪڙو - صفحي ڏانھن، پوسٽ ميسيج جي چوٽي تي. ان لاءِ اسان هي استعمال ڪندا آهيون هي پيڪيج metamask جي ٺاهيندڙن کان. ٻيو وهڪرو بندرگاهن کان حاصل ڪيل پس منظر ڏانهن آهي 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);
    }
}

ھاڻي اسان انپيج ۾ ھڪڙو api اعتراض ٺاھيو ۽ ان کي گلوبل تي سيٽ ڪريو:

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

اسان تيار آهيون ريموٽ پروسيسنگ ڪال (RPC) صفحي ۽ UI لاءِ الڳ API سان. جڏهن هڪ نئين صفحي کي پس منظر سان ڳنڍيندا آهيون ته اسان هن کي ڏسي سگهون ٿا:

هڪ محفوظ برائوزر واڌارو لکڻ

خالي API ۽ اصل. صفحي جي پاسي تي، اسان ھيلو فنڪشن کي ھن طرح سڏي سگھون ٿا:

هڪ محفوظ برائوزر واڌارو لکڻ

جديد JS ۾ ڪال بڪ افعال سان ڪم ڪرڻ خراب آداب آهي، تنهن ڪري اچو ته هڪ dnode ٺاهڻ لاءِ هڪ ننڍڙو مددگار لکون جيڪو توهان کي اجازت ڏئي ٿو API اعتراض کي استعمال ڪرڻ لاءِ.

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

۽ ڪالنگ فنڪشن هڪ واعدو موٽائي ٿو:

هڪ محفوظ برائوزر واڌارو لکڻ

هم وقت ساز افعال سان نسخو دستياب آهي هتي.

مجموعي طور تي، آر پي سي ۽ اسٽريم جو طريقو ڪافي لچڪدار لڳي ٿو: اسان استعمال ڪري سگھون ٿا ٻاڦ ملائيپليڪسنگ ۽ مختلف ڪمن لاءِ ڪيترائي مختلف APIs ٺاهي سگھون ٿا. اصول ۾، dnode ڪٿي به استعمال ڪري سگهجي ٿو، بنيادي شيء هڪ nodejs نديء جي صورت ۾ ٽرانسپورٽ لفافي ڪرڻ آهي.

ھڪڙو متبادل آھي JSON فارميٽ، جيڪو JSON RPC 2 پروٽوڪول کي لاڳو ڪري ٿو. جيتوڻيڪ، اھو ڪم ڪري ٿو مخصوص ٽرانسپورٽ (TCP ۽ HTTP(S))، جيڪو اسان جي صورت ۾ لاڳو نٿو ٿئي.

اندروني رياست ۽ مقامي اسٽوريج

اسان کي ايپليڪيشن جي اندروني حالت کي ذخيرو ڪرڻ جي ضرورت پوندي - گهٽ ۾ گهٽ سائن ان ڪنجيون. اسان آساني سان ايپليڪيشن ۾ رياست شامل ڪري سگھون ٿا ۽ ان کي پاپ اپ API ۾ تبديل ڪرڻ جا طريقا:

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 ڪنسول مان ڪجھ ڪنجيون شامل ڪيون ۽ ڏسو ته رياست سان ڇا ٿئي ٿو:

هڪ محفوظ برائوزر واڌارو لکڻ

رياست کي مسلسل بڻائڻ جي ضرورت آهي ته جيئن ٻيهر شروع ڪرڻ وقت ڪنجيون گم نه ٿين.

اسان ان کي مقامي اسٽوريج ۾ ذخيرو ڪنداسين، ان کي هر تبديلي سان مٿي لکندي. تنهن کان پوء، ان تائين رسائي پڻ ضروري هوندي 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 سڀني اسٽورن جي فيلڊ کي پراکسي سان تبديل ڪيو آهي ۽ انهن کي سڀني ڪالن کي روڪيو آهي. انهن پيغامن جي رڪنيت حاصل ڪرڻ ممڪن آهي.

هيٺ آئون اڪثر اصطلاح استعمال ڪندس "جڏهن تبديل ٿيڻ"، جيتوڻيڪ اهو مڪمل طور تي درست ناهي. Mobx فيلڊ تائين رسائي کي ٽريڪ ڪري ٿو. پراڪسي شين جا گيٽر ۽ سيٽرز جيڪي لائبريري ٺاهي ٿي استعمال ڪيا ويندا آهن.

ايڪشن سجاڳي جا ٻه مقصد پورا ڪن ٿا:

  1. انفورس ايڪشن پرچم سان سخت موڊ ۾، موبڪس سڌو رياست کي تبديل ڪرڻ کان منع ڪري ٿو. سخت حالتن ۾ ڪم ڪرڻ سٺو عمل سمجهيو ويندو آهي.
  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. اهو هڪ نئون اعتراض واپس ڏئي ٿو سڀني پراکسي سان اصل فيلڊ سان تبديل ٿيل. عمل جي دوران، اهو اعتراض جي سڀني شعبن کي پڙهي ٿو - تنهنڪري حاصل ڪندڙن کي متحرڪ ڪيو ويو آهي.

پاپ اپ ڪنسول ۾ اسين ٻيهر شامل ڪنداسين ڪيترن ئي ڪيچ. هن ڀيري اهي مقامي اسٽوريج ۾ پڻ ويا:

هڪ محفوظ برائوزر واڌارو لکڻ

جڏهن پس منظر واري صفحي کي ٻيهر لوڊ ڪيو ويندو آهي، معلومات پنهنجي جاء تي رهي ٿي.

ھن نقطي تائين سڀ ايپليڪيشن ڪوڊ ڏسي سگھجي ٿو هتي.

پرائيويٽ ڪنجيون جي محفوظ اسٽوريج

صاف متن ۾ خانگي چابيون محفوظ ڪرڻ غير محفوظ آهي: اتي هميشه هڪ موقعو آهي ته توهان کي هيڪ ڪيو ويندو، توهان جي ڪمپيوٽر تائين رسائي حاصل ڪريو، وغيره. تنهن ڪري، localStorage ۾ اسان چيڪن کي محفوظ ڪنداسين پاسورڊ-انڪريپ ٿيل فارم ۾.

وڌيڪ سيڪيورٽي لاءِ، اسان ايپليڪيشن ۾ هڪ بند ٿيل حالت شامل ڪنداسين، جنهن ۾ چاٻين تائين ڪا به رسائي نه هوندي. اسان وقت ختم ٿيڻ جي ڪري خودڪار طور تي واڌ کي بند ٿيل حالت ۾ منتقل ڪنداسين.

Mobx توهان کي ڊيٽا جي صرف هڪ گهٽ ۾ گهٽ سيٽ محفوظ ڪرڻ جي اجازت ڏئي ٿو، ۽ باقي خودڪار طريقي سان حساب ڪيو ويندو آهي ان جي بنياد تي. اهي نام نهاد computed ملڪيت آهن. انهن کي ڊيٽابيس ۾ نظرن سان مقابلو ڪري سگهجي ٿو:

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

ھاڻي اسان صرف انڪريپٽ ڪيل ڪيز ۽ پاسورڊ محفوظ ڪندا آھيون. باقي هر شيءِ جو حساب آهي. اسان رياست مان پاسورڊ کي هٽائڻ سان بند ٿيل رياست ڏانهن منتقلي ڪندا آهيون. عوامي API وٽ هاڻي اسٽوريج جي شروعات لاءِ هڪ طريقو آهي.

انڪرپشن لاءِ لکيل آهي crypto-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)
}

برائوزر وٽ ھڪڙو بيڪار API آھي جنھن ذريعي توھان رڪنيت حاصل ڪري سگھو ٿا ھڪڙي واقعي - رياستي تبديليون. رياست، مطابق، ٿي سگهي ٿو 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 blockchain ۽ لائبريري موجن- ٽرانزيڪشن.

پهرين، اچو ته رياست ۾ شامل ڪريون پيغامن جو هڪ صف جنهن تي دستخط ٿيڻ جي ضرورت آهي، پوءِ نئون پيغام شامل ڪرڻ، دستخط جي تصديق، ۽ انڪار ڪرڻ جا طريقا شامل ڪريو:

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 تمام سادو: اسان صرف پيغام جي صورتحال کي تبديل ڪريون ٿا، جيڪڏهن ضروري هجي ته ان تي دستخط ڪرڻ کان پوء.

اسان UI API ۾ منظور ۽ رد ڪريون ٿا، صفحي 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 اعتراض ڏانهن:

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- رد عمل جزو تي، ۽ رينڊر خودڪار طريقي سان سڏيو ويندو جڏهن ڪو به مشاهدو جزو جي تبديلي طرفان حوالو ڏنو ويو آهي. توهان کي ڪنهن به نقشي جي اسٽيٽ ٽو پروپس جي ضرورت ناهي يا ڳنڍڻ جهڙو 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}
    );

...
}

تنهن ڪري، اپليڪيشن تيار آهي. ويب صفحا ٽرانزيڪشن لاءِ دستخط جي درخواست ڪري سگھن ٿا:

هڪ محفوظ برائوزر واڌارو لکڻ

هڪ محفوظ برائوزر واڌارو لکڻ

ڪوڊ موجود آهي هتي لنڪ.

ٿڪل

جيڪڏهن توهان مضمون کي آخر تائين پڙهيو آهي، پر اڃا تائين سوال آهن، توهان انهن کان پڇي سگهو ٿا واڌ سان گڏ repositories. اتي توهان هر نامزد ٿيل قدم لاءِ ڪمٽ پڻ ڳوليندا.

۽ جيڪڏھن توھان ڏسڻ ۾ دلچسپي رکو ٿا اصل توسيع لاءِ ڪوڊ، توھان ھي ڳولي سگھوٿا هتي.

ڪوڊ، مخزن ۽ نوڪري جي وضاحت کان سيماريل

جو ذريعو: www.habr.com

تبصرو شامل ڪريو