ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

පොදු "සේවාදායක-සේවාදායක" ගෘහ නිර්මාණ ශිල්පය මෙන් නොව, විමධ්‍යගත යෙදුම් සංලක්ෂිත වන්නේ:

  • පරිශීලක පිවිසුම් සහ මුරපද සමඟ දත්ත සමුදායක් ගබඩා කිරීම අවශ්ය නොවේ. ප්‍රවේශ තොරතුරු පරිශීලකයින් විසින්ම පමණක් ගබඩා කර ඇති අතර ඒවායේ සත්‍යතාව තහවුරු කිරීම ප්‍රොටෝකෝල මට්ටමින් සිදු වේ.
  • සේවාදායකයක් භාවිතා කිරීමට අවශ්ය නොවේ. යෙදුම් තර්කනය බ්ලොක්චේන් ජාලයක් මත ක්‍රියාත්මක කළ හැකි අතර, අවශ්‍ය දත්ත ප්‍රමාණය ගබඩා කිරීමට හැකි වේ.

පරිශීලක යතුරු සඳහා සාපේක්ෂව ආරක්ෂිත ගබඩා 2ක් ඇත - දෘඪාංග පසුම්බි සහ බ්රවුසර දිගු. දෘඪාංග මුදල් පසුම්බි බොහෝ දුරට අතිශයින්ම ආරක්ෂිත, නමුත් භාවිතා කිරීමට අපහසු සහ නොමිලේ, නමුත් බ්‍රවුසර දිගු යනු ආරක්ෂාව සහ භාවිතයේ පහසුවෙහි පරිපූර්ණ සංකලනයක් වන අතර අවසාන පරිශීලකයින් සඳහාද සම්පූර්ණයෙන්ම නොමිලේ විය හැක.

මේ සියල්ල සැලකිල්ලට ගනිමින්, ගනුදෙනු සහ අත්සන් සමඟ වැඩ කිරීම සඳහා සරල API ලබා දීමෙන් විමධ්‍යගත යෙදුම් සංවර්ධනය සරල කරන වඩාත් ආරක්ෂිත දිගුවක් කිරීමට අපට අවශ්‍ය විය.
මෙම අත්දැකීම ගැන අපි ඔබට පහතින් කියන්නෙමු.

කේත උදාහරණ සහ තිරපිටපත් සමඟ බ්‍රව්සර් දිගුවක් ලියන ආකාරය පිළිබඳ පියවරෙන් පියවර උපදෙස් ලිපියේ අඩංගු වේ. ඔබට සියලුම කේතයන් සොයාගත හැකිය ගබඩා. සෑම කැපවීමක්ම තර්කානුකූලව මෙම ලිපියේ කොටසකට අනුරූප වේ.

බ්‍රවුසර දිගු වල කෙටි ඉතිහාසයක්

බ්‍රව්සර් දිගු දිගු කාලයක් තිස්සේ පැවතුනි. ඔවුන් 1999 දී ඉන්ටර්නෙට් එක්ස්ප්ලෝරර් හි ද, 2004 දී ෆයර්ෆොක්ස් හි ද පෙනී සිටියහ. කෙසේ වෙතත්, ඉතා දිගු කාලයක් දිගු කිරීම සඳහා තනි ප්රමිතියක් නොතිබුණි.

එය ගූගල් ක්‍රෝම් හි සිව්වන අනුවාදයේ දිගු සමඟ දිස් වූ බව අපට පැවසිය හැකිය. ඇත්ත වශයෙන්ම, එවකට කිසිදු පිරිවිතරයක් නොතිබුණි, නමුත් එය එහි පදනම බවට පත් වූයේ Chrome API ය: බ්‍රව්සර් වෙළඳපොලේ වැඩි ප්‍රමාණයක් ජයගෙන සහ ගොඩනඟන ලද යෙදුම් ගබඩාවක් තිබීම, ක්‍රෝම් ඇත්ත වශයෙන්ම බ්‍රව්සර් දිගු සඳහා ප්‍රමිතිය සකසා ඇත.

Mozilla සතුව තමන්ගේම ප්‍රමිතියක් තිබුනද ක්‍රෝම් දිගු වල ජනප්‍රියතාවය දුටු සමාගම අනුකූල API එකක් සෑදීමට තීරණය කළේය. 2015 දී, Mozilla හි මූලිකත්වයෙන්, Cross-browser extension Specifications මත වැඩ කිරීම සඳහා ලෝක ව්‍යාප්ත වෙබ් සම්මේලනය (W3C) තුළ විශේෂ කණ්ඩායමක් නිර්මාණය කරන ලදී.

Chrome සඳහා පවතින API දිගු පදනමක් ලෙස ගෙන ඇත. මයික්‍රොසොෆ්ට් හි සහාය ඇතිව වැඩ කටයුතු සිදු කරන ලදී (ගූගල් ප්‍රමිතියේ සංවර්ධනයට සහභාගී වීම ප්‍රතික්ෂේප කළේය), එහි ප්‍රති result ලයක් ලෙස කෙටුම්පතක් දර්ශනය විය. පිරිවිතර.

විධිමත් ලෙස, පිරිවිතරයන් Edge, Firefox සහ Opera විසින් සහාය දක්වයි (Chrome මෙම ලැයිස්තුවේ නොමැති බව සලකන්න). නමුත් ඇත්ත වශයෙන්ම, සම්මතය බොහෝ දුරට Chrome සමඟ අනුකූල වේ, එය ඇත්ත වශයෙන්ම එහි දිගු මත පදනම්ව ලියා ඇත. ඔබට WebExtensions API ගැන වැඩිදුර කියවිය හැක මෙහි.

දිගු ව්යුහය

දිගුව සඳහා අවශ්‍ය එකම ගොනුව මැනිෆෙස්ට් (manifest.json) වේ. එය ව්යාප්තිය සඳහා "ඇතුල් වීමේ ස්ථානය" ද වේ.

ප්‍රකාශනය

පිරිවිතරයට අනුව, මැනිෆෙස්ට් ගොනුව වලංගු JSON ගොනුවකි. බ්‍රවුසරය බැලිය හැක්කේ කුමන යතුරු සඳහා සහය දක්වන්නේද යන්න පිළිබඳ තොරතුරු සහිත මැනිෆෙස්ට් යතුරු පිළිබඳ සම්පූර්ණ විස්තරයක් මෙහි.

"විය හැක" යන පිරිවිතරයේ නොමැති යතුරු නොසලකා හරිනු ලැබේ (Chrome සහ Firefox යන දෙකම දෝෂ වාර්තා කරයි, නමුත් දිගු දිගටම ක්‍රියා කරයි).

ඒ වගේම මම කරුණු කිහිපයක් කෙරෙහි අවධානය යොමු කිරීමට කැමතියි.

  1. පසුබිම - පහත ක්ෂේත්‍ර ඇතුළත් වස්තුවක්:
    1. පිටපත් - පසුබිම් සන්දර්භය තුළ ක්‍රියාත්මක වන ස්ක්‍රිප්ට් මාලාවක් (අපි මේ ගැන ටිකක් පසුව කතා කරමු);
    2. පිටුව - හිස් පිටුවක ක්‍රියාත්මක වන ස්ක්‍රිප්ට් වෙනුවට, ඔබට අන්තර්ගතය සමඟ html සඳහන් කළ හැක. මෙම අවස්ථාවේදී, ස්ක්‍රිප්ට් ක්ෂේත්‍රය නොසලකා හරිනු ඇති අතර, ස්ක්‍රිප්ට් අන්තර්ගත පිටුවට ඇතුළු කිරීමට අවශ්‍ය වනු ඇත;
    3. දිගටම — ද්විමය ධජයක්, නිශ්චිතව දක්වා නොමැති නම්, බ්‍රවුසරය එය කිසිවක් නොකරන බව සලකන විට පසුබිම් ක්‍රියාවලිය “මරා දමනු ඇත” සහ අවශ්‍ය නම් එය නැවත ආරම්භ කරයි. එසේ නොමැතිනම්, බ්‍රවුසරය වසා ඇති විට පමණක් පිටුව මුදා හරිනු ඇත. Firefox හි සහය නොදක්වයි.
  2. අන්තර්ගත_ස්ක්‍රිප්ට් — විවිධ වෙබ් පිටු වෙත විවිධ ස්ක්‍රිප්ට් පැටවීමට ඔබට ඉඩ සලසන වස්තු සමූහයකි. සෑම වස්තුවකම පහත වැදගත් ක්ෂේත්‍ර අඩංගු වේ:
    1. තරග - රටා url, විශේෂිත අන්තර්ගත පිටපතක් ඇතුළත් වේද නැද්ද යන්න තීරණය කරයි.
    2. js - මෙම තරඟයට පටවනු ලබන ස්ක්‍රිප්ට් ලැයිස්තුවක්;
    3. ගැලපීම්_ බැහැර කරන්න - ක්ෂේත්රයෙන් බැහැර කරයි match මෙම ක්ෂේත්‍රයට ගැලපෙන URL.
  3. පිටුව_ක්‍රියාව - ඇත්ත වශයෙන්ම බ්‍රවුසරයේ ලිපින තීරුව අසල දිස්වන අයිකනය සහ එය සමඟ අන්තර් ක්‍රියා කිරීම සඳහා වගකිව යුතු වස්තුවකි. ඔබගේම HTML, CSS සහ JS භාවිතයෙන් නිර්වචනය කරන ලද උත්පතන කවුළුවක් පෙන්වීමටද එය ඔබට ඉඩ සලසයි.
    1. default_popup — උත්පතන අතුරුමුහුණත සමඟින් HTML ගොනුව වෙත යන මාර්ගය, CSS සහ JS අඩංගු විය හැක.
  4. අවසර - දිගු කිරීමේ අයිතිවාසිකම් කළමනාකරණය කිරීම සඳහා අරාවක්. විස්තරාත්මකව විස්තර කර ඇති අයිතිවාසිකම් වර්ග 3 ක් ඇත මෙහි
  5. web_accessible_sources — වෙබ් පිටුවක් ඉල්ලා සිටිය හැකි දිගු සම්පත්, උදාහරණයක් ලෙස, පින්තූර, JS, CSS, HTML ගොනු.
  6. බාහිරව_සම්බන්ධ කළ හැකි — මෙහිදී ඔබට වෙනත් දිගුවල ID සහ ඔබට සම්බන්ධ විය හැකි වෙබ් පිටු වල වසම් පැහැදිලිව සඳහන් කළ හැක. වසමක් දෙවන මට්ටම හෝ ඉහළ විය හැක. Firefox වල වැඩ කරන්නේ නැහැ.

ක්රියාත්මක කිරීමේ සන්දර්භය

දිගුවට කේත ක්‍රියාත්මක කිරීමේ සන්දර්භ තුනක් ඇත, එනම්, බ්‍රව්සර් 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"]
}

හිස් පසුබිම්.js, popup.js, inpage.js සහ contentscript.js සාදන්න. අපි popup.html එකතු කරන්නෙමු - සහ අපගේ යෙදුම දැනටමත් Google Chrome වෙත පූරණය කළ හැකි අතර එය ක්‍රියා කරන බවට වග බලා ගන්න.

මෙය සත්‍යාපනය කිරීමට, ඔබට කේතය ගත හැක මෙතනින්. අප කළ දෙයට අමතරව, සබැඳිය වෙබ්පැක් භාවිතයෙන් ව්‍යාපෘතියේ එකලස් කිරීම වින්‍යාස කර ඇත. බ්‍රවුසරයට යෙදුමක් එක් කිරීම සඳහා, chrome://extensions හි ඔබ load unpacked සහ අනුරූප දිගුව සහිත ෆෝල්ඩරය තෝරාගත යුතුය - අපගේ නඩුවේදී dist.

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

දැන් අපගේ දිගුව ස්ථාපනය කර වැඩ කරයි. ඔබට පහත පරිදි විවිධ සන්දර්භයන් සඳහා සංවර්ධක මෙවලම් ධාවනය කළ හැක:

popup ->

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

අන්තර්ගත ස්ක්‍රිප්ට් කොන්සෝලය වෙත ප්‍රවේශය එය දියත් කර ඇති පිටුවේම කොන්සෝලය හරහා සිදු කෙරේ.ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

පණිවිඩ හුවමාරුව

එබැවින්, අපි සන්නිවේදන නාලිකා දෙකක් ස්ථාපිත කළ යුතුය: inpage <-> පසුබිම සහ උත්පතන <-> පසුබිම. ඇත්ත වශයෙන්ම, ඔබට වරායට පණිවිඩ යැවීමට සහ ඔබේම ප්‍රොටෝකෝලය නිර්මාණය කළ හැකිය, නමුත් මම මෙටාමාස්ක් විවෘත කේත ව්‍යාපෘතියේ දුටු ප්‍රවේශයට වඩා කැමැත්තෙමි.

මෙය Ethereum ජාලය සමඟ වැඩ කිරීම සඳහා බ්රවුසර දිගුවකි. එහි, යෙදුමේ විවිධ කොටස් dnode පුස්තකාලය භාවිතයෙන් RPC හරහා සන්නිවේදනය කරයි. ඔබ එය ප්‍රවාහනයක් ලෙස nodejs ප්‍රවාහයක් ලබා දෙන්නේ නම් (එනම් එකම අතුරු මුහුණත ක්‍රියාත්මක කරන වස්තුවක්) හුවමාරුවක් ඉතා ඉක්මනින් සහ පහසු ලෙස සංවිධානය කිරීමට එය ඔබට ඉඩ සලසයි:

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

මෙහි සහ පහළින්, ගෝලීය Chrome වස්තුව වෙනුවට, අපි extensionApi භාවිතා කරමු, එය Google හි බ්‍රවුසරයේ Chrome වෙත ප්‍රවේශ වන අතර අනෙකුත් බ්‍රවුසරයේ බ්‍රවුසරයයි. මෙය හරස් බ්‍රවුසර් ගැළපුම සඳහා සිදු කර ඇත, නමුත් මෙම ලිපියේ අරමුණු සඳහා කෙනෙකුට '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 ප්‍රවාහ සමඟ ක්‍රියා කරන බැවින් සහ අපට වරායක් ලැබෙන බැවින්, ඇඩැප්ටර පන්තියක් අවශ්‍ය වේ. එය බ්‍රවුසරයේ nodejs ප්‍රවාහ ක්‍රියාත්මක කරන කියවිය හැකි ප්‍රවාහ පුස්තකාලය භාවිතයෙන් සාදා ඇත:

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. අපි ධාරාවන් දෙකක් නිර්මාණය කරමු. එකක් - පිටුව දෙසට, postMessage එකට උඩින්. මේ සඳහා අපි මෙය භාවිතා කරමු මෙම පැකේජය මෙටාමාස්ක් නිර්මාතෘවරුන්ගෙන්. දෙවන ප්‍රවාහය වන්නේ ලැබුණු වරාය හරහා පසුබිම් කිරීමයි 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);
    }
}

දැන් අපි inpage හි 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 සහ සම්භවය. පිටුව පැත්තෙන්, අපට මේ ආකාරයට hello ශ්‍රිතය ඇමතීමට හැකිය:

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

නවීන JS හි ආපසු ඇමතුම් ශ්‍රිතයන් සමඟ වැඩ කිරීම නරක පුරුදු වේ, එබැවින් ඔබට API වස්තුවක් උපයෝගිතා වෙත යැවීමට ඉඩ සලසන 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))
    })
});

සහ ඇමතුම් කාර්යයන් පොරොන්දුවක් ලබා දෙයි:

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

අසමමුහුර්ත ශ්‍රිත සහිත අනුවාදය පවතී මෙහි.

සමස්තයක් වශයෙන්, RPC සහ ප්‍රවාහ ප්‍රවේශය තරමක් නම්‍යශීලී බව පෙනේ: අපට වාෂ්ප බහුකාර්ය භාවිතා කළ හැකි අතර විවිධ කාර්යයන් සඳහා විවිධ API කිහිපයක් සෑදිය හැකිය. ප්‍රතිපත්තිමය වශයෙන්, 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. බලාත්මක ක්‍රියා ධජය සමඟ දැඩි ප්‍රකාරයේදී, 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 අප දායක වන්නේ කුමන නිරීක්ෂණ වලටද යන්න තීරණය කරන්නේ කෙසේද යන්න නිවැරදිව තේරුම් ගැනීම වැදගත් වේ. මම මේ වගේ code එකේ සිලෙක්ටර් එකක් ලිව්වනම්() => app.store, එවිට ප්‍රතික්‍රියාව කිසිවිටෙක කැඳවනු නොලැබේ, ගබඩාව ම නිරීක්ෂණය කළ නොහැකි බැවින්, එහි ක්ෂේත්‍ර පමණක් වේ.

මම ලිව්වේ මෙහෙම නම් () => app.store.keys, එවිට නැවතත් කිසිවක් සිදු නොවනු ඇත, මන්ද අරා මූලද්‍රව්‍ය එකතු කිරීමේදී/ඉවත් කරන විට, එයට යොමුව වෙනස් නොවේ.

Mobx ප්‍රථම වරට තේරීම් කාරකයක් ලෙස ක්‍රියා කරන අතර අප ප්‍රවේශ වී ඇති නිරීක්ෂණ පමණක් සටහන් කරයි. මේක කරන්නේ proxy getters හරහා. එම නිසා, මෙහි ඉදිකළ ශ්‍රිතය භාවිතා වේ toJS. එය මුල් ක්ෂේත්‍ර සමඟ ප්‍රතිස්ථාපනය කරන ලද සියලුම ප්‍රොක්සි සහිත නව වස්තුවක් ආපසු ලබා දෙයි. ක්‍රියාත්මක කිරීමේදී, එය වස්තුවේ සියලුම ක්ෂේත්‍ර කියවයි - එබැවින් ලබා ගන්නන් අවුලුවනු ලැබේ.

උත්පතන කොන්සෝලය තුළ අපි නැවතත් යතුරු කිහිපයක් එකතු කරන්නෙමු. මෙවර ඒවා දේශීය ගබඩාවෙන්ද අවසන් විය:

ආරක්ෂිත බ්‍රවුසර දිගුවක් ලිවීම

පසුබිම් පිටුව නැවත පූරණය කළ විට, තොරතුරු ස්ථානයේ පවතී.

මේ දක්වා සියලුම යෙදුම් කේතය නැරඹිය හැකිය මෙහි.

පුද්ගලික යතුරු සුරක්ෂිත ගබඩා කිරීම

පැහැදිලි පෙළෙහි පුද්ගලික යතුරු ගබඩා කිරීම අනාරක්ෂිත ය: ඔබ අනවසරයෙන් ඇතුළු වීමට, ඔබේ පරිගණකයට ප්‍රවේශය ලබා ගැනීමට සහ යනාදිය සඳහා සෑම විටම අවස්ථාවක් තිබේ. එබැවින්, localStorage හි අපි යතුරු මුරපදය-සංකේතනය කළ ආකාරයෙන් ගබඩා කරමු.

වැඩි ආරක්ෂාවක් සඳහා, අපි යෙදුමට අගුලු දැමූ තත්වයක් එක් කරන්නෙමු, එහි යතුරු වෙත ප්‍රවේශයක් නොමැත. කල් ඉකුත්වීම හේතුවෙන් අපි දිගුව ස්වයංක්‍රීයව අගුලු දැමූ තත්වයට මාරු කරන්නෙමු.

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. idle සඳහා ඔබට කල් ඉකුත්වීමක් සැකසිය හැක, OS එකම අවහිර වූ විට අගුලු දමා ඇත. අපි 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 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 ඉතා සරලයි: අවශ්‍ය නම් එය අත්සන් කිරීමෙන් පසු අපි පණිවිඩයේ තත්ත්වය සරලව වෙනස් කරමු.

අපි API පිටුවේ UI 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-react සංරචකය මත, සහ විදැහුම්කරණය සංරචකය මගින් යොමු කරන ලද යම් නිරීක්ෂණ වෙනස් වූ විට ස්වයංක්‍රීයව කැඳවනු ලැබේ. ඔබට කිසිදු mapStateToProps හෝ 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

මූලාශ්රය: www.habr.com

අදහස් එක් කරන්න