P.S. Каждая часть — это часть, сама по себе смысла не имеет, чтобы обзавестись необходимым контекстом и не испытывать когнитивный диссонанс от отсутствия так необходимых блоков текста начните читать с 1 части

После того, как все части приложения были разработаны, их можно объединить в одно большое приложение.image
Еще раз сделаю обзор на API всех компонентов.

OpenVPN— основной компонент, отвечающий за установку и использование VPN соединения.

API OpenVPN
const OpenVPN = require('./../../app/components/OpenVPN')

const ovpn = new OpenVPN()

// Обработка статусов ответов
ovpn.on(({ id, message }, other) => {
    if (id == 10 || id == 11) {
        // Установка
        ovpn.installer()
    }
    console.log(`id: ${id} | message: ${message}`)
})

// Подключение
ovpn.connect(`${__dirname}/config.ovpn`, { reconnect: true })

setTimeout(() => {
    // Отключение
    ovpn.disconnect()  
}, 30000)


Configs — Компонент, отвечающий за хранение и загрузку OpenVPN конфигов.

API Configs
const Configs = require('./../../app/components/configs')

Configs.load().then(length => {
    console.log(`Загружено конфигов: ${length}`)
    
    const search = Configs.get({
        CountryLong: 'Japan'
      , Ping: 22 // 1-22
      , NumVpnSessions: '-' // empty
      , Score: '-' // empty
      , Speed: 13311 // bit
      , Uptime: 3600000 // ms
      , TotalUsers: '-' // empty
      , TotalTraffic: '-' // empty
    })

    console.log(`Найдено конфигов: ${search.length}`)
}, () => {
    console.log(`Ошибка загрузки конфигов`)
})


Electron компоненты.

Vpn — Основной элемент управления приложением.

API Vpn
const { app }  = require('electron')
    , VPN      = require('./../../app/components/vpn')

app.on('ready', async() => {

    const Vpn = new VPN()

    // Только после того как окно инициализируется программа продолжит исполнятся
    await Vpn.ready()

    // Показываем окно
    Vpn.show()
        // Показываем Tray
    Vpn.showTray()

    // Обработчик подключения
    Vpn.onConnect(() => {
        console.log('Connect')
            // Передаем в окно статус
        Vpn.setStatus('waiting', 'yellow')
        setTimeout(() => {
            console.log('Connected!')
                // Передаем в окно статус
            Vpn.setStatus('resolve', 'blue')
        }, 4000)
    })

    // Обработчик отключения 
    Vpn.onDisconnect(() => {
        console.log('Disconnect')
            // Передаем в окно статус
        Vpn.setStatus('reject', 'red')
    })

    Vpn.onContext(() => {
        console.log('Context menu')
    })

    // Переподключатель если в течении 5 сек после объявления 
    // не будет вызвана функция Vpn.stopReconnect()
    // произойдет переподключение
    Vpn.reconnect(next => {
        console.log('Reconnect')
        next()
    }, 5000)

    // Предотвращает или останавливает переподключение
    //stopReconnect()

    // Обработчик клика на Tray
    Vpn.onTrayClick(() => {
        // Имитация подключения и отключения
        if (!Vpn.status) {
            Vpn.connect()
        } else {
            Vpn.disconnect()
        }
    })

    // Обработчик клика правой кнопкой мыши на Tray
    Vpn.onTrayRightClick(() => {
        if (Vpn.isVisible()) {
            Vpn.hide()
        } else {
            Vpn.show()
        }
    })

})


Notify — Элемент представления уведомлений.

API Notify
const { app }   = require('electron')
     , VPN      = require('./../../app/components/vpn')
     , NOTIFY   = require('./../../app/components/notify')

app.on('ready', async() => {

    const Vpn    = new VPN()
        , Notify = new NOTIFY(Vpn.root)

    // Только после того как окна инициализируются программа продолжит исполнятся
    await Promise.all([
        Notify.ready(),
        Vpn.ready()
    ])

    Vpn.show()
    Vpn.showTray()

    const SASI_FSB = [{
        title: 'Да',
        return: 'Да, подключайте меня!'
    }, {
        title: 'Нет',
        return: false
    }]

    setTimeout(() => {
        // Уведомление типа "alert"
        Notify.alert('Прогресс не остановить!', 3000, () => {
            // Уведомление типа "confirm"
            Notify.confirm('Вы хотите подключиться к VPN', CONFIRN_FSB, data => {
                console.log(data)
                if (data == false) {
                    // Устанавливает тип уведомления
                    Notify.setType('static')
                        // Уведомление типа "confirm"
                    Notify.confirm('Вы хотите выйти ?', SASI_FSB, data => {
                        if (data != false) {
                            app.quit()
                        }
                    })
                } else {
                    Vpn.setStatus('resolve', 'blue')
                }
            })
        })
    }, 2000)

    // Устанавливает тип уведомления
    // Notify.setType('static')

    // Уведомление типа "alert"
    // Notify.alert('Прогресс не остановить!', 3000, () => {})

    // Уведомление типа "confirm"
    //Notify.confirm('Вы хотите подключиться к VPN', SASI_FSB, console.log)


Context — элемент навигации по приложению.

API Context
const { app }   = require('electron')
    , VPN       = require('./../../app/components/vpn')
    , CONTEXT   = require('./../../app/components/context')

app.on('ready', async() => {

    const Vpn   = new VPN(),
        Context = new CONTEXT(Vpn.root)

    // Только после того как окна инициализируются программа продолжит исполнятся
    await Promise.all([
        Context.ready(),
        Vpn.ready()
    ])

    Vpn.show()
    Vpn.showTray()

    Vpn.onContext(() => Context.show())

    // обработчики  
    Context.onCheckIP(() => {
        console.log('Определить IP')
    })

    Context.onUpdate(() => {
        console.log('Обновить сервера')
    })

    Context.onSetting(() => {
        console.log('Настройки')
    })

    Context.onCallback(() => {
        console.log('Обратная связь')
    })

    Context.onHidden(() => {
        console.log('Свернуть')
    })

    Context.onExit(() => {
        console.log('Выход')
    })

})


Setting — Элемент настройки приложения.

API Setting
const { app } = require('electron')
    , SETTING = require('./../../app/components/setting')

app.on('ready', async() => {

    const Setting = new SETTING()

    // Только после того как окно инициализируются программа продолжит исполнятся
    await Setting.ready()

    // Показывает окно
    Setting.show()
    
    // Обработчик сохранения
    Setting.onSave(async () => {
        // Запрашиваем настройки
        const vpn_setting = await Setting.get()
        console.log(vpn_setting)
    })

})


Callback — Элемент обратной связи.

API Callback
const { app }   = require('electron')
    , CALLBACK  = require('./../../app/components/callback')

app.on('ready', async() => {

    const Callback = new CALLBACK()

    await Callback.ready()

    // Показываем окно
    Callback.show()
})


Самое время убедиться, каким действительно простым получается мой подход к разработке многооконных приложений на Electron.

Основной код, небольшой код с которого начинается разработка приложения.

Код
const { app, Menu }  = require('electron')
    , ipify          = require('ipify')
    
    , OPENVPN        = require('./components/OpenVPN')
    , Configs        = require('./components/configs')
    , NOTIFY         = require('./components/notify')
    , CONTEXT        = require('./components/context')
    , CALLBACK       = require('./components/callback')
    , SETTING        = require('./components/setting')
    , VPN            = require('./components/vpn')

app.on('ready', async() => {

    const Vpn       = new VPN()
        , Notify    = new NOTIFY(Vpn.root)
        , Context   = new CONTEXT(Vpn.root)
        , Callback  = new CALLBACK(Context.root)
        , Setting   = new SETTING(Context.root)

    await Promise.all([
        Notify.ready(),
        Context.ready(),
        Callback.ready(),
        Setting.ready(),
        Vpn.ready()
    ])

    // все окна инициализированы и готовы к взаимодействию 
    // многие ошибки возникают на почве не инициализированных окон
})


Базовый код, явно демонстрирующий способ взаимодействия компонентов между собой.

Код
const { app, Menu }  = require('electron')
    , ipify          = require('ipify')
    
    , OPENVPN        = require('./components/OpenVPN')
    , Configs        = require('./components/configs')
    , NOTIFY         = require('./components/notify')
    , CONTEXT        = require('./components/context')
    , CALLBACK       = require('./components/callback')
    , SETTING        = require('./components/setting')
    , VPN            = require('./components/vpn')

// кнопки
const CONFIRM_BUTTON_CATEGORY = [{
        title: 'Да',
        return: true
    }, {
        title: 'Нет',
        return: false
    }],
    CONFIRM_BUTTON_CONNECT = [{
        title: 'Настр.',
        return: true
    }, {
        title: 'Обнов. серв.',
        return: false
    }]

app.on('ready', async() => {

   const Vpn       = new VPN()
       , Notify    = new NOTIFY(Vpn.root)
       , Context   = new CONTEXT(Vpn.root)
       , Callback  = new CALLBACK(Context.root)
       , Setting   = new SETTING(Context.root)

    await Promise.all([
        Notify.ready(),
        Context.ready(),
        Callback.ready(),
        Setting.ready(),
        Vpn.ready()
    ])

    //o// Завершение инициализации окон //o//

    Context.onUpdate(() => (
        Notify.alert(`Обновление серверов...`, 2000, () =>
            Configs.load().then(
                length => Notify.alert(`Доступно: ${length} серверов`),
                error => Notify.confirm(`VPN сервера не обновлены. Попробовать еще раз ?`, CONFIRM_BUTTON_CATEGORY, reload => {
                    reload && Context.update()
                })
            )
        )
    ))

    Context.onCheckIP(() =>
        Notify.alert(`Определение IP ...`, 2000, () =>
            ipify().then(ip => Notify.alert(`IP: ${ip}`))
        )
    )

    Context.onSetting(() => Setting.show())

    Context.onCallback(() => Callback.show())

    Context.onHidden(() => {
        Vpn.hide()
        Context.hide()
        Callback.hide()
        Setting.hide()
        Notify.setType('static')
    })

    Context.onExit(() => app.quit())

    Vpn.onContext(() => Context.show())

    Vpn.showTray()
    Vpn.show()
})


Полный код основного файла index.js.

Код
const { app, Menu }  = require('electron')
    , ipify          = require('ipify')
    
    , OPENVPN        = require('./components/OpenVPN')
    , Configs        = require('./components/configs')
    , NOTIFY         = require('./components/notify')
    , CONTEXT        = require('./components/context')
    , CALLBACK       = require('./components/callback')
    , SETTING        = require('./components/setting')
    , VPN            = require('./components/vpn')

const CONFIRM_BUTTON_CATEGORY = [{
        title: 'Да',
        return: true
    }, {
        title: 'Нет',
        return: false
    }],
    CONFIRM_BUTTON_CONNECT = [{
        title: 'Настр.',
        return: true
    }, {
        title: 'Обнов. серв.',
        return: false
    }]

app.on('ready', async() => {

   const Vpn       = new VPN()
       , Notify    = new NOTIFY(Vpn.root)
       , Context   = new CONTEXT(Vpn.root)
       , Callback  = new CALLBACK(Context.root)
       , Setting   = new SETTING(Context.root)

    await Promise.all([
        Notify.ready(),
        Context.ready(),
        Callback.ready(),
        Setting.ready(),
        Vpn.ready()
    ])

    //o// Завершение инициализации окон //о//

    Context.onUpdate(() => (
        Notify.alert(`Обновление серверов...`, 2000, () =>
            Configs.load().then(
                length => Notify.alert(`Доступно: ${length} серверов`),
                error => Notify.confirm(`VPN сервера не обновлены. Попробовать еще раз ?`, CONFIRM_BUTTON_CATEGORY, reload => {
                    reload && Context.update()
                })
            )
        )
    ))

    Context.onCheckIP(() =>
        Notify.alert(`Определение IP ...`, 2000, () =>
            ipify().then(ip => Notify.alert(`IP: ${ip}`))
        )
    )

    Context.onSetting(() => Setting.show())

    Context.onCallback(() => Callback.show())

    Context.onHidden(() => {
        Vpn.hide()
        Context.hide()
        Callback.hide()
        Setting.hide()
        Notify.setType('static')
    })

    Context.onExit(() => app.quit())

    Vpn.onContext(() => Context.show())

    Vpn.showTray()

    Vpn.onTrayClick(() => {
        if (Vpn.status) {
            Vpn.disconnect()
        } else {
            Vpn.disconnect()
            Vpn.connect()
        }
    })

    Vpn.onTrayRightClick(() => {
        if (!Vpn.isVisible()) {
            Vpn.show()
            Notify.setType('fly')
            return
        }
        Vpn.hide()
        Context.hide()
        Callback.hide()
        Setting.hide()
        Notify.setType('static')
    })

    Setting.onSave(async() => {
        const vpn_setting = await Setting.get()
        const configs = Configs.get(vpn_setting)
        if (configs.length != 0) {
            Notify.alert(`Доступно: ${configs.length} серверов`, 6000)
        } else {
            Notify.confirm('Нет доступных для подключения серверов. Изменить настройки или обновить сервера ?', CONFIRM_BUTTON_CONNECT, config => {
                if (config) {
                    Setting.show()
                } else {
                    Context.update()
                }
            })
        }
    })

    const {
        AutoUpdate, Permutation, StartHidden
    } = await Setting.get()

    if (Permutation) {
        Vpn.center()
    }

    if (StartHidden) {
        Vpn.hide()
        Notify.setType('static')
    } else {
        Vpn.show()
    }

    if (AutoUpdate) {
        Context.update()
    }

    Configs.get({}).length == 0 && Context.update()

    //o// С в этой части кода происходит взаимодействие с основным модулем //о//

    const OpenVPN = new OPENVPN()

    OpenVPN.on(({
        id, message
    }, {
        config_information, reconnect
    }) => {

        if (id == 1) {
            Vpn.setStatus('waiting', 'yellow')
        }

        if (id == 2) {
            Vpn.stopReconnect()
            Vpn.setStatus('resolve', 'blue')
            Notify.alert('VPN подключен', 2000, () =>
                Notify.alert(config_information, 30000)
            )
        }

        if (id == 3) {
            Vpn.setStatus('waiting', 'yellow')
            Notify.alert('Переподключение к VPN', 4000)
            if (reconnect) {
                Vpn.reconnect(next => {
                    Notify.alert('Переподключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
                        next()
                    })
                })
            }
        }

        if (id == 4) {
            Vpn.setStatus('reject', 'red')
            reconnect && Vpn.reconnect(next => next())
        }

        if (id == 5) {
            if (!Vpn.isReconnect) {
                Vpn.stopReconnect()
                Vpn.setStatus('reject', 'red')
            }
        }

        if (id == 6) {
            reconnect && Vpn.reconnect(next => {
                Notify.alert('Переподключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
                    next()
                })
            })
        }

        if (id == 7) {
            Notify.alert('TCP-соединение с VPN не удалось', 4000, () => {
                Vpn.setStatus('reject', 'red')
                Vpn.disconnect()
            })
            reconnect && Vpn.reconnect(next => next())
        }

        if (id == 8) {
            reconnect && Vpn.reconnect(next => {
                Notify.alert('Подключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
                    next()
                })
            })
        }

        if (id == 9) {
            Vpn.stopReconnect()
            Vpn.setStatus('waiting', 'orange')
            Notify.alert('Установка OpenVPN')
        }

        if (id == 10 || id == 11) {
            Notify.confirm('Не удается подключиться к OpenVPN, установить необходимые компоненты ?', CONFIRM_BUTTON_CATEGORY, install => {
                if (install) {
                    OpenVPN.installer()
                } else {
                    Notify.confirm('Выйти из JS.VPN-Client ?', CONFIRM_BUTTON_CATEGORY,
                        exit => exit && app.quit()
                    )
                }
            })
        }

        if (id == 12) {
            Vpn.setStatus('reject', 'red')
            Notify.confirm('Не удалось установить TAP адаптер, попробовать еще раз ?', CONFIRM_BUTTON_CATEGORY, install => {
                if (install) {
                    OpenVPN.installer()
                } else {
                    Notify.confirm('Выйти из JS.VPN-Client ?', CONFIRM_BUTTON_CATEGORY,
                        exit => exit && app.quit()
                    )
                }
            })
        }

        if (id == 13) {
            Vpn.setStatus('reject', 'red')
            Notify.alert('Необходимые компоненты установлены!')
        }

        if (id == 14) {
            Vpn.setStatus('reject', 'red')
            Notify.alert('Неизвестная ошибка')
            reconnect && Vpn.reconnect(next => next())
        }

        console.log(`${id} | ${message}`)
    })

    Vpn.onConnect(async() => {
        const vpn_setting = await Setting.get()

        const configs = Configs.get(vpn_setting),
            random_config = parseInt(Math.random() * (configs.length - 1)),
            conf = configs[random_config]

        if (!conf) {
            return Notify.confirm('Нет доступных для подключения серверов. Изменить настройки или обновить сервера ?', CONFIRM_BUTTON_CONNECT, config => {
                if (config) {
                    Setting.show()
                } else {
                    Context.update()
                }
            })
        }

        const config_information =
            `IP: ${conf.IP}<br>` +
            `Страна: ${conf.CountryLong.length > 9 ? `${conf.CountryLong.slice(0, 9)}..` : conf.CountryLong}<br>` +
            `${conf.Ping != '-' && `Ping: ${conf.Ping}ms <br> `}` +
            `${(conf.Speed || conf.Speed != 0) ? `Скорость: ${(conf.Speed / 1024 / 1024).toFixed(2)}Mb <br> ` : ''}` +
            `${(conf.Uptime || conf.Speed != 0) ? `Uptime: ${parseUptime(conf.Uptime)} <br> ` : ''}` +
            `Подключено: ${conf.NumVpnSessions}`

        OpenVPN.connect(conf.path, {
            reconnect: vpn_setting.AutoReconnect,
            config_information
        })
    })

    Vpn.onDisconnect(vpn_setting => {
        OpenVPN.disconnect()
    })
})

//о// Прочие функции для оформления //о//

const declOfNum = (number, titles) => {
    cases = [2, 0, 1, 1, 1, 2];
    return titles[(number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5]];
}

const parseUptime = (l) => {

    const day = parseInt(l / 60000 / 60 / 24)
    const hours = parseInt(l / 60000 / 60)
    const min = parseInt(l / 60000)

    if (day) {
        return `${day} ${declOfNum(day, ['день', 'дня', 'дней'])}`
    }

    if (hours) {

        return `${hours} ${declOfNum(hours, ['час', 'часа', 'часов'])}`
    }

    if (min) {
        return `${min} ${declOfNum(min, ['минута', 'минуты', 'минут'])}`
    }

    return 'нет данных'
}


Остается только запустить.

electron .

11 часть — Сборка приложения под Windows


Собственный VPN клиент на JavaScript by JSus

Комментарии (2)


  1. limassolsk
    02.12.2018 21:21
    +1

    Вы написали уже десяток статей на эту тему, но они все выглядят не как статьи, а как куски кода.
    Вы не рассматривали вариант опубликовать код на гитхабе, а здесь написать одну статью о вашем проекте?
    Хабр всё таки больше для статей, а не репозиторий исходного кода.
    Если бы разработчики openvpn стали выкладывать сюда свой код, то обычные статьи найти на хабре было бы просто нереально.


    1. JsusDev Автор
      02.12.2018 22:07

      Все что я хотел сказать, я сказал в 1 части. Сейчас я Демонстрирую ход разработки, не беспокойтесь осталась последняя часть.