Scribens secure pasco extensio

Scribens secure pasco extensio

Dissimilis architecturae communis "clientis-servatoris", applicationes decenterales propriae sunt:

  • Non opus est database cum logins et passwords reponere. Accessus informationes solum ab ipsis utentibus reposita est, eorumque confirmatio authenticitatis fit in protocollo gradu.
  • Non opus est servo utere. Applicatio logicae exsecutioni mandari potest in retis clausurae, ubi condere potest datarum quantitatem debitam.

2 Receptationes respective tutae sunt claves usuarii - reticulae ferrariae et extensiones navigandi. Securae ferrariae maxime tutae sunt, sed difficile ad utendum et procul ab libero, at extensiones navigandi sunt perfectae complexionis securitatis et commoditatis utendi, et etiam pro usoribus finis omnino liberi esse possunt.

Haec omnia ratione habita, extensionem tutissimam facere voluimus, quae progressionem applicationum decentrarum simpliciorem reddere voluimus, providendo simplex API ad operandum cum transactionibus et subscriptionibus.
De hac experientia infra narrabimus.

Articulus gradatim instructiones continebit quomodo extensionem navigandi scribere, cum exemplis ac eenshotsscray codice. Potes invenire omnia in codice repositoria. Singula committere logice respondet sectioni huius articuli.

Brevis Historia Pasco Extensiones

Circuli extensiones diu circum fuerunt. Visi sunt in Penitus Rimor anno 1999, in Firefox in 2004 . Sed diutissime nullum signum extensionis erat.

Dicere possumus eam una cum extensionibus in quarta versione Google Chrome apparuisse. Utique, nulla tunc specificatio erat, sed Chrome API fundamentum eius factum est: maxime victis mercatus navigatri et in schedula constructus, Chrome vexillum pro extensionibus pasco actu posuit.

Mozilla suum vexillum habuit, sed videns extensiones favoris Chrome, societas API compatibile facere constituit. Anno 2015, ineunte Mozilla, specialis coetus intra Consortium Telam Mundani (W3C) creatus est ut operaretur in extensione transmissionum specificationum.

Exsistentes extensiones API Chrome pro fundamento captae sunt. Opus Microsoft adiuvante peractum est (Google evolutionis vexillum participare noluit), et inde apparuit captura cubits.

Formaliter specificatio suffragatur Edge, Firefox et Opera (nota quod Chrome non est in hoc indice). Re quidem vera, vexillum cum Chrome maxime compatitur, cum actu in suis extensionibus scriptum sit. Plus legere potes de WebExtensions API hic.

Extensio compages

Solus fasciculus qui ad extensionem requiritur est manifestus (manifest.json). Est etiam "punctum introitus" expansionis.

Manifesto

Secundum speciem, file manifesta JSON valida est. Plena descriptio clavium manifestorum cum informationibus de quibus claves sustentantur quibus navigatrum spectari potest hic.

Claves, quae in specificatione "fortasse" non sunt, neglecta sunt (utriusque Chrome et Firefox errores referunt, sed extensiones ad laborem pergunt).

Et quaedam notare velim.

  1. font - object quod includit his agris:
    1. scriptor — series scriptorum quae in contextu curriculo facienda erunt (de hoc paulo post loquemur);
    2. Page - pro scriptorum qui in pagina vacua exsecutioni mandabuntur, HTML contenta exprimere potes. In hoc casu, scriptura campus ignorabitur, et scriptorum paginae contentae inseri debebit;
    3. perseverare - Vexillum binarium, si non specificatur, navigatrum "occidet" processum curriculi cum considerat rem non esse faciendam, et sileo si opus est. Alioquin pagina tantum exonerabitur cum navigatrum clauditur. In Firefox non valet.
  2. content_scripts — ordo rerum quae te sinit diversis scriptoribus onerare ad diversas paginas. Singula haec res continet magnas agros:
    1. matches - forma urlqui determinat utrum scriptura particularis contineatur necne.
    2. js — elenchus scriptorum qui huic copulae oneratur;
    3. exclude_matches - excludit agrum match Delata quae huic campo par.
  3. page_action - re vera obiectum est quod iconem exhibet quae iuxta vectis electronici in navigatro et commercio cum eo ostenditur. Etiam permittit te ut fenestram populo exhibeas, quae definitur utens tuis propriis HTML, CSS et JS.
    1. default_popup - iter ad HTML fasciculum cum interface, contineat CSS et JS.
  4. permissiones - ordinata ad iura administrandi extensionem. Sunt 3 genera iurium, quae singillatim describuntur hic
  5. web_accessible_resources - extensiones facultates quas pagina interreti postulare potest, exempli gratia, imaginum, JS, CSS, HTML imaginum.
  6. externally_connectable - Hic explicite specificare potes IDs aliarum extensionum ac ditionum paginarum e quibus coniungere potes. Dominium potest esse secundum gradum vel superiorem. In Firefox non operatur.

Supplicium contextu

Extensio tres contextus in codice exsecutionis habet, id est, applicatio trium partium cum diversis gradibus accessus ad navigatorem API consistit.

Extensio contextus

Maxime API hic praesto est. Hoc in contextu "vivere":

  1. Background page - "backend" pars extensionis. Tabella specificata in manifesto clavem "background" utens.
  2. Populus pagina — pagina populi quae apparet cum iconem extensionis strepis. In manifesto browser_action -> default_popup.
  3. Custom page — extensio paginae, "vivens" in separato tab sententiarum chrome-extension://<id_расширения>/customPage.html.

Hic contextus sine pasco fenestrarum tabrumque existit. Background page in uno exemplari exstat et semper operatur (exceptio est pagina eventus, cum scriptionis curriculi in eventu est et post eius exsecutionem "moritur". Populus pagina est cum fenestram aperta est, et Custom page — tab cum aperta est. Ad alias tabs et earum contenta ex hoc contextu aditus non est.

Contentus scriptorum contextu

Contentum scriptorum fasciculus una cum navigatro tab. Accessum habet ad partem extensionis API et ad arborem paginae dom. Scripta contenta sunt quae commercio cum pagina responsales sunt. Extensiones quae arboris domnae tractant hoc faciunt in scriptis contentis, exempli gratia, ad obstructores vel translatores. Item, scriptor contentus cum pagina per vexillum communicare potest postMessage.

Contextus paginae interretialis

Haec pagina ipsa est. Nihil ad extensionem pertinet nec ibi aditus habet, nisi in casibus ubi paginae paginae regio in manifesto expresse (de hoc infra plura notatur).

nuntius commutationem

Variae partes applicationis nuntiis inter se permutare debent. Est API hoc runtime.sendMessage mittere nuntium background и tabs.sendMessage nuntium ad paginam mittere (contentum scriptor, populus vel pagina si available" externally_connectable). Infra exemplum cum accessione 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))
    }
)

Ad plenam communicationem per nexus creare potes runtime.connect. Propter nos accipere runtime.Portcui, cum apertum est, aliquos epistulas mittere potes. In clientelam partem, e.g. contentscriptsic videtur:

// Опять же 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"});

Servo vel background:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, 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) {
    ...
});

Est etiam res onDisconnect et methodo disconnect.

Applicationem tabula

Tractum navigatri faciamus, qui claves privatas reponit, accessum ad informationes publicas praebet (inscriptio, clavis publica cum pagina communicat ac tertia pars applicationes ad transactiones subscriptionem petendi concedit.

Applicationem progressio

Applicatio nostra et cum usore inter se cohaerere debet et paginam cum API praebere ad modos vocare (exempli gratia, ad res gestas subscribere). Fac cum uno modo contentscript non laborabit, cum ad DOM tantum accessum habeat, non autem ad JS paginae. Connect via runtime.connect non possumus, quia API in omnibus ditionibus necessarius est, et solum specificae in manifesto definiri possunt. Quam ob rem schema sic erit:

Scribens secure pasco extensio

Aliud erit scriptum - inpagequas paginam injiciemus. In contextu suo curret et API operando cum extensione providebit.

Начало

All browser extension code is available at GitHub. Per descriptionem nexus erit committendi.

In manifesto sit amet:

{
  // Имя и описание, версия. Все это будет видно в браузере в 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"]
}

Crea inanis background.js, popup.js, inpage.js et contentscript.js. Addimus popup.html - et applicatio nostra iam in Google Chrome onerari potest ac fac operare.

Ut hoc verificetur, codicem capere potes hic. Praeter ea quae fecimus, vinculum configuratur coetui rei per webpack. Applicationem ad pasco addere, in chrome://extensionibus debes eligere sarcinam inpeditam et folder cum extensione respondente - in nostro casu dist.

Scribens secure pasco extensio

Extensio autem nostra instituitur et laborat. Elit instrumenta pro diversis contextibus currere potes hoc modo:

populus ->

Scribens secure pasco extensio

Accessus ad scriptorum contentorum consolatorium exercetur per ipsum solatium paginae in quo inducitur.Scribens secure pasco extensio

nuntius commutationem

Itaque necesse est duos canales communicationis constituere: in pagina background et populus background. Utique potes, modo nuntios ad portum mittere et protocollum tuum fingere, sed accessum praefero, quem in metamask aperto fonte proiectum vidi.

Hoc navigatrum extensio est ad operandum cum reticulo Ethereo. In ea, variae partes applicationis communicant via RPC in dnode bibliotheca adhibita. Commutationem satis cito et commode disponere tibi permittit, si cum nodijs amnis onerariam praebes (obiectum quod instrumentum idem instrumenti significat);

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

Nunc applicationem classis creabimus. API obiecta pro populo et paginae creabit, et pro illis unum creabit;

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

Hic et infra, loco obiecti Chrome globalis, extensione Api utimur, qui accessus Chrome in navigatro Google et in aliis navigatur. Hoc fit ad compatibilitatem transmissionis, sed propter fines huius articuli, simpliciter uti potes 'chrome.runtime. connect'.

Exemplum applicationem in background scriptor creare scriptor:

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

Quia dnode rivis operatur, et portum accipimus, adaptatio classis necessaria est. Fit utens bibliothecae rivuli, quae instrumentis nodijs in pasco fluit;

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

Nunc nexum faciamus in 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;
    }
}

Inde nexum in scriptura contenti creamus:

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

Cum API indigemus non in scripto contento, sed in pagina directe, duo res agimus;

  1. Duo rivos creamus. Unum — versus paginam, in summitate postMessage. Hoc enim hoc utimur hoc sarcina ex metamask creatoribus. Secundus rivus est in background de portu acceptus runtime.connect. Emamus eos. Pagina iam rivum ad prospectum habebis.
  2. Injicere scriptionem in dom. Scriptum Download (in manifesto aditus ei concessum est) et tag . creare script cum contentis in medio;

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

Nunc api obiectum in pagina inpagina creamus et ad globalem ponemus:

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

Parati sumus Remotis De agendi ratione Call (RPC) separatis API ad paginam et UI. Cum novam paginam in background coniungens hanc videre possumus:

Scribens secure pasco extensio

API Vana et origo. In latere paginae, salve munus appellare possumus sic:

Scribens secure pasco extensio

Operando cum callback functionibus in recentioribus JS mali mores, ideo scribemus parvum adiutorium ut unam partem creare sinat tibi objectum API utils transmittere.

API objecta nunc sic spectant:

export class SignerApp {

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

...

}

Remotis objectum questus sic;

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

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

Muneraque vocantis reddit promissio ;

Scribens secure pasco extensio

Versionem cum asynchronous munera praesto hic.

Super, RPC rivusque accessus admodum flexibilis videtur: vapore multiplicari possumus et varias varias APIs varias facere operas. Principio, dnode alicubi adhiberi potest, summa res est onerariam in modum nodejs amnis involvere.

Vel est forma JSON, quae protocollum JSON RPC 2 conficit, sed agit cum onerariis specificis (TCP et HTTP(S)), quod in casu nostro non applicatur.

Internus status et localStorage

Oportet nos applicationis statum internum reponere - claves signandi saltem. Status applicationis et methodi mutandi in populo API satis facile possumus;

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

    ...

} 

In scaena, omnia in munere involvemus et rem applicationis ad fenestram scribemus ut cum ea e console laborare possimus:

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

Paucas claves addamus de UI console et videas quae fiunt in re publica:

Scribens secure pasco extensio

Status perseverans debet fieri ut claves cum restarting non amittant.

Nos eam in localiStorage reponemus, eamque omni mutatione inscribentes. Postmodum accessus ad illud etiam pro UI necessarium erit, et mutationes etiam subscribas velim. Secundum hoc, commodum erit notabilem tabulam creare et eius mutationes subscribere.

Nos bibliotheca uti mobx (https://github.com/mobxjs/mobx). Electio in eo cecidit quia opus non habui, sed vere studere volui.

Addamus initializationem status initialis et copiam notabilem facimus;

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

    ...

}

"Sub cucullo" mobx omnes thesauros cum procuratorio restituit et omnes vocatos intercipit. His nuntiis subscribere poterit.

Infra saepe utar vocabulo mutando, quamvis hoc minus recte. Mobx ad agros aditus semita. Getters et procuratores rerum quae bibliothecam gignit usus est.

Actio decoratorum duobus propositis inserviunt:

  1. In stricto modo cum enforceActions vexillum, mobx vetat directe mutare statum. Consuetudo bona censetur laborandi strictis condicionibus.
  2. Etiam si munus pluries mutat statum - exempli gratia, varias regiones in pluribus lineis codicis mutamus - observatores notificantur tantum cum complet. Hoc potissimum interest in frontispicio, ubi res publica necessariae ad supervacuas elementa reddendas ducunt. In nobis neque primum neque secundum maxime pertinet, sed optimas consuetudines sequemur. Sollemne est ornamentorum omnium functionum, quae statum agrorum observatorum mutant, apponere.

In curriculo adiiciemus initializationem et statum in locali Storage servatum:

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

Munus reactionem est interesting hic. Habet duas rationes;

  1. Data lego.
  2. Tractator qui hac notitia appellabitur omni tempore mutat.

Dissimilis redux, ubi diserte rem publicam accipimus argumentationem, mobx meminit, quae observabilia intra electrix accedimus, et solum cum mutatione vocat tractorem.

Gravis est prorsus intelligere quomodo mobx decernat quae observabilia subscribamus. Si scripsi electrix in codice sic() => app.storetunc reactio numquam appellabitur, cum ipsa tabularia non notetur, solum agri eius sunt.

Si scripsi sic () => app.store.keystunc rursus nihil accideret, quoniam, remotis elementis ordinatis, relatio eius non mutabitur.

Mobx electrix primum agit et solum vestigia observabilium quae accessimus habemus. Hoc fit per procuratorem. Ideo hic ponitur constructum in munere toJS. Res nova redit cum omnibus procuratoribus cum agris originalibus substitutis. In executione legit omnes agros obiecti - hinc getters utitur.

In consolatorio populi plures claves iterum addemus. Hoc tempus etiam in localStorage finivit:

Scribens secure pasco extensio

Cum pagina curriculi reposita est, notitia in loco manet.

Omnes applicationis codice usque ad hunc locum spectari possunt hic.

Secure repono claves privatas

Claves privatas in textu perspicuo recondere non tutum est: semper est casus ut detruncaberis, aditus ad computatorium tuum, et sic porro. Itaque in localisStorage claves in forma tesserae encryptatae reponamus.

Ad maiorem cautelam applicandum statum clausum apponemus, in quo omnino clavium aditus non erit. Automatice extensionem ad rempublicam clausam tempore temporis transferemus.

Mobx te permittit ut minimum copiae notitiarum tantum recondas, reliqua automatice innixa computantur. Hae proprietates sic dictae computatae sunt. Comparari possunt ad opiniones in databases:

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

Nunc tantum claves encryptas et tesseras reponemus. Cetera omnia computata sunt. Transfermus ad rempublicam clausam, subtrahendo tesseram e civitate. Publicus API nunc modum repositionis initialis habet.

Scriptum est encryption utilitates per 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)
}

Inertem API navigatrum habet per quod subscribi potes mutationes rei publicae. Publica igitur potest esse idle, active и locked. Nam otiosum tempus potes ponere, et clausum positum cum ipsa OS obsidetur. Nos quoque electrix servandi ad localisStorage mutabimus:

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

In codice ante hunc gradum est hic.

Transactions

Venimus ergo ad rem principalissimam: res in stipendio creando et signando. Undis utemur scandalo et bibliotheca fluctus-actiones.

Primum addamus in statu nuntiorum copiam signandi, adde methodos addendo novum nuntium, subscriptionem confirmantem et recusantem:

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

    ...
}

Cum novum nuntium accipimus, metadatam ei addimus, do observable et adde to store.messages.

Si non facis observable manually, tunc mobx id ipsum faciet, cum nuntiis ad aciem additis. Nihilominus novam rem creabit ad quam referentiam non habebimus, sed ad gradum proximum egebimus.

Promissionem deinde reddimus quae cum nuntius status mutationes resolvit. Status per reactionem monitorem est, quod "se ipsum occident" cum status mutatur.

Modus code approve и reject valde simplex: simpliciter statum nuntii mutamus, si necesse fuerit signans.

Approbamus et reiciamus in UI API, newMessage in pagina 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)
        }
    }

    ...
}

Nunc agamus rem cum extensione subscribere;

Scribens secure pasco extensio

Fere omnia parata sunt, quidquid reliquum est adde simplex UI *.

UI

Interface accessum ad schedulam statui indiget. In UI parte faciemus observable statum et functionem adde API qui hunc statum mutabit. Addamus observable ad API objectum a background:

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

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

setupUi().catch(console.error);

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

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

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

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

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

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

In fine incipimus applicationis instrumenti reddere. Hoc est gravissimum applicatione. Subiectum obiectum simpliciter transiit utens fulcit. Recte quidem, separatim inservire methodis ac promptuarium rei publicae, sed ad proposita huius articuli satis est;

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

Cum mobx facile est incipere reddere cum data mutationes. Simpliciter pendet aspectum decorator ex sarcina mobx-reac componens, et ipso facto vocabitur, cum observabilia quaecunque per mutationem componentium referuntur. Non opus est aliqua mapStateToProps vel connectere sicut in redux. Omnia recta de arca operatur:

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

Reliquae partes in codice considerari possunt in III folder.

Nunc in genere applicationis debes facere electorem statum UI et UI notificare quando mutatur. Hoc ut facias, methodum addamus getState и reactionvocatio 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())

        })
    }

    ...
}

Recipiens objectum remote creatus reaction mutare statum, qui munus vocat in UI parte.

Ultimus tactus est ad ostentationem nuntiorum novarum in icone extensionis addere:

function setupApp() {
...

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

...
}

Ita, applicatio parata est. Paginae interretiales postulare possunt obsignatio pro transactionibus:

Scribens secure pasco extensio

Scribens secure pasco extensio

In codice hic praesto est Link.

conclusio,

Si articulum ad finem legisti, sed adhuc interrogationes habes, eas petere potes repositoria cum extensio. Ibi invenies et committit per singulos gradus designatos.

Et si interest inspicere codicem actualis extensionis, hoc invenire potes hic.

Codex, repositorium ac officium descriptum e siemarell

Source: www.habr.com

Add a comment