You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

368 lines
7.7 KiB

package m9zTtyPwd
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
g "tgk-touch/internal/global"
"time"
"github.com/towgo/towgo/lib/system"
"github.com/towgo/towgo/towgo"
)
const (
DefaultPassword = "123456"
DefaultSalt = "m9zTty"
SessionExpireMinutes = 10
)
var (
manager *PasswordManager = &PasswordManager{
verifiedSessions: make(map[string]time.Time),
}
)
var noInterceptor bool
var (
whiteListMethods = map[string]bool{
"/m9z/password/verify": true,
"/m9z/password/change": true,
// 实时监控
"/m9z/touchdisplay/getDeviceID": true,
"/m9z/getDeviceStatus2": true,
"/getMessageInterval": true,
"/adl400/readPhaseData": true,
// 报警预览
"/m9z/fault/list": true,
}
whiteListPrefixes = []string{
//"/m9z/get",
//"/m9z/system/get",
//"/m9z/fault",
}
)
type PasswordManager struct {
mu sync.RWMutex
hashedPwd string
salt string
verifiedSessions map[string]time.Time
}
type PasswordData struct {
HashedPassword string `json:"hashed_password"`
Salt string `json:"salt"`
}
func getPwdFilePath() string {
return filepath.Join(system.GetPathOfProgram(), "data", "m9zTtyPwd.json")
}
func hashPassword(pwd, salt string) string {
data := []byte(pwd + salt)
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func (m *PasswordManager) loadFromFile() error {
m.mu.Lock()
defer m.mu.Unlock()
filePath := getPwdFilePath()
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
m.hashedPwd = hashPassword(DefaultPassword, DefaultSalt)
m.salt = DefaultSalt
return m.saveToFile()
}
return err
}
var pwdData PasswordData
if err := json.Unmarshal(data, &pwdData); err != nil {
return err
}
m.hashedPwd = pwdData.HashedPassword
m.salt = pwdData.Salt
return nil
}
func (m *PasswordManager) saveToFile() error {
filePath := getPwdFilePath()
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
pwdData := PasswordData{
HashedPassword: m.hashedPwd,
Salt: m.salt,
}
data, err := json.Marshal(pwdData)
if err != nil {
return err
}
return os.WriteFile(filePath, data, 0600)
}
func (m *PasswordManager) verify(pwd string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.hashedPwd == hashPassword(pwd, m.salt)
}
func (m *PasswordManager) change(newPwd string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.hashedPwd = hashPassword(newPwd, m.salt)
return m.saveToFile()
}
func (m *PasswordManager) addVerifiedSession(sessionId string) {
m.mu.Lock()
defer m.mu.Unlock()
m.verifiedSessions[sessionId] = time.Now().Add(SessionExpireMinutes * time.Minute)
}
func (m *PasswordManager) isSessionVerified(sessionId string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
expireTime, ok := m.verifiedSessions[sessionId]
if !ok {
return false
}
if time.Now().After(expireTime) {
return false
}
return true
}
func (m *PasswordManager) removeSession(sessionId string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.verifiedSessions, sessionId)
}
func (m *PasswordManager) cleanupExpiredSessions() {
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now()
for sessionId, expireTime := range m.verifiedSessions {
if now.After(expireTime) {
delete(m.verifiedSessions, sessionId)
}
}
}
func (m *PasswordManager) isWhiteListed(method string) bool {
if whiteListMethods[method] {
return true
}
for _, prefix := range whiteListPrefixes {
if strings.HasPrefix(method, prefix) {
return true
}
}
return false
}
func NoInterceptor(b bool) {
noInterceptor = b
}
func UnauthorizedMethodAdd(method string) {
whiteListMethods[method] = true
}
func UnauthorizedMethodPrefixAdd(prefix string) {
whiteListPrefixes = append(whiteListPrefixes, prefix)
}
var mu sync.Mutex
func Init() {
mu.Lock()
defer mu.Unlock()
if err := manager.loadFromFile(); err != nil {
g.GVA_LOG.Sugar().Warnf("load password data failed: %v, using default", err)
manager.hashedPwd = hashPassword(DefaultPassword, DefaultSalt)
manager.salt = DefaultSalt
manager.saveToFile()
}
towgo.SetFunc("/m9z/password/verify", verifyPwd)
towgo.SetFunc("/m9z/password/change", changePwd)
towgo.SetFunc("/m9z/system/getTime", getSystemTime)
towgo.SetFunc("/m9z/system/setTime", setSystemTime)
towgo.AddInterceptor(pwdInterceptor)
go func() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
manager.cleanupExpiredSessions()
}
}()
}
func generateSessionId() string {
data := []byte(time.Now().String())
sum := md5.Sum(data)
return hex.EncodeToString(sum[:])
}
func verifyPwd(rpcConn towgo.JsonRpcConnection) {
var req struct {
Password string `json:"password"`
}
rpcConn.ReadParams(&req)
sessionId := generateSessionId()
if manager.verify(req.Password) {
manager.addVerifiedSession(sessionId)
rpcConn.WriteResult(map[string]interface{}{
"success": true,
"session_id": sessionId,
"message": "verified",
})
} else {
rpcConn.WriteError(401, "invalid password")
}
}
type ChangePwdReq struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
func changePwd(rpcConn towgo.JsonRpcConnection) {
var req ChangePwdReq
rpcConn.ReadParams(&req)
if !manager.verify(req.OldPassword) {
rpcConn.WriteError(500, "旧密码错误")
return
}
if len(req.NewPassword) < 6 {
rpcConn.WriteError(500, "新密码长度小于 6")
return
}
if err := manager.change(req.NewPassword); err != nil {
rpcConn.WriteError(500, "change password failed: "+err.Error())
return
}
rpcConn.WriteResult(map[string]interface{}{
"success": true,
"message": "password changed",
})
}
func pwdInterceptor(conn towgo.JsonRpcConnection) error {
if noInterceptor {
return nil
}
rpcRequest := conn.GetRpcRequest()
rpcResponse := conn.GetRpcResponse()
if manager.isWhiteListed(rpcRequest.Method) {
return nil
}
sessionId := rpcRequest.Session
if sessionId == "" {
rpcResponse.Error.Set(401, "password verification required")
conn.Write()
return errors.New("password verification required")
}
if !manager.isSessionVerified(sessionId) {
rpcResponse.Error.Set(401, "session expired or invalid")
conn.Write()
return errors.New("password verification required")
}
return nil
}
func getSystemTime(rpcConn towgo.JsonRpcConnection) {
now := time.Now()
rpcConn.WriteResult(map[string]interface{}{
"timestamp": now.Unix(),
"datetime": now.Format("2006-01-02 15:04:05"),
"timezone": now.Location().String(),
})
}
type SetTimeReq struct {
Datetime string `json:"datetime"`
}
func setSystemTime(rpcConn towgo.JsonRpcConnection) {
var req SetTimeReq
rpcConn.ReadParams(&req)
if req.Datetime == "" {
rpcConn.WriteError(400, "datetime is required")
return
}
layouts := []string{
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04",
"2006-01-02",
}
var targetTime time.Time
var parseErr error
for _, layout := range layouts {
targetTime, parseErr = time.Parse(layout, req.Datetime)
if parseErr == nil {
break
}
}
if parseErr != nil {
rpcConn.WriteError(400, "invalid datetime format, supported: 2006-01-02 15:04:05, 2006-01-02T15:04:05Z, 2006-01-02 15:04, 2006-01-02")
return
}
unixTime := targetTime.Unix()
cmd := exec.Command("date", "-s", targetTime.Format("2006-01-02 15:04:05"))
cmd.Run()
cmd = exec.Command("hwclock", "-w")
cmd.Run()
cmd = exec.Command("timedatectl", "set-ntp", "false")
cmd.Run()
rpcConn.WriteResult(map[string]interface{}{
"success": true,
"message": "system time updated",
"old_time": time.Now().Format("2006-01-02 15:04:05"),
"new_time": targetTime.Format("2006-01-02 15:04:05"),
"unix_time": unixTime,
})
}