diff --git a/base64/base64.go b/base64/base64.go index b566992..58c55d2 100644 --- a/base64/base64.go +++ b/base64/base64.go @@ -1,4 +1,4 @@ -// Package base64 with customized talbe +// Package base64 with customized table package base64 import b64 "encoding/base64" diff --git a/cmd/main.go b/cmd/main.go index addd8f2..8231d3c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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()) diff --git a/go.mod b/go.mod index da21549..4d425fe 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f354ed2..14ab261 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/portal/portal.go b/portal/portal.go index 906f058..e327a2f 100644 --- a/portal/portal.go +++ b/portal/portal.go @@ -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) -} diff --git a/portal/server.go b/portal/server.go index 63bb814..3e50854 100644 --- a/portal/server.go +++ b/portal/server.go @@ -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)) diff --git a/portal/server_test.go b/portal/server_test.go index b0567b3..147e415 100644 --- a/portal/server_test.go +++ b/portal/server_test.go @@ -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, ), )