ایک محفوظ براؤزر ایکسٹینشن لکھنا

ایک محفوظ براؤزر ایکسٹینشن لکھنا

عام "کلائنٹ-سرور" فن تعمیر کے برعکس، وکندریقرت ایپلی کیشنز کی خصوصیات ہیں:

  • صارف لاگ ان اور پاس ورڈ کے ساتھ ڈیٹا بیس کو ذخیرہ کرنے کی ضرورت نہیں ہے۔ رسائی کی معلومات کو خصوصی طور پر صارفین خود محفوظ کرتے ہیں، اور ان کی صداقت کی تصدیق پروٹوکول کی سطح پر ہوتی ہے۔
  • سرور استعمال کرنے کی ضرورت نہیں۔ ایپلیکیشن لاجک کو بلاک چین نیٹ ورک پر عمل میں لایا جا سکتا ہے، جہاں مطلوبہ ڈیٹا کو ذخیرہ کرنا ممکن ہے۔

یوزر کیز کے لیے 2 نسبتاً محفوظ اسٹوریجز ہیں - ہارڈویئر والیٹس اور براؤزر ایکسٹینشن۔ ہارڈ ویئر والیٹس زیادہ تر انتہائی محفوظ، لیکن استعمال میں مشکل اور مفت سے دور ہوتے ہیں، لیکن براؤزر ایکسٹینشن سیکیورٹی اور استعمال میں آسانی کا بہترین امتزاج ہیں، اور آخری صارفین کے لیے مکمل طور پر مفت بھی ہوسکتے ہیں۔

ان سب باتوں کو مدنظر رکھتے ہوئے، ہم سب سے محفوظ ایکسٹینشن بنانا چاہتے تھے جو لین دین اور دستخطوں کے ساتھ کام کرنے کے لیے ایک سادہ API فراہم کرکے وکندریقرت ایپلی کیشنز کی ترقی کو آسان بناتا ہے۔
ہم آپ کو ذیل میں اس تجربے کے بارے میں بتائیں گے۔

مضمون میں کوڈ کی مثالوں اور اسکرین شاٹس کے ساتھ براؤزر ایکسٹینشن لکھنے کے طریقے کے بارے میں مرحلہ وار ہدایات شامل ہوں گی۔ آپ کو تمام کوڈ اس میں مل سکتے ہیں۔ ذخیرے. ہر عہد منطقی طور پر اس مضمون کے ایک حصے سے مطابقت رکھتا ہے۔

براؤزر ایکسٹینشنز کی مختصر تاریخ

براؤزر کی توسیعات ایک طویل عرصے سے موجود ہیں۔ وہ 1999 میں انٹرنیٹ ایکسپلورر میں، 2004 میں فائر فاکس میں نمودار ہوئے۔ تاہم، بہت طویل عرصے تک توسیع کے لیے کوئی ایک معیار نہیں تھا۔

ہم کہہ سکتے ہیں کہ یہ گوگل کروم کے چوتھے ورژن میں ایکسٹینشن کے ساتھ نمودار ہوا۔ یقیناً، اس وقت کوئی تصریح نہیں تھی، لیکن یہ کروم API تھا جو اس کی بنیاد بنا: زیادہ تر براؤزر مارکیٹ کو فتح کرنے اور بلٹ ان ایپلیکیشن اسٹور رکھنے کے بعد، کروم نے درحقیقت براؤزر ایکسٹینشن کے لیے معیار قائم کیا۔

موزیلا کا اپنا ایک معیار تھا لیکن کروم ایکسٹینشن کی مقبولیت کو دیکھتے ہوئے کمپنی نے ایک مطابقت پذیر API بنانے کا فیصلہ کیا۔ 2015 میں، Mozilla کی پہل پر، کراس براؤزر ایکسٹینشن کی تفصیلات پر کام کرنے کے لیے ورلڈ وائڈ ویب کنسورشیم (W3C) کے اندر ایک خصوصی گروپ بنایا گیا۔

کروم کے لیے موجودہ API ایکسٹینشنز کو بنیاد کے طور پر لیا گیا تھا۔ یہ کام مائیکروسافٹ کے تعاون سے کیا گیا تھا (گوگل نے معیار کی ترقی میں حصہ لینے سے انکار کر دیا)، اور اس کے نتیجے میں ایک مسودہ سامنے آیا۔ وضاحتیں.

رسمی طور پر، تصریح Edge، Firefox اور Opera کے ذریعے سپورٹ کی جاتی ہے (نوٹ کریں کہ کروم اس فہرست میں نہیں ہے)۔ لیکن درحقیقت، معیار زیادہ تر کروم کے ساتھ مطابقت رکھتا ہے، کیونکہ یہ اصل میں اس کی ایکسٹینشن کی بنیاد پر لکھا گیا ہے۔ آپ WebExtensions API کے بارے میں مزید پڑھ سکتے ہیں۔ یہاں.

توسیعی ڈھانچہ

صرف ایک فائل جو ایکسٹینشن کے لیے درکار ہے وہ مینی فیسٹ (manifest.json) ہے۔ یہ توسیع کا "داخلہ نقطہ" بھی ہے۔

منشور۔

تفصیلات کے مطابق، مینی فیسٹ فائل ایک درست JSON فائل ہے۔ مینی فیسٹ کیز کی مکمل تفصیل اس معلومات کے ساتھ کہ کون سی کلیدیں معاون ہیں جس میں براؤزر دیکھا جا سکتا ہے۔ یہاں.

وہ کلیدیں جو تصریح میں نہیں ہیں "ممکن ہے" کو نظر انداز کیا جا سکتا ہے (کروم اور فائر فاکس دونوں ہی غلطیوں کی اطلاع دیتے ہیں، لیکن ایکسٹینشن کام کرتی رہتی ہیں)۔

اور میں کچھ نکات کی طرف توجہ دلانا چاہتا ہوں۔

  1. پس منظر - ایک آبجیکٹ جس میں درج ذیل فیلڈز شامل ہیں:
    1. سکرپٹ - اسکرپٹس کی ایک صف جو پس منظر کے تناظر میں عمل میں لائی جائے گی (ہم اس کے بارے میں تھوڑی دیر بعد بات کریں گے)؛
    2. صفحہ - اسکرپٹ کے بجائے جو خالی صفحے پر چلائی جائیں گی، آپ مواد کے ساتھ html کی وضاحت کر سکتے ہیں۔ اس صورت میں، اسکرپٹ فیلڈ کو نظر انداز کر دیا جائے گا، اور اسکرپٹ کو مواد کے صفحہ میں داخل کرنے کی ضرورت ہوگی۔
    3. اڑے - ایک بائنری جھنڈا، اگر متعین نہ کیا گیا ہو، براؤزر پس منظر کے عمل کو "مار" دے گا جب یہ سمجھے کہ وہ کچھ نہیں کر رہا ہے، اور اگر ضروری ہو تو اسے دوبارہ شروع کر دے گا۔ بصورت دیگر، صفحہ صرف براؤزر کے بند ہونے پر ہی اتارا جائے گا۔ فائر فاکس میں تعاون یافتہ نہیں ہے۔
  2. مواد_اسکرپٹس — اشیاء کی ایک صف جو آپ کو مختلف ویب صفحات پر مختلف اسکرپٹ لوڈ کرنے کی اجازت دیتی ہے۔ ہر آبجیکٹ میں درج ذیل اہم فیلڈز ہوتے ہیں:
    1. میچ - پیٹرن یو آر ایل، جو اس بات کا تعین کرتا ہے کہ آیا کوئی خاص مواد اسکرپٹ شامل کیا جائے گا یا نہیں۔
    2. js - اسکرپٹ کی فہرست جو اس میچ میں لوڈ کی جائیں گی۔
    3. exclude_maches - میدان سے خارج ہے match URLs جو اس فیلڈ سے مماثل ہیں۔
  3. صفحہ_عمل - اصل میں ایک ایسی چیز ہے جو براؤزر میں ایڈریس بار کے ساتھ ظاہر ہونے والے آئیکن اور اس کے ساتھ تعامل کے لیے ذمہ دار ہے۔ یہ آپ کو ایک پاپ اپ ونڈو ڈسپلے کرنے کی بھی اجازت دیتا ہے، جس کی وضاحت آپ کے اپنے HTML، CSS اور JS کے ذریعے کی گئی ہے۔
    1. ڈیفالٹ_پاپ اپ - پاپ اپ انٹرفیس کے ساتھ HTML فائل کا راستہ، CSS اور JS پر مشتمل ہو سکتا ہے۔
  4. اجازتیں - توسیعی حقوق کے انتظام کے لیے ایک صف۔ حقوق کی 3 قسمیں ہیں، جن کی تفصیل بیان کی گئی ہے۔ یہاں
  5. ویب_قابل رسائی_وسائل — توسیعی وسائل جن کی درخواست ویب صفحہ کر سکتا ہے، مثال کے طور پر، تصاویر، JS، CSS، HTML فائلیں۔
  6. externally_connectable — یہاں آپ واضح طور پر دیگر ایکسٹینشنز اور ویب پیجز کے ڈومینز کی IDs کو واضح کر سکتے ہیں جہاں سے آپ جڑ سکتے ہیں۔ ڈومین دوسری سطح یا اس سے اوپر کا ہو سکتا ہے۔ فائر فاکس میں کام نہیں کرتا۔

پھانسی کا سیاق و سباق

ایکسٹینشن میں تین کوڈ ایگزیکیوشن سیاق و سباق ہیں، یعنی ایپلیکیشن تین حصوں پر مشتمل ہے جس میں براؤزر API تک رسائی کی مختلف سطحیں ہیں۔

توسیعی سیاق و سباق

زیادہ تر API یہاں دستیاب ہے۔ اس تناظر میں وہ "رہتے ہیں":

  1. پس منظر کا صفحہ - توسیع کا "بیک اینڈ" حصہ۔ فائل کو "بیک گراؤنڈ" کلید کا استعمال کرتے ہوئے مینی فیسٹ میں بیان کیا گیا ہے۔
  2. پاپ اپ صفحہ - ایک پاپ اپ صفحہ جو ظاہر ہوتا ہے جب آپ ایکسٹینشن آئیکن پر کلک کرتے ہیں۔ منشور میں browser_action -> default_popup.
  3. کسٹم پیج - توسیعی صفحہ، منظر کے الگ ٹیب میں "رہنا" chrome-extension://<id_расширения>/customPage.html.

یہ سیاق و سباق براؤزر ونڈوز اور ٹیبز سے آزادانہ طور پر موجود ہے۔ پس منظر کا صفحہ ایک ہی کاپی میں موجود ہے اور ہمیشہ کام کرتا ہے (استثنیٰ ایونٹ کا صفحہ ہے، جب بیک گراؤنڈ اسکرپٹ کسی ایونٹ کے ذریعہ لانچ کیا جاتا ہے اور اس کے عمل کے بعد "مر جاتا ہے")۔ پاپ اپ صفحہ موجود ہے جب پاپ اپ ونڈو کھلی ہے، اور کسٹم پیج - جب کہ اس کے ساتھ ٹیب کھلا ہے۔ اس سیاق و سباق سے دوسرے ٹیبز اور ان کے مواد تک رسائی نہیں ہے۔

مواد کا اسکرپٹ سیاق و سباق

مواد کی اسکرپٹ فائل کو ہر براؤزر ٹیب کے ساتھ لانچ کیا جاتا ہے۔ اسے ایکسٹینشن کے API کے حصے اور ویب صفحہ کے DOM ٹری تک رسائی حاصل ہے۔ یہ مواد کے اسکرپٹ ہیں جو صفحہ کے ساتھ تعامل کے لیے ذمہ دار ہیں۔ ایکسٹینشنز جو 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 میں آپ کو لوڈ ان پیکڈ اور متعلقہ ایکسٹینشن والا فولڈر منتخب کرنا ہوگا - ہمارے معاملے میں dist۔

ایک محفوظ براؤزر ایکسٹینشن لکھنا

اب ہماری ایکسٹینشن انسٹال اور کام کر رہی ہے۔ آپ مندرجہ ذیل مختلف سیاق و سباق کے لیے ڈویلپر ٹولز چلا سکتے ہیں:

پاپ اپ ->

ایک محفوظ براؤزر ایکسٹینشن لکھنا

مواد کے اسکرپٹ کنسول تک رسائی اس صفحے کے کنسول کے ذریعے کی جاتی ہے جس پر اسے لانچ کیا گیا ہے۔ایک محفوظ براؤزر ایکسٹینشن لکھنا

پیغام کا تبادلہ

لہذا، ہمیں دو مواصلاتی چینلز قائم کرنے کی ضرورت ہے: ان پیج <-> پس منظر اور پاپ اپ <-> پس منظر۔ آپ یقیناً بندرگاہ پر پیغامات بھیج سکتے ہیں اور اپنا پروٹوکول ایجاد کر سکتے ہیں، لیکن میں اس نقطہ نظر کو ترجیح دیتا ہوں جو میں نے میٹاماسک اوپن سورس پروجیکٹ میں دیکھا تھا۔

یہ 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 آبجیکٹ بنائے گا، اور ان کے لیے ایک 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)
        })
    }
}

یہاں اور نیچے، عالمی کروم آبجیکٹ کے بجائے، ہم extensionApi استعمال کرتے ہیں، جو گوگل کے براؤزر میں کروم اور دوسروں میں براؤزر تک رسائی حاصل کرتا ہے۔ یہ کراس براؤزر مطابقت کے لیے کیا جاتا ہے، لیکن اس مضمون کے مقاصد کے لیے، آپ صرف '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. آئیے انہیں خریدتے ہیں۔ اب صفحہ کے پس منظر میں ایک سلسلہ ہوگا۔
  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;
}

ہم تیار ہیں صفحہ اور UI کے لیے علیحدہ API کے ساتھ ریموٹ پروسیجر کال (RPC). ایک نئے صفحہ کو پس منظر سے منسلک کرتے وقت ہم یہ دیکھ سکتے ہیں:

ایک محفوظ براؤزر ایکسٹینشن لکھنا

خالی API اور اصلیت۔ صفحہ کی طرف، ہم ہیلو فنکشن کو اس طرح کال کرسکتے ہیں:

ایک محفوظ براؤزر ایکسٹینشن لکھنا

جدید JS میں کال بیک فنکشنز کے ساتھ کام کرنا برا آداب ہے، لہذا آئیے ایک dnode بنانے کے لیے ایک چھوٹا مددگار لکھیں جو آپ کو API آبجیکٹ کو utils میں منتقل کرنے کی اجازت دیتا ہے۔

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

اور کالنگ فنکشنز ایک وعدہ واپس کرتا ہے:

ایک محفوظ براؤزر ایکسٹینشن لکھنا

غیر مطابقت پذیر افعال کے ساتھ ورژن دستیاب ہے۔ یہاں.

مجموعی طور پر، RPC اور سٹریم اپروچ کافی لچکدار معلوم ہوتا ہے: ہم سٹیم ملٹی پلیکسنگ استعمال کر سکتے ہیں اور مختلف کاموں کے لیے کئی مختلف APIs بنا سکتے ہیں۔ اصولی طور پر، dnode کہیں بھی استعمال کیا جا سکتا ہے، اہم بات یہ ہے کہ نقل و حمل کو نوڈجز سٹریم کی شکل میں لپیٹنا ہے۔

ایک متبادل 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. انفورس ایکشن پرچم کے ساتھ سخت موڈ میں، 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. یہ اصل فیلڈز کے ساتھ تبدیل کردہ تمام پراکسیوں کے ساتھ ایک نیا آبجیکٹ لوٹاتا ہے۔ عملدرآمد کے دوران، یہ آبجیکٹ کے تمام فیلڈز کو پڑھتا ہے - اس لیے گیٹرز کو متحرک کیا جاتا ہے۔

پاپ اپ کنسول میں ہم دوبارہ کئی کیز شامل کریں گے۔ اس بار وہ لوکل اسٹوریج میں بھی آگئے:

ایک محفوظ براؤزر ایکسٹینشن لکھنا

جب پس منظر کا صفحہ دوبارہ لوڈ کیا جاتا ہے، معلومات اپنی جگہ پر رہتی ہے۔

اس مقام تک تمام ایپلیکیشن کوڈ دیکھے جا سکتے ہیں۔ یہاں.

نجی چابیاں کا محفوظ ذخیرہ

واضح متن میں نجی کلیدوں کو محفوظ کرنا غیر محفوظ ہے: اس بات کا ہمیشہ امکان رہتا ہے کہ آپ کو ہیک کر لیا جائے، آپ کے کمپیوٹر تک رسائی حاصل ہو جائے، وغیرہ۔ لہذا، لوکل سٹوریج میں ہم چابیاں کو پاس ورڈ سے خفیہ کردہ شکل میں محفوظ کریں گے۔

زیادہ سیکیورٹی کے لیے، ہم ایپلیکیشن میں ایک لاک اسٹیٹ شامل کریں گے، جس میں چابیاں تک بالکل بھی رسائی نہیں ہوگی۔ وقت ختم ہونے کی وجہ سے ہم خود بخود ایکسٹینشن کو لاک حالت میں منتقل کر دیں گے۔

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

اب ہم صرف انکرپٹڈ کیز اور پاس ورڈ محفوظ کرتے ہیں۔ باقی سب کا حساب ہے۔ ہم ریاست سے پاس ورڈ ہٹا کر مقفل حالت میں منتقلی کرتے ہیں۔ عوامی 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. بیکار کے لیے آپ ٹائم آؤٹ سیٹ کر سکتے ہیں، اور OS کے ہی بلاک ہونے پر لاک سیٹ ہو جاتا ہے۔ ہم لوکل سٹوریج میں محفوظ کرنے کے لیے سلیکٹر کو بھی تبدیل کر دیں گے:

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 بہت آسان: اگر ضروری ہو تو ہم اس پر دستخط کرنے کے بعد صرف پیغام کی حیثیت کو تبدیل کرتے ہیں۔

ہم UI API میں منظور اور مسترد کرتے ہیں، صفحہ API میں newMessage:

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- رد عمل جزو پر، اور رینڈر کو خود بخود کال کیا جائے گا جب کسی بھی مشاہدے کو جزو کی تبدیلی کے ذریعے حوالہ دیا جائے گا۔ آپ کو کسی میپ اسٹیٹ ٹو پروپس کی ضرورت نہیں ہے یا ریڈکس کی طرح جڑنے کی ضرورت نہیں ہے۔ سب کچھ باکس سے باہر کام کرتا ہے:

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

...
}

لہذا، درخواست تیار ہے. ویب صفحات لین دین کے لیے دستخط کی درخواست کر سکتے ہیں:

ایک محفوظ براؤزر ایکسٹینشن لکھنا

ایک محفوظ براؤزر ایکسٹینشن لکھنا

کوڈ یہاں دستیاب ہے۔ لنک.

حاصل يہ ہوا

اگر آپ نے مضمون کو آخر تک پڑھ لیا ہے، لیکن پھر بھی سوالات ہیں، تو آپ ان سے پوچھ سکتے ہیں۔ توسیع کے ساتھ ذخیرہ. وہاں آپ کو ہر نامزد قدم کے لیے کمٹٹس بھی ملیں گے۔

اور اگر آپ اصل توسیع کے کوڈ کو دیکھنے میں دلچسپی رکھتے ہیں، تو آپ اسے تلاش کر سکتے ہیں۔ یہاں.

کوڈ، مخزن اور ملازمت کی تفصیل سے سیمیریل

ماخذ: www.habr.com

نیا تبصرہ شامل کریں