1
0
mirror of https://github.com/fumiama/go-nd-portal.git synced 2026-06-05 00:10:25 +08:00

feat: support new dorm area login (#1)

## Pull Request Overview

This PR enhances the login CLI to support a new “dorm area” and replaces the old `-x` flag with two new flags for server host (`-s`) and login type (`-t`).  
- Introduces a `LoginType` enum and dynamic domain/AcID mapping in `Portal`.  
- Refactors URL construction to use `go-querystring` and JSON-encoded user info.  
- Updates tests and removes the hard-coded `-x` logic in `cmd/main.go`.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                   | Description                                                     |
| ---------------------- | --------------------------------------------------------------- |
| portal/server_test.go  | Updated tests to supply the new `loginType` parameter and use `GetUserInfo`. |
| portal/server.go       | Reworked URL builders (`GetChallengeURL`, `GetLoginURL`), JSON `UserInfo`, and dynamic flags. |
| portal/portal.go       | Added `LoginType`, `ToDomainAcID`, updated `NewPortal`, `GetChallenge`, and `Login` signatures. |
| cmd/main.go            | Removed `-x`, added `-s`/`-t` flags, wired them through to `Portal`. |
| base64/base64.go       | Fixed typo in package comment.                                  |
| go.mod                 | Added `github.com/google/go-querystring` dependency.            |
</details>



<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**base64/base64.go:1**
* Typo in the comment: 'talbe' should be 'table'.
```
// Package base64 with customized talbe
```
**portal/portal.go:53**
* New login types (`qsh-dx`, `qshd-dx`, `qshd-cmcc`) were added but there are no unit tests verifying `ToDomainAcID` returns the correct domain and AC ID for each, or that invalid types produce an error. Consider adding tests for each mapping and an invalid case.
```
func (lt LoginType) ToDomainAcID() (string, string, error) {
```
**cmd/main.go:54**
* [nitpick] The flag variables `s` and `t` are not descriptive. Consider renaming them to `server` and `loginType` (or similar) to improve clarity for users and maintainers.
```
s := flag.String("s", portal.PortalServerIPQsh, "login host")
```
**cmd/main.go:106**
* The call to `netip.ParseAddr` requires importing the `net/netip` package, but `netip` is not imported. Add `import "net/netip"` to fix the compile error.
```
_, err := netip.ParseAddr(*s)
```
</details>

---------

Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
This commit is contained in:
chasey-dev
2025-05-18 20:03:34 +08:00
committed by GitHub
parent 2371c0a51b
commit 0fb9dda436
7 changed files with 349 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
// Package base64 with customized talbe
// Package base64 with customized table
package base64
import b64 "encoding/base64"

View File

@@ -51,7 +51,8 @@ func Main() {
h := flag.Bool("h", false, "display this help")
w := flag.Bool("w", false, "only display warn-or-higher-level log")
d := flag.Bool("d", false, "display debug-level log")
x := flag.Bool("x", false, "do dx login")
s := flag.String("s", portal.PortalServerIPQsh, "login host")
t := flag.String("t", "qsh-edu", "login type [qsh-edu | qsh-dx | qshd-dx | qshd-cmcc]")
flag.Parse()
if *h {
fmt.Println("Usage:")
@@ -98,27 +99,36 @@ func Main() {
*p = helper.BytesToString(data)
fmt.Println()
}
ptl, err := portal.NewPortal(*n, *p, ip)
logrus.Debugf("server addr: %s, login type: %s", *s, *t)
if *s != portal.PortalServerIPQsh {
// just validate IP here,
// dont convert to net.IP because we need only its string later
_, err := netip.ParseAddr(*s)
if err != nil {
logrus.Errorln(err)
os.Exit(line())
}
}
// n : username
// p: password
// ip : public ip
// *t : login type
ptl, err := portal.NewPortal(*n, *p, ip, portal.LoginType(*t))
if err != nil {
logrus.Errorln(err)
os.Exit(line())
}
u := portal.PortalGetChallenge
if *x {
u = portal.PortalGetChallengeDX
}
challenge, err := ptl.GetChallenge(u)
// input:
// server IP
challenge, err := ptl.GetChallenge(*s)
if err != nil {
logrus.Errorln(err)
os.Exit(line())
}
u = portal.PortalLogin
dm := portal.PortalDomain
if *x {
u = portal.PortalLoginDX
dm = portal.PortalDomainDX
}
err = ptl.Login(u, dm, challenge)
// input:
// server IP
// challenge
err = ptl.Login(*s, challenge)
if err != nil {
logrus.Errorln(err)
os.Exit(line())

1
go.mod
View File

@@ -3,6 +3,7 @@ module github.com/fumiama/go-nd-portal
go 1.19
require (
github.com/google/go-querystring v1.1.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.7.0
golang.org/x/term v0.2.0

5
go.sum
View File

@@ -1,6 +1,10 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@@ -13,6 +17,7 @@ golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

View File

@@ -1,3 +1,4 @@
// Package portal handles login process
package portal
import (
@@ -6,9 +7,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"time"
"github.com/sirupsen/logrus"
@@ -17,35 +16,107 @@ import (
)
var (
// ErrIllegalIPv4 is returned when an invalid IPv4 address is provided
ErrIllegalIPv4 = errors.New("illegal ipv4")
// ErrIllegalLoginType is returned when an invalid login type is provided
ErrIllegalLoginType = errors.New("illegal login type")
// ErrUnexpectedChallengeResponse is returned when challenge is shorter than expected
ErrUnexpectedChallengeResponse = errors.New("unexpected challenge response")
// ErrUnexpectedLoginResponse is returned when login resp is shorter than expected
ErrUnexpectedLoginResponse = errors.New("unexpected login response")
)
// Portal struct for login config
type Portal struct {
nam string
pwd string
ip net.IP
name string
pswd string
ip net.IP
domain string
acid string
}
// LoginType defines known login types
type LoginType string
const (
// LoginTypeQshEdu edu in Qsh work area
LoginTypeQshEdu LoginType = "qsh-edu"
// LoginTypeQshDX dx in Qsh work area
LoginTypeQshDX LoginType = "qsh-dx"
// LoginTypeQshDormDX dx in Qsh new dorm area
LoginTypeQshDormDX LoginType = "qshd-dx"
// LoginTypeQshDormCMCC cmcc in Qsh new dorm area
LoginTypeQshDormCMCC LoginType = "qshd-cmcc"
)
// ToDomainAcID converts LoginType to domain and acid
func (lt LoginType) ToDomainAcID() (string, string, error) {
var domain, acid string
switch lt {
case LoginTypeQshEdu:
// qsh-edu is assumed that cant login from dorm
domain = PortalDomainQsh
acid = AcIDQsh
case LoginTypeQshDX:
domain = PortalDomainQshDX
acid = AcIDQsh
case LoginTypeQshDormDX:
domain = PortalDomainQshDX
acid = AcIDQshDorm
case LoginTypeQshDormCMCC:
domain = PortalDomainQshCMCC
acid = AcIDQshDorm
default:
return "", "", ErrIllegalLoginType
}
return domain, acid, nil
}
// rsp struct for converting from raw response data to JSON
type rsp struct {
Challenge string `json:"challenge"`
Error string `json:"error"`
}
func NewPortal(name, password string, ipv4 net.IP) (*Portal, error) {
// NewPortal creates a new Portal instance
func NewPortal(name, password string, ipv4 net.IP, loginType LoginType) (*Portal, error) {
if len(ipv4) != 4 {
return nil, ErrIllegalIPv4
}
domain, acid, err := loginType.ToDomainAcID()
if err != nil {
return nil, err
}
logrus.Debugf("portal domain: %s, ac_id: %s", domain, acid)
return &Portal{
nam: name,
pwd: password,
ip: ipv4,
name: name,
pswd: password,
ip: ipv4,
domain: domain,
acid: acid,
}, nil
}
func (p *Portal) GetChallenge(u string) (string, error) {
u = fmt.Sprintf(u, "gondportal", url.QueryEscape(p.nam), p.ip, time.Now().UnixMilli())
// GetChallenge gets token for encryption from server
// input:
// server IP
func (p *Portal) GetChallenge(sIP string) (string, error) {
// Note: no need to do URL encoding here
u, err := GetChallengeURL(
sIP,
"gondportal",
p.name,
p.domain,
p.ip,
time.Now().UnixMilli(),
)
if err != nil {
return "", err
}
logrus.Debugln("GET", u)
data, err := requestDataWith(u, "GET", PortalHeaderUA)
if err != nil {
@@ -67,17 +138,42 @@ func (p *Portal) GetChallenge(u string) (string, error) {
return r.Challenge, nil
}
// PasswordHMd5 encrypts password with hmacmd5 algorithm
func (p *Portal) PasswordHMd5(challenge string) string {
var buf [16]byte
h := hmac.New(md5.New, helper.StringToBytes(challenge))
_, _ = h.Write(helper.StringToBytes(p.pwd))
_, _ = h.Write(helper.StringToBytes(p.pswd))
return hex.EncodeToString(h.Sum(buf[:0]))
}
func (p *Portal) Login(u, domain, challenge string) error {
info := EncodeUserInfo(p.String(), challenge)
// Login sends login request to server
// input:
// server IP
// challenge
func (p *Portal) Login(sIP, challenge string) error {
userInfo, err := GetUserInfo(p.name, p.domain, p.pswd, p.ip, p.acid)
if err != nil {
return err
}
info := EncodeUserInfo(userInfo, challenge)
hmd5 := p.PasswordHMd5(challenge)
u = fmt.Sprintf(u, "gondportal", url.QueryEscape(p.nam), hmd5, p.ip, p.CheckSum(domain, challenge, hmd5, info), url.QueryEscape(info), time.Now().UnixMilli())
// Note: no need to do URL encoding here
u, err := GetLoginURL(
sIP,
"gondportal",
p.name,
p.domain,
hmd5,
p.acid,
p.ip,
p.CheckSum(challenge, p.name, p.domain, hmd5, p.acid, p.ip, info),
info,
time.Now().UnixMilli(),
)
if err != nil {
return err
}
logrus.Debugln("GET", u)
data, err := requestDataWith(u, "GET", PortalHeaderUA)
if err != nil {
@@ -98,7 +194,3 @@ func (p *Portal) Login(u, domain, challenge string) error {
}
return nil
}
func (p *Portal) String() string {
return fmt.Sprintf(PortalUserInfo, p.nam, p.pwd, p.ip)
}

View File

@@ -4,30 +4,181 @@ import (
"crypto/sha1"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"strings"
"github.com/google/go-querystring/query"
"github.com/fumiama/go-nd-portal/base64"
"github.com/fumiama/go-nd-portal/helper"
)
const (
PortalServerIP = "10.253.0.237"
PortalDomain = "@dx-uestc"
PortalDomainDX = "@dx"
PortalGetChallenge = "http://" + PortalServerIP + "/cgi-bin/get_challenge?callback=%s&username=%s" + PortalDomain + "&ip=%v&_=%d"
PortalGetChallengeDX = "http://" + PortalServerIP + "/cgi-bin/get_challenge?callback=%s&username=%s" + PortalDomainDX + "&ip=%v&_=%d"
PortalLogin = "http://" + PortalServerIP + "/cgi-bin/srun_portal?callback=%s&action=login&username=%s" + PortalDomain + "&password={MD5}%s&ac_id=1&ip=%v&chksum=%s&info={SRBX1}%s&n=200&type=1&os=Windows+10&name=Windows&double_stack=0&_=%d"
PortalLoginDX = "http://" + PortalServerIP + "/cgi-bin/srun_portal?callback=%s&action=login&username=%s" + PortalDomainDX + "&password={MD5}%s&ac_id=1&ip=%v&chksum=%s&info={SRBX1}%s&n=200&type=1&os=Windows+10&name=Windows&double_stack=0&_=%d"
// PortalServerIPQsh default Server IP String in Qsh work area
PortalServerIPQsh = "10.253.0.237"
// PortalServerIPQshDorm default Server IP String in Qsh new dorm area
PortalServerIPQshDorm = "10.253.0.235"
// PortalDomainQsh PortalDomain for qsh-edu login type
PortalDomainQsh = "@dx-uestc"
// PortalDomainQshDX PortalDomain for qsh-dx, qshd-dx login types
PortalDomainQshDX = "@dx"
// PortalDomainQshCMCC PortalDomain for qshd-cmcc login type
PortalDomainQshCMCC = "@cmcc"
// PortalGetChallenge GetChallenge URL
PortalGetChallenge = "http://%v/cgi-bin/get_challenge?%s"
// 1.server IP
// 2.callback
// 3.username 4.PortalDomain
// 5.client IP
// 6.timestamp
// PortalGetChallenge = "http://%v/cgi-bin/get_challenge?callback=%s&username=%s%s&ip=%v&_=%d"
// AcIDQsh ACID for Qsh work area
AcIDQsh = "1"
// AcIDQshDorm ACID for Qsh new dorm area
AcIDQshDorm = "3"
// PortalCGI Auth CGI URL
PortalCGI = "http://%v/cgi-bin/srun_portal?%s"
// qsh LoginURL key-value order
// 1.server IP
// 2.callback
// 3.username 4.PortalDomain
// 5.encrypted password
// 6.ac_id: determined by login area
// 7.client IP
// 8.checksum
// 9.info
// 10.timestamp
// PortalLogin = "http://%v/cgi-bin/srun_portal?callback=%s&action=login&username=%s%s&password={MD5}%s&ac_id=%s&ip=%v&chksum=%s&info={SRBX1}%s&n=200&type=1&os=Windows+10&name=Windows&double_stack=0&_=%d"
)
// GetChallengeReq struct for GetChallenge URL query
type GetChallengeReq struct {
Callback string `url:"callback"`
Username string `url:"username"`
IP string `url:"ip"`
Timestamp int64 `url:"_"`
}
// GetPortalReq struct for Portal Auth CGI URL query
type GetPortalReq struct {
Callback string `url:"callback"`
Action string `url:"action"`
Username string `url:"username"`
EncryptedPassword string `url:"password"`
AcID string `url:"ac_id"`
IP string `url:"ip"`
Checksum string `url:"chksum"`
EncodedUserInfo string `url:"info"`
ConstantN string `url:"n"`
ConstantType string `url:"type"`
OS string `url:"os"`
Platform string `url:"name"`
DoubleStack string `url:"double_stack"`
Timestamp int64 `url:"_"`
}
// GetChallengeURL generates the URL for getchallenge req
func GetChallengeURL(
sIP,
callback,
username, domain string,
cIP net.IP,
timestamp int64) (string, error) {
v, err := query.Values(&GetChallengeReq{
Callback: callback,
Username: username + domain,
IP: cIP.String(),
Timestamp: timestamp,
})
if err != nil {
return "", err
}
return fmt.Sprintf(PortalGetChallenge, sIP, v.Encode()), nil
}
// GetLoginURL generates the URL for login req
func GetLoginURL(
sIP,
callback,
username, domain,
md5Password,
acid string,
cIP net.IP,
chksum,
info string,
timestamp int64) (string, error) {
v, err := query.Values(&GetPortalReq{
Callback: callback,
Action: "login",
Username: username + domain,
EncryptedPassword: "{MD5}" + md5Password,
AcID: acid,
IP: cIP.String(),
Checksum: chksum,
EncodedUserInfo: "{SRBX1}" + info,
ConstantN: "200",
ConstantType: "1",
OS: "Windows 10",
Platform: "Windows",
DoubleStack: "0",
Timestamp: timestamp,
})
if err != nil {
return "", err
}
return fmt.Sprintf(PortalCGI, sIP, v.Encode()), nil
}
const (
// PortalHeaderUA fake User-Agent
PortalHeaderUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.56"
)
const (
PortalUserInfo = `{"username":"%s` + PortalDomain + `","password":"%s","ip":"%v","acid":"1","enc_ver":"srun_bx1"}`
PortalUserInfoDX = `{"username":"%s` + PortalDomainDX + `","password":"%s","ip":"%v","acid":"1","enc_ver":"srun_bx1"}`
)
// UserInfo struct for userinfo JSON required by server
type UserInfo struct {
Username string `json:"username"` // = username + domain
Password string `json:"password"`
IP string `json:"ip"`
AcID string `json:"acid"`
EncVer string `json:"enc_ver"`
}
// GetUserInfo serializes UserInfo JSON to string
func GetUserInfo(
username,
domain,
password string,
cIP net.IP,
acid string) (string, error) {
var b strings.Builder
err := json.NewEncoder(&b).Encode(&UserInfo{
Username: username + domain,
Password: password,
IP: cIP.String(),
AcID: acid,
EncVer: "srun_bx1",
})
if err != nil {
return "", err
}
// Note: in case of unexpected error
// we have to remove "\n" at the tail to match actual JSON format
return strings.TrimSpace(b.String()), nil
}
// EncodeUserInfo encodes userinfo with challenge
func EncodeUserInfo(info, challenge string) string {
if len(info) == 0 || len(challenge) == 0 || len(challenge)%4 != 0 {
return ""
@@ -80,18 +231,27 @@ func EncodeUserInfo(info, challenge string) string {
return base64.Base64Encoding.EncodeToString(lv)
}
func (p *Portal) CheckSum(domain, challenge, hmd5, info string) string {
// CheckSum calculates chksum parameter for login
func (p *Portal) CheckSum(
challenge,
username,
domain,
hmd5,
acid string,
cIP net.IP,
info string) string {
var buf [20]byte
h := sha1.New()
_, _ = h.Write(helper.StringToBytes(challenge))
_, _ = h.Write(helper.StringToBytes(p.nam))
_, _ = h.Write(helper.StringToBytes(username))
_, _ = h.Write([]byte(domain))
_, _ = h.Write(helper.StringToBytes(challenge))
_, _ = h.Write(helper.StringToBytes(hmd5))
_, _ = h.Write(helper.StringToBytes(challenge))
_, _ = h.Write([]byte("1")) // ac_id
_, _ = h.Write([]byte(acid)) // acid
_, _ = h.Write(helper.StringToBytes(challenge))
_, _ = h.Write(helper.StringToBytes(p.ip.String()))
_, _ = h.Write(helper.StringToBytes(cIP.String()))
_, _ = h.Write(helper.StringToBytes(challenge))
_, _ = h.Write([]byte("200")) // n
_, _ = h.Write(helper.StringToBytes(challenge))

View File

@@ -12,6 +12,19 @@ import (
"github.com/fumiama/go-nd-portal/helper"
)
func TestGetUserInfo(t *testing.T) {
u, err := NewPortal("2000010101001", "12345678", net.IPv4(1, 2, 3, 4).To4(),"qsh-edu")
if err != nil {
t.Fatal(err)
}
info, err := GetUserInfo(u.name, u.domain, u.pswd, u.ip, u.acid)
if err != nil {
t.Fatal(err)
}
t.Log(info)
assert.Equal(t, `{"username":"2000010101001@dx-uestc","password":"12345678","ip":"1.2.3.4","acid":"1","enc_ver":"srun_bx1"}`, info)
}
func TestDecodeInfo(t *testing.T) {
info := `{"username":"2000010101001@dx-uestc","password":"12345678","ip":"1.2.3.4","acid":"1","enc_ver":"srun_bx1"}`
sc := len(info)
@@ -40,17 +53,21 @@ func TestDecodeKey(t *testing.T) {
}
func TestEncodeUserInfo(t *testing.T) {
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4())
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4(),"qsh-edu")
if err != nil {
t.Fatal(err)
}
t.Log(u.String())
r := EncodeUserInfo(u.String(), "d26466d4036507dadb17e87e23358126e0210cb289d19151f59bcfcefdcf345e")
info, err := GetUserInfo(u.name, u.domain, u.pswd, u.ip, u.acid)
if err != nil {
t.Fatal(err)
}
t.Log(info)
r := EncodeUserInfo(info, "d26466d4036507dadb17e87e23358126e0210cb289d19151f59bcfcefdcf345e")
assert.Equal(t, "CfVnZ9mvKmdgvm/ivovlPibZL6RLAWcx+nBTaYmWH3kmThco+eO4LVsCPFceSmM9PyI0UcMgLE7bmpfY9pr0EWnWdTncXrbW29Aydp+lw6QjxKMgNzgYd7uopiPbIyKpxvJZDHsGw5xh8rMEeq3JXrD2vex27xeI", r)
}
func TestHMd5(t *testing.T) {
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4())
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4(),"qsh-edu")
if err != nil {
t.Fatal(err)
}
@@ -64,18 +81,25 @@ func TestSha1(t *testing.T) {
}
func TestCheckSum(t *testing.T) {
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4())
u, err := NewPortal("2001010101001", "1234567890", net.IPv4(113, 54, 148, 243).To4(),"qsh-edu")
if err != nil {
t.Fatal(err)
}
t.Log(u.String())
info, err := GetUserInfo(u.name, u.domain, u.pswd, u.ip, u.acid)
if err != nil {
t.Fatal(err)
}
t.Log(info)
challenge := "d26466d4036507dadb17e87e23358126e0210cb289d19151f59bcfcefdcf345e"
s := u.CheckSum(
PortalDomain,
challenge,
u.name,
PortalDomainQsh,
u.PasswordHMd5(challenge),
u.acid,
u.ip,
EncodeUserInfo(
u.String(),
info,
challenge,
),
)