1
0
mirror of https://github.com/fumiama/go-nd-portal.git synced 2026-06-05 00:10:25 +08:00
Files
go-nd-portal/portal/portal.go
chasey-dev 4d9b9e1886 feat: support sh campus login (#6)
- added login type: sh-edu, sh-dx, sh-cmcc
2025-09-03 12:55:51 +08:00

272 lines
6.8 KiB
Go

// Package portal handles login process
package portal
import (
"crypto/hmac"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"net"
"net/netip"
"time"
"github.com/sirupsen/logrus"
"github.com/fumiama/go-nd-portal/helper"
)
var (
// 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")
// ErrCannotDetermineClientIP is returned when client IP cant get from challenge or local resolution with cip not specified
ErrCannotDetermineClientIP = errors.New("failed to determine client IP from challenge response or local resolution")
// ErrUnexpectedLoginResponse is returned when login resp is shorter than expected
ErrUnexpectedLoginResponse = errors.New("unexpected login response")
)
// Portal struct for login config
type Portal struct {
name string
pswd string
cip string
sip string
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"
// LoginTypeShEdu edu in Sh
LoginTypeShEdu LoginType = "sh-edu"
// LoginTypeShDX dx in Sh
LoginTypeShDX LoginType = "sh-dx"
// LoginTypeShCMCC cmcc in Sh
LoginTypeShCMCC LoginType = "sh-cmcc"
)
// GetDefaultPortalServerIP returns default PortalServerIP by LoginType
func (lt LoginType) GetDefaultPortalServerIP() (string, error) {
var sIP string
switch lt {
// Qsh work area
case LoginTypeQshEdu, LoginTypeQshDX:
sIP = PortalServerIPQsh
// Qsh new dorm area
case LoginTypeQshDormDX, LoginTypeQshDormCMCC:
sIP = PortalServerIPQshDorm
// Sh
case LoginTypeShEdu, LoginTypeShDX, LoginTypeShCMCC:
sIP = PortalServerIPSh
default:
return "", ErrIllegalLoginType
}
return sIP, nil
}
// ToDomainAcID converts LoginType to domain and acid
func (lt LoginType) ToDomainAcID() (string, string, error) {
var domain, acid string
switch lt {
// Qsh work area
case LoginTypeQshEdu:
// qsh-edu is assumed that cant login from dorm
domain = PortalDomainQsh
acid = AcIDQsh
case LoginTypeQshDX:
domain = PortalDomainQshDX
acid = AcIDQsh
// Qsh new dorm area
case LoginTypeQshDormDX:
domain = PortalDomainQshDX
acid = AcIDQshDorm
case LoginTypeQshDormCMCC:
domain = PortalDomainQshCMCC
acid = AcIDQshDorm
// Sh
case LoginTypeShEdu:
domain = PortalDomainSh
acid = AcIDSh
case LoginTypeShDX:
domain = PortalDomainShDX
acid = AcIDSh
case LoginTypeShCMCC:
domain = PortalDomainShCMCC
acid = AcIDSh
default:
return "", "", ErrIllegalLoginType
}
return domain, acid, nil
}
// ResolveLocalClientIP resolves Client IP locally
func ResolveLocalClientIP() (string, error) {
conn, err := net.Dial("udp", "8.8.8.8:53")
if err != nil {
return "", err
}
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP.String(), nil
}
// rsp struct for converting from raw response data to JSON
type rsp struct {
ClientIP string `json:"client_ip"`
Challenge string `json:"challenge"`
Error string `json:"error"`
}
// NewPortal creates a new Portal instance
func NewPortal(name, password, sIP string, cIP string, loginType LoginType) (*Portal, error) {
domain, acid, err := loginType.ToDomainAcID()
if err != nil {
return nil, err
}
logrus.Debugf("login type: %s, portal domain: %s, ac_id: %s", loginType, domain, acid)
if sIP == "" {
sIP, err = loginType.GetDefaultPortalServerIP()
if err != nil {
return nil, err
}
}
logrus.Debugf("server addr: %s", sIP)
return &Portal{
name: name,
pswd: password,
cip: cIP,
sip: sIP,
domain: domain,
acid: acid,
}, nil
}
// GetChallenge gets token for encryption from server
func (p *Portal) GetChallenge() (string, error) {
// Note: no need to do URL encoding here
u, err := GetChallengeURL(
p.sip,
"gondportal",
p.name,
p.domain,
p.cip,
time.Now().UnixMilli(),
)
if err != nil {
return "", err
}
logrus.Debugln("GET", u)
data, err := requestDataWith(u, "GET", PortalHeaderUA)
if err != nil {
return "", err
}
logrus.Debugln("get challenge resp:", helper.BytesToString(data))
if len(data) < 12 {
return "", ErrUnexpectedChallengeResponse
}
var r rsp
err = json.Unmarshal(data[11:len(data)-1], &r)
if err != nil {
return "", err
}
if r.Error != "ok" {
return "", errors.New(r.Error)
}
// if cip was left empty, try get from challenge resp
if p.cip == "" {
logrus.Debugln("client ip is not specified, try get client ip from challenge resp")
_, err = netip.ParseAddr(r.ClientIP)
if err == nil {
p.cip = r.ClientIP
logrus.Debugln("get client ip from challenge resp:", r.ClientIP)
} else {
// if ClientIP is invalid, try resolve it locally
p.cip, err = ResolveLocalClientIP()
if err != nil {
return "", ErrCannotDetermineClientIP
}
logrus.Debugln("failed to get client ip from challenge resp, using locally resolved ip:", p.cip)
}
}
logrus.Debugln("get challenge:", r.Challenge)
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.pswd))
return hex.EncodeToString(h.Sum(buf[:0]))
}
// Login sends login request to server
// input:
// challenge
func (p *Portal) Login(challenge string) error {
userInfo, err := GetUserInfo(p.name, p.domain, p.pswd, p.cip, p.acid)
if err != nil {
return err
}
info := EncodeUserInfo(userInfo, challenge)
hmd5 := p.PasswordHMd5(challenge)
// Note: no need to do URL encoding here
u, err := GetLoginURL(
p.sip,
"gondportal",
p.name,
p.domain,
hmd5,
p.acid,
p.cip,
p.CheckSum(challenge, p.name, p.domain, hmd5, p.acid, p.cip, info),
info,
time.Now().UnixMilli(),
)
if err != nil {
return err
}
logrus.Debugln("GET", u)
data, err := requestDataWith(u, "GET", PortalHeaderUA)
if err != nil {
return err
}
logrus.Debugln("get login resp:", helper.BytesToString(data))
if len(data) < 12 {
return ErrUnexpectedLoginResponse
}
var r rsp
err = json.Unmarshal(data[11:len(data)-1], &r)
if err != nil {
return err
}
logrus.Debugln("login rsp:", &r)
// compare local cip with response client_ip
if p.cip != r.ClientIP {
logrus.Warnln("client ip in login request does not match response! unexpected errors may occur")
logrus.Warnf("request: %s, response: %s", p.cip, r.ClientIP)
}
if r.Error != "ok" {
return errors.New(r.Error)
}
return nil
}