Собственный VPN клиент на JavaScript. 10 часть — Объединение всех компонентов

    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
      +5
      Вы написали уже десяток статей на эту тему, но они все выглядят не как статьи, а как куски кода.
      Вы не рассматривали вариант опубликовать код на гитхабе, а здесь написать одну статью о вашем проекте?
      Хабр всё таки больше для статей, а не репозиторий исходного кода.
      Если бы разработчики openvpn стали выкладывать сюда свой код, то обычные статьи найти на хабре было бы просто нереально.
        –5
        Все что я хотел сказать, я сказал в 1 части. Сейчас я Демонстрирую ход разработки, не беспокойтесь осталась последняя часть.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое