Skip to content

Commit

Permalink
feat(vpn-qr-code): feature of loading QR code image for QR code login
Browse files Browse the repository at this point in the history
  • Loading branch information
genshen committed Jun 9, 2024
1 parent 75e16af commit 3480617
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 0 deletions.
9 changes: 9 additions & 0 deletions client-ui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ func main() {
{Text: "password", Widget: uiVpnPassword},
}},
),
container.NewTabItemWithIcon(
"QR Code",
theme.MoreHorizontalIcon(),
container.NewVBox(
LoadQRImage(),
widget.NewLabel("QR code scanned"),
widget.NewButton("Load/Reload QR Code", func() {}),
),
),
)
tabs.SetTabLocation(container.TabLocationTop)
selectCopyProxyCommand := container.NewBorder(nil, nil, nil, nil,
Expand Down
25 changes: 25 additions & 0 deletions client-ui/qr_login_ui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/storage"
"github.com/genshen/wssocks-plugin-ustb/plugins/vpn"
log "github.com/sirupsen/logrus"
)

// LoadQRImage shows QR image for login
func LoadQRImage() *canvas.Image {
if qrCodeImgUrl, err := vpn.ParseQRCodeImgUrl(); err != nil {
log.Println(err) // todo
return nil
} else {
if uri, err := storage.ParseURI(qrCodeImgUrl); err != nil {
log.Println(err) // todo:
return nil
} else {
image := canvas.NewImageFromURI(uri)
image.FillMode = canvas.ImageFillOriginal
return image
}
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ require (
github.com/genshen/wssocks v0.6.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/crypto v0.18.0
gopkg.in/yaml.v3 v3.0.1
)
146 changes: 146 additions & 0 deletions plugins/vpn/qr_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package vpn

import (
"bufio"
"bytes"
"errors"
"fmt"
"gopkg.in/yaml.v3"
"net/http"
"net/url"
"strings"
)

const LoadImgUrl = "https://n.ustb.edu.cn/login/"
const FindQrcodeUrlRegex = `"ustb-qrcode",`
const FindQrcodeImgTagRegex = `<img`

// QRCodeImgLoaderConfig is the config to generate an url of sis.ustb.edu.cn for requesting qr code image.
// here we treat js as yaml (because the source string is not standard json, we can not parse it as a json)
type QRCodeImgLoaderConfig struct {
Id string `yaml:"id"`
ApiUrl string `yaml:"api_url"`
AppID string `yaml:"appid"`
ReturnUrl string `yaml:"return_url"`
RandToken string `yaml:"rand_token"`
}

// GenUrl generates the url of the iframe.
func (q *QRCodeImgLoaderConfig) GenIframeUrl() (string, error) {
if q.ApiUrl == "" {
return "", errors.New("api url is empty")
}
if q.AppID == "" {
return "", errors.New("app id is empty")
}
if q.ReturnUrl == "" {
return "", errors.New("return url is empty")
}
if q.RandToken == "" {
return "", errors.New("rand token is empty")
}
// todo encodeURI for return_url
return fmt.Sprintf("%s?appid=%s&return_url=%s&rand_token=%s&embed_flag=1", q.ApiUrl, q.AppID, q.ReturnUrl, q.RandToken), nil
}

// ParseQRCodeImgUrl uses ParseQRCodeHtmlUrl to get the iframe html,
// and then parse the html file to get final image url (contains SID)
func ParseQRCodeImgUrl() (string, error) {
iframeUrl, err := ParseQRCodeHtmlUrl()
if err != nil {
return "", err
}
htmlUri, err := url.Parse(iframeUrl)
if err != nil {
return "", err
}

// make a http request of the iframe
response, err := http.Get(iframeUrl)
if err != nil {
return "", err
}

defer response.Body.Close()

scanner := bufio.NewScanner(response.Body)
imgUrl := ""
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, FindQrcodeImgTagRegex) { // first line to match start
// line e.g. <img id="qrimg" src="/connect/qrimg?sid=3894c5568dd1ef0f6434f426297a678d" height="90%" border="0">
subStr := strings.SplitN(line, "\"", 5)
if len(subStr) != 5 {
return "", errors.New("invalid format in qr image url parsing")
} else {
imgUrl = subStr[3]
break
}
}
}

if err := scanner.Err(); err != nil {
return "", err
}

// use htmlUri's host, schema
return fmt.Sprintf("%s://%s%s", htmlUri.Scheme, htmlUri.Host, imgUrl), nil
}

func ParseQRCodeHtmlUrl() (string, error) {
// parse the html of `LOAD_IMG_URL` to get following object text:
//{
// id: "ustb-qrcode",
// api_url: "",
// appid: "",
// return_url: "",
// rand_token: "",
// width: "",
// height: ""
//}
response, err := http.Get(LoadImgUrl)
if err != nil {
}

defer response.Body.Close()

scanner := bufio.NewScanner(response.Body)
var findQrMatchStart = false
var qrConfigBuffer bytes.Buffer
qrConfigBuffer.WriteString("{")
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, FindQrcodeUrlRegex) { // first line to match start
findQrMatchStart = true // start match
}
// only start matched and end not matched, we can add text to buffer.
if findQrMatchStart {
qrConfigBuffer.WriteString(line)
if strings.Contains(line, "}") {
break
}
}
}

if err := scanner.Err(); err != nil {
return "", err
}

fmt.Println("parsed qr code config:", qrConfigBuffer.String())
var qrImgUrlConfig QRCodeImgLoaderConfig
if err := yaml.Unmarshal(qrConfigBuffer.Bytes(), &qrImgUrlConfig); err != nil {
return "", err
}

// generate iframe url.
if qrUrl, err := qrImgUrlConfig.GenIframeUrl(); err != nil {
return "", err
} else {
return qrUrl, nil
}
}

// wait qr state and get auth code
func WaitQrState(imgUrl *url.URL) {
// get https://sis.ustb.edu.cn/connect/state?sid=bf1a027b75d6e21b351f81cdc1b739a2
}
22 changes: 22 additions & 0 deletions plugins/vpn/qr_code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package vpn

import (
"fmt"
"testing"
)

func TestQRCodeHtmlUrl(t *testing.T) {
u, err := ParseQRCodeHtmlUrl()
if err != nil {
t.Error("error in loading qr code html url:", err)
}
fmt.Println(u)
}

func TestQRCodeImgUrl(t *testing.T) {
u, err := ParseQRCodeImgUrl()
if err != nil {
t.Error("error in loading qr code img url:", err)
}
fmt.Println(u)
}

0 comments on commit 3480617

Please sign in to comment.