Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature vpn qrcode login #27

Merged
merged 6 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 125 additions & 64 deletions client-ui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ const (
ProxyCommandSsh
)

const (
TextVpnAuthMethodPasswd = "Password"
TextVpnAuthMethodQrCode = "QR Code"
)

func newEntryWithText(text string) *widget.Entry {
entry := widget.NewEntry()
entry.SetText(text)
Expand Down Expand Up @@ -79,32 +84,8 @@ func main() {
}
}

// vpn input
uiVpnEnable := newCheckbox("enable ustb vpn", true, nil)
uiVpnForceLogout := newCheckbox("", true, nil)
uiVpnHostEncrypt := newCheckbox("", true, nil)
uiVpnHostInput := &widget.Entry{PlaceHolder: "vpn hostname", Text: "n.ustb.edu.cn"}
uiVpnUsername := &widget.Entry{PlaceHolder: "vpn username", Text: ""}
uiVpnPassword := &widget.Entry{PlaceHolder: "vpn password", Text: "", Password: true}

loadVPNPreference(wssApp.Preferences(), uiVpnEnable, uiVpnForceLogout,
uiVpnHostEncrypt, uiVpnHostInput, uiVpnUsername, uiVpnPassword)

uiVpnEnable.OnChanged = func(checked bool) {
if checked {
uiVpnForceLogout.Enable()
uiVpnHostEncrypt.Enable()
uiVpnHostInput.Enable()
uiVpnUsername.Enable()
uiVpnPassword.Enable()
} else {
uiVpnForceLogout.Disable()
uiVpnHostEncrypt.Disable()
uiVpnHostInput.Disable()
uiVpnUsername.Disable()
uiVpnPassword.Disable()
}
}
// create vpn ui and necessary callbacks.
vpnUi, onLoadValue, onVpnClose := loadVpnUI(&wssApp)

btnStart := widget.NewButtonWithIcon("Start", theme.MailSendIcon(), nil)
btnStart.Importance = widget.HighImportance
Expand All @@ -128,14 +109,7 @@ func main() {
LocalHttpAddr: uiHttpLocalAddr.Text,
SkipTLSVerify: uiSkipTSLVerify.Checked,
},
UstbVpn: vpn.UstbVpn{
Enable: uiVpnEnable.Checked,
ForceLogout: uiVpnForceLogout.Checked,
HostEncrypt: uiVpnHostEncrypt.Checked,
TargetVpn: uiVpnHostInput.Text,
Username: uiVpnUsername.Text,
Password: uiVpnPassword.Text,
},
UstbVpn: onLoadValue(),
RemoteAddr: uiRemoteAddr.Text,
}
btnStatus = btnStarting
Expand Down Expand Up @@ -178,33 +152,15 @@ func main() {
return
}

tabs := container.NewAppTabs(
container.NewTabItemWithIcon(
"Basic",
theme.SettingsIcon(),
&widget.Form{Items: []*widget.FormItem{
{Text: "socks5 address", Widget: uiLocalAddr},
{Text: "remote address", Widget: uiRemoteAddr},
{Text: "auth token", Widget: uiAuthToken},
{Text: "http(s) proxy", Widget: uiHttpEnable},
{Text: "http(s) address", Widget: uiHttpLocalAddr},
{Text: "skip TSL verify", Widget: uiSkipTSLVerify},
}},
),
container.NewTabItemWithIcon(
"USTB VPN",
theme.AccountIcon(),
&widget.Form{Items: []*widget.FormItem{
{Text: "enable", Widget: uiVpnEnable},
{Text: "force logout", Widget: uiVpnForceLogout},
{Text: "host encrypt", Widget: uiVpnHostEncrypt},
{Text: "vpn host", Widget: uiVpnHostInput},
{Text: "username", Widget: uiVpnUsername},
{Text: "password", Widget: uiVpnPassword},
}},
),
)
tabs.SetTabLocation(container.TabLocationTop)
basicUi := &widget.Form{Items: []*widget.FormItem{
{Text: "socks5 address", Widget: uiLocalAddr},
{Text: "remote address", Widget: uiRemoteAddr},
{Text: "auth token", Widget: uiAuthToken},
{Text: "http(s) proxy", Widget: uiHttpEnable},
{Text: "http(s) address", Widget: uiHttpLocalAddr},
{Text: "skip TSL verify", Widget: uiSkipTSLVerify},
}}

selectCopyProxyCommand := container.NewBorder(nil, nil, nil, nil,
NewWSelectWithCopyProxyCommand([]string{"git", "http/https", "ssh/sftp/scp"},
func(sel *widget.Select, value string) {
Expand All @@ -224,7 +180,8 @@ func main() {
)

w.SetContent(container.NewVBox(
widget.NewCard("Settings", "", tabs),
widget.NewCard("", "wssocks settings", basicUi), // wssocks basic ui
widget.NewCard("", "USTB VPN authentication", vpnUi), // vpn ui
btnStart,
selectCopyProxyCommand,
&widget.Separator{},
Expand Down Expand Up @@ -264,13 +221,117 @@ func main() {
handles.NotifyCloseWrapper()
}
saveBasicPreference(wssApp.Preferences(), uiLocalAddr, uiRemoteAddr, uiHttpLocalAddr, uiHttpEnable, uiSkipTSLVerify)
saveVPNPreference(wssApp.Preferences(), uiVpnEnable, uiVpnForceLogout,
uiVpnHostEncrypt, uiVpnHostInput, uiVpnUsername, uiVpnPassword)
onVpnClose()
})
//w.SetOnClosed() todo
w.ShowAndRun()
}

// loadVpnUI creates ui for ustb vpn, including auth method selection and the input box.
// it returns callback function: onAppClose for saving preference,
// loadUiValue for loading value from the input box.
func loadVpnUI(wssApp *fyne.App) (*fyne.Container, func() vpn.UstbVpn, func()) {
uiVpnEnable := newCheckbox("enable ustb vpn", true, nil)
uiVpnForceLogout := newCheckbox("", true, nil)
uiVpnHostEncrypt := newCheckbox("", true, nil)
uiVpnHostInput := &widget.Entry{PlaceHolder: "vpn hostname", Text: "n.ustb.edu.cn"}
uiVpnUsername := &widget.Entry{PlaceHolder: "vpn username", Text: ""}
uiVpnPassword := &widget.Entry{PlaceHolder: "vpn password", Text: "", Password: true}

// select auth method
uiVpnAuthMethod := widget.NewRadioGroup([]string{"Password", "QR Code"}, func(value string) {
// todo:
})
uiVpnAuthMethod.Horizontal = true
// vpn auth config buttons
uiBtnAuthPasswd := widget.NewButton("Config password auth", func() {
passwdAuthWindow := (*wssApp).NewWindow("password vpn auth")
passwdAuthWindow.SetContent(&widget.Form{Items: []*widget.FormItem{
{Text: "force logout", Widget: uiVpnForceLogout},
{Text: "host encrypt", Widget: uiVpnHostEncrypt},
{Text: "vpn host", Widget: uiVpnHostInput},
{Text: "username", Widget: uiVpnUsername},
{Text: "password", Widget: uiVpnPassword},
}})
passwdAuthWindow.Resize(fyne.NewSize(320, 0))
passwdAuthWindow.Show()
return
})
uiBtnAuthQrCode := widget.NewButton("Config QR Code auth", func() {
qrAuthWindow := (*wssApp).NewWindow("QR Code vpn auth")
qrAuthWindow.SetContent(container.NewVBox(
widget.NewLabel("QR code scanned"),
widget.NewButton("Load/Reload QR Code", func() {}),
))
qrAuthWindow.Show()
return
})

loadVPNPreference((*wssApp).Preferences(), uiVpnAuthMethod, uiVpnEnable, uiVpnForceLogout,
uiVpnHostEncrypt, uiVpnHostInput, uiVpnUsername, uiVpnPassword)

uiVpnEnable.OnChanged = func(checked bool) {
if checked {
uiVpnAuthMethod.Enable()
uiVpnForceLogout.Enable()
uiVpnHostEncrypt.Enable()
uiVpnHostInput.Enable()
uiVpnUsername.Enable()
uiVpnPassword.Enable()
uiBtnAuthPasswd.Enable()
uiBtnAuthQrCode.Enable()
} else {
uiVpnAuthMethod.Disable()
uiVpnForceLogout.Disable()
uiVpnHostEncrypt.Disable()
uiVpnHostInput.Disable()
uiVpnUsername.Disable()
uiVpnPassword.Disable()
uiBtnAuthPasswd.Disable()
uiBtnAuthQrCode.Disable()
}
}

// convert from selected string to int value
getAuthMethodInt := func() int {
if uiVpnAuthMethod.Selected == TextVpnAuthMethodPasswd {
return vpn.VpnAuthMethodPasswd
} else {
return vpn.VpnAuthMethodQRCode
}
}
// the vpn UI
vpnUi := container.NewVBox(
&widget.Form{Items: []*widget.FormItem{
{Text: "vpn enable", Widget: uiVpnEnable},
{Text: "auth method", Widget: uiVpnAuthMethod},
}},
container.NewGridWithColumns(2,
uiBtnAuthPasswd,
uiBtnAuthQrCode,
))

loadUiValues := func() vpn.UstbVpn {
return vpn.UstbVpn{
Enable: uiVpnEnable.Checked,
ForceLogout: uiVpnForceLogout.Checked,
HostEncrypt: uiVpnHostEncrypt.Checked,
TargetVpn: uiVpnHostInput.Text,
AuthMethod: getAuthMethodInt(),
PasswdAuth: vpn.UstbVpnPasswdAuth{
Username: uiVpnUsername.Text,
Password: uiVpnPassword.Text,
},
QrCodeAuth: newQrCodeAuth(wssApp),
}
}
onVpnClose := func() {
saveVPNPreference((*wssApp).Preferences(), uiVpnAuthMethod, uiVpnEnable, uiVpnForceLogout,
uiVpnHostEncrypt, uiVpnHostInput, uiVpnUsername, uiVpnPassword)
}
return vpnUi, loadUiValues, onVpnClose
}

// NewWSelectWithCopyProxyCommand is copied from widget.NewSelect.
func NewWSelectWithCopyProxyCommand(options []string, changed func(sel *widget.Select, val string)) *widget.Select {
s := &widget.Select{
Expand Down
21 changes: 20 additions & 1 deletion client-ui/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
"github.com/genshen/wssocks-plugin-ustb/plugins/vpn"
"strings"
)

Expand All @@ -14,6 +15,7 @@ const (
PrefHttpLocalAddr = "http_local_addr"
PrefSkipTSLVerify = "skip_TSL_verify"
PrefVpnEnable = "vpn_enable"
PrefVpnAuthMethod = "auth_method"
PrefVpnForceLogout = "vpn_force_logout"
PrefVpnHostEncrypt = "vpn_host_encrypt"
PrefVpnHostInput = "vpn_host"
Expand All @@ -34,6 +36,7 @@ func saveBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr,
}

func saveVPNPreference(pref fyne.Preferences,
uiVpnAuthMethod *widget.RadioGroup,
uiVpnEnable, uiVpnForceLogout, uiVpnHostEncrypt *widget.Check,
uiVpnHostInput, uiVpnUsername, uiVpnPassword *widget.Entry) {
pref.SetBool(PrefVpnEnable, uiVpnEnable.Checked)
Expand All @@ -42,6 +45,11 @@ func saveVPNPreference(pref fyne.Preferences,
pref.SetString(PrefVpnHostInput, uiVpnHostInput.Text)
pref.SetString(PrefVpnUsername, uiVpnUsername.Text)
//pref.SetString(PrefVpnPassword,uiVpnPassword.Text)
if uiVpnAuthMethod.Selected == TextVpnAuthMethodPasswd {
pref.SetInt(PrefVpnAuthMethod, vpn.VpnAuthMethodPasswd)
} else if uiVpnAuthMethod.Selected == TextVpnAuthMethodQrCode {
pref.SetInt(PrefVpnAuthMethod, vpn.VpnAuthMethodQRCode)
}
}

func loadBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr,
Expand Down Expand Up @@ -79,7 +87,7 @@ func loadBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr,
}

func loadVPNPreference(pref fyne.Preferences,
uiVpnEnable, uiVpnForceLogout, uiVpnHostEncrypt *widget.Check,
uiVpnAuthMethod *widget.RadioGroup, uiVpnEnable, uiVpnForceLogout, uiVpnHostEncrypt *widget.Check,
uiVpnHostInput, uiVpnUsername, uiVpnPassword *widget.Entry) {
if !pref.Bool(PrefHasPreference) {
return
Expand All @@ -98,6 +106,16 @@ func loadVPNPreference(pref fyne.Preferences,
uiVpnHostEncrypt.SetChecked(enable)
}

// vpn auth method
authMethod := pref.Int(PrefVpnAuthMethod)
if authMethod == vpn.VpnAuthMethodPasswd {
uiVpnAuthMethod.SetSelected(TextVpnAuthMethodPasswd)
} else if authMethod == vpn.VpnAuthMethodQRCode {
uiVpnAuthMethod.SetSelected(TextVpnAuthMethodQrCode)
} else {
// todo error
}

// vpn host, username, password
if host := pref.String(PrefVpnHostInput); strings.TrimSpace(host) != "" {
uiVpnHostInput.SetText(strings.TrimSpace(host))
Expand All @@ -111,6 +129,7 @@ func loadVPNPreference(pref fyne.Preferences,

// if vpn is disabled
if !uiVpnEnable.Checked {
uiVpnAuthMethod.Disable()
uiVpnForceLogout.Disable()
uiVpnHostEncrypt.Disable()
uiVpnHostInput.Disable()
Expand Down
78 changes: 78 additions & 0 deletions client-ui/qr_login_ui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"bytes"
"context"
"errors"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"github.com/genshen/wssocks-plugin-ustb/plugins/vpn"
"github.com/skip2/go-qrcode"
"net/http"
"time"
)

type FyneQrCodeAuth struct {
appRef *fyne.App
}

func newQrCodeAuth(app *fyne.App) vpn.QrCodeAuth {
return &FyneQrCodeAuth{
appRef: app,
}
}

func (q *FyneQrCodeAuth) ShowQrCodeAndWait(client *http.Client, cookies []*http.Cookie, qr vpn.QrImg) ([]*http.Cookie, error) {
// generate qr code from image
qrPng, err := qrcode.Encode(qr.GenQrCodeContent(), qrcode.Medium, 256)
if err != nil {
return nil, err
}
buf := bytes.NewReader(qrPng)
QrImage := canvas.NewImageFromReader(buf, "qr.png")
QrImage.FillMode = canvas.ImageFillOriginal

scanned := make(chan bool, 1) // signal of qr code scan finished
// show qr code window
qrAuthWindow := (*q.appRef).NewWindow("QR Code vpn auth")
qrAuthWindow.SetContent(container.NewVBox(
QrImage,
widget.NewLabel("scan QR code, and then click button `Finish` "),
widget.NewButton("Finish", func() {
scanned <- true
}),
))
qrAuthWindow.Show()

// wait qr code scanned or time out
// scan the qr code in 30 seconds. Otherwise, an error of Timeout will return.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

select {
case <-scanned:
qrAuthWindow.Close()
return WaitStatus(client, cookies, qr)
case <-ctx.Done():
qrAuthWindow.Close()
return nil, errors.New("scan QR code canceled due to timeout")
}
}

func WaitStatus(client *http.Client, cookies []*http.Cookie, qr vpn.QrImg) ([]*http.Cookie, error) {
// todo: set http cancel, after timeout
if state, err := vpn.WaitQrState(qr.Sid); err != nil {
fmt.Println(err)
return nil, err
} else {
if err = vpn.RedirectToLogin(client, cookies, qr.Config.AppID, state, qr.Config.RandToken); err != nil {
fmt.Println(err)
return nil, err
}
}

return nil, nil
}
7 changes: 5 additions & 2 deletions extra/go-api/wssocks_client_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ func StartClientWrapper(handlesPtr uintptr, localAddr, remoteAddr, httpLocalAddr
ForceLogout: bool(vpnForceLogout),
HostEncrypt: bool(vpnHostEncrypt),
TargetVpn: C.GoString(vpnHostInput),
Username: C.GoString(vpnUsername),
Password: C.GoString(vpnPassword),
AuthMethod: 0,
PasswdAuth: vpn.UstbVpnPasswdAuth{
Username: C.GoString(vpnUsername),
Password: C.GoString(vpnPassword),
},
},
RemoteAddr: C.GoString(remoteAddr),
}
Expand Down
Loading
Loading