style(be):拆分模块目录
This commit is contained in:
89
pkg/config/config.go
Normal file
89
pkg/config/config.go
Normal file
@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Demo bool
|
||||
DB string
|
||||
Server *Server
|
||||
Mysql *Mysql
|
||||
Sqlite *Sqlite
|
||||
ResetPassword string
|
||||
}
|
||||
|
||||
type Mysql struct {
|
||||
Hostname string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Database string
|
||||
}
|
||||
|
||||
type Sqlite struct {
|
||||
File string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Addr string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
|
||||
func SetupConfig() *Config {
|
||||
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yml")
|
||||
viper.AddConfigPath("/etc/next-terminal/")
|
||||
viper.AddConfigPath("$HOME/.next-terminal")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
pflag.String("db", "sqlite", "db mode")
|
||||
pflag.String("sqlite.file", "next-terminal.db", "sqlite db file")
|
||||
pflag.String("mysql.hostname", "127.0.0.1", "mysql hostname")
|
||||
pflag.Int("mysql.port", 3306, "mysql port")
|
||||
pflag.String("mysql.username", "mysql", "mysql username")
|
||||
pflag.String("mysql.password", "mysql", "mysql password")
|
||||
pflag.String("mysql.database", "next_terminal", "mysql database")
|
||||
|
||||
pflag.String("server.addr", "", "server listen addr")
|
||||
pflag.String("server.cert", "", "tls cert file")
|
||||
pflag.String("server.key", "", "tls key file")
|
||||
pflag.String("reset-password", "", "")
|
||||
|
||||
pflag.Parse()
|
||||
_ = viper.BindPFlags(pflag.CommandLine)
|
||||
_ = viper.ReadInConfig()
|
||||
|
||||
var config = &Config{
|
||||
DB: viper.GetString("db"),
|
||||
Mysql: &Mysql{
|
||||
Hostname: viper.GetString("mysql.hostname"),
|
||||
Port: viper.GetInt("mysql.port"),
|
||||
Username: viper.GetString("mysql.username"),
|
||||
Password: viper.GetString("mysql.password"),
|
||||
Database: viper.GetString("mysql.database"),
|
||||
},
|
||||
Sqlite: &Sqlite{
|
||||
File: viper.GetString("sqlite.file"),
|
||||
},
|
||||
Server: &Server{
|
||||
Addr: viper.GetString("server.addr"),
|
||||
Cert: viper.GetString("server.cert"),
|
||||
Key: viper.GetString("server.key"),
|
||||
},
|
||||
ResetPassword: viper.GetString("reset-password"),
|
||||
Debug: viper.GetBool("debug"),
|
||||
Demo: viper.GetBool("demo"),
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
41
pkg/constant/const.go
Normal file
41
pkg/constant/const.go
Normal file
@ -0,0 +1,41 @@
|
||||
package constant
|
||||
|
||||
import "next-terminal/pkg/guacd"
|
||||
|
||||
const (
|
||||
AccessRuleAllow = "allow" // 允许访问
|
||||
AccessRuleReject = "reject" // 拒绝访问
|
||||
|
||||
Custom = "custom" // 密码
|
||||
PrivateKey = "private-key" // 密钥
|
||||
|
||||
JobStatusRunning = "running" // 计划任务运行状态
|
||||
JobStatusNotRunning = "not-running" // 计划任务未运行状态
|
||||
FuncCheckAssetStatusJob = "check-asset-status-job" // 检测资产是否在线
|
||||
FuncShellJob = "shell-job" // 执行Shell脚本
|
||||
JobModeAll = "all" // 全部资产
|
||||
JobModeCustom = "custom" // 自定义选择资产
|
||||
|
||||
SshMode = "ssh-mode" // ssh模式
|
||||
MailHost = "mail-host" // 邮件服务器地址
|
||||
MailPort = "mail-port" // 邮件服务器端口
|
||||
MailUsername = "mail-username" // 邮件服务账号
|
||||
MailPassword = "mail-password" // 邮件服务密码
|
||||
|
||||
NoConnect = "no_connect" // 会话状态:未连接
|
||||
Connecting = "connecting" // 会话状态:连接中
|
||||
Connected = "connected" // 会话状态:已连接
|
||||
Disconnected = "disconnected" // 会话状态:已断开连接
|
||||
|
||||
Guacd = "guacd" // 接入模式:guacd
|
||||
Naive = "naive" // 接入模式:原生
|
||||
|
||||
TypeUser = "user" // 普通用户
|
||||
TypeAdmin = "admin" // 管理员
|
||||
)
|
||||
|
||||
var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode}
|
||||
var RDPParameterNames = []string{guacd.Domain, guacd.RemoteApp, guacd.RemoteAppDir, guacd.RemoteAppArgs}
|
||||
var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort}
|
||||
var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex}
|
||||
var KubernetesParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.Namespace, guacd.Pod, guacd.Container, guacd.UesSSL, guacd.ClientCert, guacd.ClientKey, guacd.CaCert, guacd.IgnoreCert}
|
23
pkg/global/global.go
Normal file
23
pkg/global/global.go
Normal file
@ -0,0 +1,23 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/config"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var Cache *cache.Cache
|
||||
|
||||
var Config *config.Config
|
||||
|
||||
var Store *TunStore
|
||||
|
||||
var Cron *cron.Cron
|
||||
|
||||
type Security struct {
|
||||
Rule string
|
||||
IP string
|
||||
}
|
||||
|
||||
var Securities []*Security
|
71
pkg/global/store.go
Normal file
71
pkg/global/store.go
Normal file
@ -0,0 +1,71 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/pkg/term"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Tun struct {
|
||||
Protocol string
|
||||
Mode string
|
||||
WebSocket *websocket.Conn
|
||||
Tunnel *guacd.Tunnel
|
||||
NextTerminal *term.NextTerminal
|
||||
}
|
||||
|
||||
func (r *Tun) Close(code int, reason string) {
|
||||
if r.Tunnel != nil {
|
||||
_ = r.Tunnel.Close()
|
||||
}
|
||||
if r.NextTerminal != nil {
|
||||
_ = r.NextTerminal.Close()
|
||||
}
|
||||
|
||||
ws := r.WebSocket
|
||||
if ws != nil {
|
||||
if r.Mode == "guacd" {
|
||||
err := guacd.NewInstruction("error", "", strconv.Itoa(code))
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
|
||||
disconnect := guacd.NewInstruction("disconnect")
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
|
||||
} else {
|
||||
msg := `{"type":"closed","content":"` + reason + `"}`
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Observable struct {
|
||||
Subject *Tun
|
||||
Observers []Tun
|
||||
}
|
||||
|
||||
type TunStore struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
func (s *TunStore) Set(k string, v *Observable) {
|
||||
s.m.Store(k, v)
|
||||
}
|
||||
|
||||
func (s *TunStore) Del(k string) {
|
||||
s.m.Delete(k)
|
||||
}
|
||||
|
||||
func (s *TunStore) Get(k string) (item *Observable, ok bool) {
|
||||
value, ok := s.m.Load(k)
|
||||
if ok {
|
||||
return value.(*Observable), true
|
||||
}
|
||||
return item, false
|
||||
}
|
||||
|
||||
func NewStore() *TunStore {
|
||||
store := TunStore{sync.Map{}}
|
||||
return &store
|
||||
}
|
320
pkg/service/job.go
Normal file
320
pkg/service/job.go
Normal file
@ -0,0 +1,320 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/pkg/term"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type JobService struct {
|
||||
jobRepository *repository.JobRepository
|
||||
jobLogRepository *repository.JobLogRepository
|
||||
assetRepository *repository.AssetRepository
|
||||
credentialRepository *repository.CredentialRepository
|
||||
}
|
||||
|
||||
func NewJobService(jobRepository *repository.JobRepository, jobLogRepository *repository.JobLogRepository, assetRepository *repository.AssetRepository, credentialRepository *repository.CredentialRepository) *JobService {
|
||||
return &JobService{jobRepository: jobRepository, jobLogRepository: jobLogRepository, assetRepository: assetRepository, credentialRepository: credentialRepository}
|
||||
}
|
||||
|
||||
func (r JobService) ChangeStatusById(id, status string) error {
|
||||
job, err := r.jobRepository.FindById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == constant.JobStatusRunning {
|
||||
j, err := getJob(&job, &r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entryID, err := global.Cron.AddJob(job.Cron, j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("开启计划任务「%v」,运行中计划任务数量「%v」", job.Name, len(global.Cron.Entries()))
|
||||
|
||||
jobForUpdate := model.Job{ID: id, Status: constant.JobStatusRunning, CronJobId: int(entryID)}
|
||||
|
||||
return r.jobRepository.UpdateById(&jobForUpdate)
|
||||
} else {
|
||||
global.Cron.Remove(cron.EntryID(job.CronJobId))
|
||||
log.Debugf("关闭计划任务「%v」,运行中计划任务数量「%v」", job.Name, len(global.Cron.Entries()))
|
||||
jobForUpdate := model.Job{ID: id, Status: constant.JobStatusNotRunning}
|
||||
return r.jobRepository.UpdateById(&jobForUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
func getJob(j *model.Job, jobService *JobService) (job cron.Job, err error) {
|
||||
switch j.Func {
|
||||
case constant.FuncCheckAssetStatusJob:
|
||||
job = CheckAssetStatusJob{ID: j.ID, Mode: j.Mode, ResourceIds: j.ResourceIds, Metadata: j.Metadata, jobService: jobService}
|
||||
case constant.FuncShellJob:
|
||||
job = ShellJob{ID: j.ID, Mode: j.Mode, ResourceIds: j.ResourceIds, Metadata: j.Metadata, jobService: jobService}
|
||||
default:
|
||||
return nil, errors.New("未识别的任务")
|
||||
}
|
||||
return job, err
|
||||
}
|
||||
|
||||
type CheckAssetStatusJob struct {
|
||||
ID string
|
||||
Mode string
|
||||
ResourceIds string
|
||||
Metadata string
|
||||
jobService *JobService
|
||||
}
|
||||
|
||||
func (r CheckAssetStatusJob) Run() {
|
||||
if r.ID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var assets []model.Asset
|
||||
if r.Mode == constant.JobModeAll {
|
||||
assets, _ = r.jobService.assetRepository.FindAll()
|
||||
} else {
|
||||
assets, _ = r.jobService.assetRepository.FindByIds(strings.Split(r.ResourceIds, ","))
|
||||
}
|
||||
|
||||
if len(assets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
msgChan := make(chan string)
|
||||
for i := range assets {
|
||||
asset := assets[i]
|
||||
go func() {
|
||||
t1 := time.Now()
|
||||
active := utils.Tcping(asset.IP, asset.Port)
|
||||
elapsed := time.Since(t1)
|
||||
msg := fmt.Sprintf("资产「%v」存活状态检测完成,存活「%v」,耗时「%v」", asset.Name, active, elapsed)
|
||||
|
||||
_ = r.jobService.assetRepository.UpdateActiveById(active, asset.ID)
|
||||
log.Infof(msg)
|
||||
msgChan <- msg
|
||||
}()
|
||||
}
|
||||
|
||||
var message = ""
|
||||
for i := 0; i < len(assets); i++ {
|
||||
message += <-msgChan + "\n"
|
||||
}
|
||||
|
||||
_ = r.jobService.jobRepository.UpdateLastUpdatedById(r.ID)
|
||||
jobLog := model.JobLog{
|
||||
ID: utils.UUID(),
|
||||
JobId: r.ID,
|
||||
Timestamp: utils.NowJsonTime(),
|
||||
Message: message,
|
||||
}
|
||||
|
||||
_ = r.jobService.jobLogRepository.Create(&jobLog)
|
||||
}
|
||||
|
||||
type ShellJob struct {
|
||||
ID string
|
||||
Mode string
|
||||
ResourceIds string
|
||||
Metadata string
|
||||
jobService *JobService
|
||||
}
|
||||
|
||||
type MetadataShell struct {
|
||||
Shell string
|
||||
}
|
||||
|
||||
func (r ShellJob) Run() {
|
||||
if r.ID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var assets []model.Asset
|
||||
if r.Mode == constant.JobModeAll {
|
||||
assets, _ = r.jobService.assetRepository.FindByProtocol("ssh")
|
||||
} else {
|
||||
assets, _ = r.jobService.assetRepository.FindByProtocolAndIds("ssh", strings.Split(r.ResourceIds, ","))
|
||||
}
|
||||
|
||||
if len(assets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataShell MetadataShell
|
||||
err := json.Unmarshal([]byte(r.Metadata), &metadataShell)
|
||||
if err != nil {
|
||||
log.Errorf("JSON数据解析失败 %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
msgChan := make(chan string)
|
||||
for i := range assets {
|
||||
asset, err := r.jobService.assetRepository.FindById(assets[i].ID)
|
||||
if err != nil {
|
||||
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询数据异常「%v」", assets[i].Name, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
username = asset.Username
|
||||
password = asset.Password
|
||||
privateKey = asset.PrivateKey
|
||||
passphrase = asset.Passphrase
|
||||
ip = asset.IP
|
||||
port = asset.Port
|
||||
)
|
||||
|
||||
if asset.AccountType == "credential" {
|
||||
credential, err := r.jobService.credentialRepository.FindById(asset.CredentialId)
|
||||
if err != nil {
|
||||
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询授权凭证数据异常「%v」", assets[i].Name, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if credential.Type == constant.Custom {
|
||||
username = credential.Username
|
||||
password = credential.Password
|
||||
} else {
|
||||
username = credential.Username
|
||||
privateKey = credential.PrivateKey
|
||||
passphrase = credential.Passphrase
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
||||
t1 := time.Now()
|
||||
result, err := ExecCommandBySSH(metadataShell.Shell, ip, port, username, password, privateKey, passphrase)
|
||||
elapsed := time.Since(t1)
|
||||
var msg string
|
||||
if err != nil {
|
||||
msg = fmt.Sprintf("资产「%v」Shell执行失败,返回值「%v」,耗时「%v」", asset.Name, err.Error(), elapsed)
|
||||
log.Infof(msg)
|
||||
} else {
|
||||
msg = fmt.Sprintf("资产「%v」Shell执行成功,返回值「%v」,耗时「%v」", asset.Name, result, elapsed)
|
||||
log.Infof(msg)
|
||||
}
|
||||
|
||||
msgChan <- msg
|
||||
}()
|
||||
}
|
||||
|
||||
var message = ""
|
||||
for i := 0; i < len(assets); i++ {
|
||||
message += <-msgChan + "\n"
|
||||
}
|
||||
|
||||
_ = r.jobService.jobRepository.UpdateLastUpdatedById(r.ID)
|
||||
jobLog := model.JobLog{
|
||||
ID: utils.UUID(),
|
||||
JobId: r.ID,
|
||||
Timestamp: utils.NowJsonTime(),
|
||||
Message: message,
|
||||
}
|
||||
|
||||
_ = r.jobService.jobLogRepository.Create(&jobLog)
|
||||
}
|
||||
|
||||
func ExecCommandBySSH(cmd, ip string, port int, username, password, privateKey, passphrase string) (result string, err error) {
|
||||
sshClient, err := term.NewSshClient(ip, port, username, password, privateKey, passphrase)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
session, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
//执行远程命令
|
||||
combo, err := session.CombinedOutput(cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(combo), nil
|
||||
}
|
||||
|
||||
func (r JobService) ExecJobById(id string) (err error) {
|
||||
job, err := r.jobRepository.FindById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j, err := getJob(&job, &r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r JobService) InitJob() error {
|
||||
jobs, _ := r.jobRepository.FindByFunc(constant.FuncCheckAssetStatusJob)
|
||||
if jobs == nil {
|
||||
job := model.Job{
|
||||
ID: utils.UUID(),
|
||||
Name: "资产状态检测",
|
||||
Func: constant.FuncCheckAssetStatusJob,
|
||||
Cron: "0 0 0/1 * * ?",
|
||||
Mode: constant.JobModeAll,
|
||||
Status: constant.JobStatusRunning,
|
||||
Created: utils.NowJsonTime(),
|
||||
Updated: utils.NowJsonTime(),
|
||||
}
|
||||
if err := r.jobRepository.Create(&job); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("创建计划任务「%v」cron「%v」", job.Name, job.Cron)
|
||||
} else {
|
||||
for i := range jobs {
|
||||
if jobs[i].Status == constant.JobStatusRunning {
|
||||
err := r.ChangeStatusById(jobs[i].ID, constant.JobStatusRunning)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("启动计划任务「%v」cron「%v」", jobs[i].Name, jobs[i].Cron)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r JobService) Create(o *model.Job) (err error) {
|
||||
|
||||
if o.Status == constant.JobStatusRunning {
|
||||
j, err := getJob(o, &r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobId, err := global.Cron.AddJob(o.Cron, j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.CronJobId = int(jobId)
|
||||
}
|
||||
|
||||
return r.jobRepository.Create(o)
|
||||
}
|
||||
|
||||
func (r JobService) DeleteJobById(id string) error {
|
||||
job, err := r.jobRepository.FindById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.Status == constant.JobStatusRunning {
|
||||
if err := r.ChangeStatusById(id, constant.JobStatusNotRunning); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return r.jobRepository.DeleteJobById(id)
|
||||
}
|
42
pkg/service/mail.go
Normal file
42
pkg/service/mail.go
Normal file
@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/smtp"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/repository"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
type MailService struct {
|
||||
propertyRepository *repository.PropertyRepository
|
||||
}
|
||||
|
||||
func NewMailService(propertyRepository *repository.PropertyRepository) *MailService {
|
||||
return &MailService{propertyRepository: propertyRepository}
|
||||
}
|
||||
|
||||
func (r MailService) SendMail(to, subject, text string) {
|
||||
propertiesMap := r.propertyRepository.FindAllMap()
|
||||
host := propertiesMap[constant.MailHost]
|
||||
port := propertiesMap[constant.MailPort]
|
||||
username := propertiesMap[constant.MailUsername]
|
||||
password := propertiesMap[constant.MailPassword]
|
||||
|
||||
if host == "" || port == "" || username == "" || password == "" {
|
||||
log.Debugf("邮箱信息不完整,跳过发送邮件。")
|
||||
return
|
||||
}
|
||||
|
||||
e := email.NewEmail()
|
||||
e.From = "Next Terminal <" + username + ">"
|
||||
e.To = []string{to}
|
||||
e.Subject = subject
|
||||
e.Text = []byte(text)
|
||||
err := e.Send(host+":"+port, smtp.PlainAuth("", username, password, host))
|
||||
if err != nil {
|
||||
log.Errorf("邮件发送失败: %v", err.Error())
|
||||
}
|
||||
}
|
237
pkg/service/property.go
Normal file
237
pkg/service/property.go
Normal file
@ -0,0 +1,237 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/utils"
|
||||
)
|
||||
|
||||
type PropertyService struct {
|
||||
propertyRepository *repository.PropertyRepository
|
||||
}
|
||||
|
||||
func NewPropertyService(propertyRepository *repository.PropertyRepository) *PropertyService {
|
||||
return &PropertyService{propertyRepository: propertyRepository}
|
||||
}
|
||||
|
||||
func (r PropertyService) InitProperties() error {
|
||||
propertyMap := r.propertyRepository.FindAllMap()
|
||||
|
||||
if len(propertyMap[guacd.Host]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.Host,
|
||||
Value: "127.0.0.1",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.Port]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.Port,
|
||||
Value: "4822",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableRecording]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableRecording,
|
||||
Value: "true",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.RecordingPath]) == 0 {
|
||||
path, _ := os.Getwd()
|
||||
property := model.Property{
|
||||
Name: guacd.RecordingPath,
|
||||
Value: path + "/recording/",
|
||||
}
|
||||
if !utils.FileExists(property.Value) {
|
||||
if err := os.Mkdir(property.Value, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.CreateRecordingPath]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.CreateRecordingPath,
|
||||
Value: "true",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.DriveName]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.DriveName,
|
||||
Value: "File-System",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.DrivePath]) == 0 {
|
||||
|
||||
path, _ := os.Getwd()
|
||||
|
||||
property := model.Property{
|
||||
Name: guacd.DrivePath,
|
||||
Value: path + "/drive/",
|
||||
}
|
||||
if !utils.FileExists(property.Value) {
|
||||
if err := os.Mkdir(property.Value, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.FontName]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.FontName,
|
||||
Value: "menlo",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.FontSize]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.FontSize,
|
||||
Value: "12",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.ColorScheme]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.ColorScheme,
|
||||
Value: "gray-black",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableDrive]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableDrive,
|
||||
Value: "true",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableWallpaper]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableWallpaper,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableTheming]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableTheming,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableFontSmoothing]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableFontSmoothing,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableFullWindowDrag]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableFullWindowDrag,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableDesktopComposition]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableDesktopComposition,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.EnableMenuAnimations]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.EnableMenuAnimations,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.DisableBitmapCaching]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.DisableBitmapCaching,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.DisableOffscreenCaching]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.DisableOffscreenCaching,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(propertyMap[guacd.DisableGlyphCaching]) == 0 {
|
||||
property := model.Property{
|
||||
Name: guacd.DisableGlyphCaching,
|
||||
Value: "false",
|
||||
}
|
||||
if err := r.propertyRepository.Create(&property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
32
pkg/service/session.go
Normal file
32
pkg/service/session.go
Normal file
@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/utils"
|
||||
)
|
||||
|
||||
type SessionService struct {
|
||||
sessionRepository *repository.SessionRepository
|
||||
}
|
||||
|
||||
func NewSessionService(sessionRepository *repository.SessionRepository) *SessionService {
|
||||
return &SessionService{sessionRepository: sessionRepository}
|
||||
}
|
||||
|
||||
func (r SessionService) Fix() {
|
||||
sessions, _ := r.sessionRepository.FindByStatus(constant.Connected)
|
||||
if sessions == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range sessions {
|
||||
session := model.Session{
|
||||
Status: constant.Disconnected,
|
||||
DisconnectedTime: utils.NowJsonTime(),
|
||||
}
|
||||
|
||||
_ = r.sessionRepository.UpdateById(&session, sessions[i].ID)
|
||||
}
|
||||
}
|
104
pkg/service/user.go
Normal file
104
pkg/service/user.go
Normal file
@ -0,0 +1,104 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/utils"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepository *repository.UserRepository
|
||||
loginLogRepository *repository.LoginLogRepository
|
||||
}
|
||||
|
||||
func NewUserService(userRepository *repository.UserRepository, loginLogRepository *repository.LoginLogRepository) *UserService {
|
||||
return &UserService{userRepository: userRepository, loginLogRepository: loginLogRepository}
|
||||
}
|
||||
|
||||
func (r UserService) InitUser() (err error) {
|
||||
|
||||
users := r.userRepository.FindAll()
|
||||
|
||||
if len(users) == 0 {
|
||||
initPassword := "admin"
|
||||
var pass []byte
|
||||
if pass, err = utils.Encoder.Encode([]byte(initPassword)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
ID: utils.UUID(),
|
||||
Username: "admin",
|
||||
Password: string(pass),
|
||||
Nickname: "超级管理员",
|
||||
Type: constant.TypeAdmin,
|
||||
Created: utils.NowJsonTime(),
|
||||
}
|
||||
if err := r.userRepository.Create(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("初始用户创建成功,账号:「%v」密码:「%v」", user.Username, initPassword)
|
||||
} else {
|
||||
for i := range users {
|
||||
// 修正默认用户类型为管理员
|
||||
if users[i].Type == "" {
|
||||
user := model.User{
|
||||
Type: constant.TypeAdmin,
|
||||
ID: users[i].ID,
|
||||
}
|
||||
if err := r.userRepository.Update(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("自动修正用户「%v」ID「%v」类型为管理员", users[i].Nickname, users[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r UserService) FixedOnlineState() error {
|
||||
// 修正用户登录状态
|
||||
onlineUsers, err := r.userRepository.FindOnlineUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range onlineUsers {
|
||||
logs, err := r.loginLogRepository.FindAliveLoginLogsByUserId(onlineUsers[i].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
if err := r.userRepository.UpdateOnline(onlineUsers[i].ID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r UserService) Logout(token string) (err error) {
|
||||
|
||||
loginLog, err := r.loginLogRepository.FindById(token)
|
||||
if err != nil {
|
||||
log.Warnf("登录日志「%v」获取失败", token)
|
||||
return
|
||||
}
|
||||
|
||||
loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token}
|
||||
err = r.loginLogRepository.Update(loginLogForUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginLogs, err := r.loginLogRepository.FindAliveLoginLogsByUserId(loginLog.UserId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(loginLogs) == 0 {
|
||||
err = r.userRepository.UpdateOnline(loginLog.UserId, false)
|
||||
}
|
||||
return
|
||||
}
|
73
pkg/task/ticker.go
Normal file
73
pkg/task/ticker.go
Normal file
@ -0,0 +1,73 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/repository"
|
||||
)
|
||||
|
||||
type Ticker struct {
|
||||
sessionRepository *repository.SessionRepository
|
||||
propertyRepository *repository.PropertyRepository
|
||||
}
|
||||
|
||||
func NewTicker(sessionRepository *repository.SessionRepository, propertyRepository *repository.PropertyRepository) *Ticker {
|
||||
return &Ticker{sessionRepository: sessionRepository, propertyRepository: propertyRepository}
|
||||
}
|
||||
|
||||
func (t *Ticker) SetupTicker() {
|
||||
|
||||
// 每隔一小时删除一次未使用的会话信息
|
||||
unUsedSessionTicker := time.NewTicker(time.Minute * 60)
|
||||
go func() {
|
||||
for range unUsedSessionTicker.C {
|
||||
sessions, _ := t.sessionRepository.FindByStatusIn([]string{constant.NoConnect, constant.Connecting})
|
||||
if len(sessions) > 0 {
|
||||
now := time.Now()
|
||||
for i := range sessions {
|
||||
if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 {
|
||||
_ = t.sessionRepository.DeleteById(sessions[i].ID)
|
||||
s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port)
|
||||
log.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 每日凌晨删除超过时长限制的会话
|
||||
timeoutSessionTicker := time.NewTicker(time.Hour * 24)
|
||||
go func() {
|
||||
for range timeoutSessionTicker.C {
|
||||
property, err := t.propertyRepository.FindByName("session-saved-limit")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if property.Value == "" || property.Value == "-" {
|
||||
return
|
||||
}
|
||||
limit, err := strconv.Atoi(property.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sessions, err := t.sessionRepository.FindOutTimeSessions(limit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(sessions) > 0 {
|
||||
var sessionIds []string
|
||||
for i := range sessions {
|
||||
sessionIds = append(sessionIds, sessions[i].ID)
|
||||
}
|
||||
err := t.sessionRepository.DeleteByIds(sessionIds)
|
||||
if err != nil {
|
||||
log.Errorf("删除离线会话失败 %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
107
pkg/term/next_terminal.go
Normal file
107
pkg/term/next_terminal.go
Normal file
@ -0,0 +1,107 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type NextTerminal struct {
|
||||
SshClient *ssh.Client
|
||||
SshSession *ssh.Session
|
||||
StdinPipe io.WriteCloser
|
||||
SftpClient *sftp.Client
|
||||
Recorder *Recorder
|
||||
NextWriter *NextWriter
|
||||
}
|
||||
|
||||
func NewNextTerminal(ip string, port int, username, password, privateKey, passphrase string, rows, cols int, recording string) (*NextTerminal, error) {
|
||||
|
||||
sshClient, err := NewSshClient(ip, port, username, password, privateKey, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer sshSession.Close()
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
if err := sshSession.RequestPty("xterm-256color", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nextWriter NextWriter
|
||||
sshSession.Stdout = &nextWriter
|
||||
sshSession.Stderr = &nextWriter
|
||||
|
||||
stdinPipe, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var recorder *Recorder
|
||||
if recording != "" {
|
||||
recorder, err = CreateRecording(recording, rows, cols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
terminal := NextTerminal{
|
||||
SshClient: sshClient,
|
||||
SshSession: sshSession,
|
||||
Recorder: recorder,
|
||||
StdinPipe: stdinPipe,
|
||||
NextWriter: &nextWriter,
|
||||
}
|
||||
|
||||
return &terminal, nil
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Write(p []byte) (int, error) {
|
||||
return ret.StdinPipe.Write(p)
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Read() ([]byte, int, error) {
|
||||
bytes, n, err := ret.NextWriter.Read()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if ret.Recorder != nil && n > 0 {
|
||||
_ = ret.Recorder.WriteData(string(bytes))
|
||||
}
|
||||
return bytes, n, nil
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Close() error {
|
||||
if ret.SshSession != nil {
|
||||
return ret.SshSession.Close()
|
||||
}
|
||||
|
||||
if ret.SshClient != nil {
|
||||
return ret.SshClient.Close()
|
||||
}
|
||||
|
||||
if ret.Recorder != nil {
|
||||
return ret.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) WindowChange(h int, w int) error {
|
||||
return ret.SshSession.WindowChange(h, w)
|
||||
}
|
30
pkg/term/next_writer.go
Normal file
30
pkg/term/next_writer.go
Normal file
@ -0,0 +1,30 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type NextWriter struct {
|
||||
b bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *NextWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.b.Write(p)
|
||||
}
|
||||
|
||||
func (w *NextWriter) Read() ([]byte, int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
p := w.b.Bytes()
|
||||
buf := make([]byte, len(p))
|
||||
read, err := w.b.Read(buf)
|
||||
w.b.Reset()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return buf, read, err
|
||||
}
|
123
pkg/term/recording.go
Normal file
123
pkg/term/recording.go
Normal file
@ -0,0 +1,123 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"next-terminal/server/utils"
|
||||
)
|
||||
|
||||
type Env struct {
|
||||
Shell string `json:"SHELL"`
|
||||
Term string `json:"TERM"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Title string `json:"title"`
|
||||
Version int `json:"version"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
Env Env `json:"env"`
|
||||
Timestamp int `json:"Timestamp"`
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
File *os.File
|
||||
Timestamp int
|
||||
}
|
||||
|
||||
func NewRecorder(recoding string) (recorder *Recorder, err error) {
|
||||
recorder = &Recorder{}
|
||||
|
||||
parentDirectory := utils.GetParentDirectory(recoding)
|
||||
|
||||
if utils.FileExists(parentDirectory) {
|
||||
if err := os.RemoveAll(parentDirectory); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(parentDirectory, 0777); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.Create(recoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recorder.File = file
|
||||
return recorder, nil
|
||||
}
|
||||
|
||||
func (recorder *Recorder) Close() {
|
||||
if recorder.File != nil {
|
||||
recorder.File.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (recorder *Recorder) WriteHeader(header *Header) (err error) {
|
||||
var p []byte
|
||||
|
||||
if p, err = json.Marshal(header); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := recorder.File.Write(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := recorder.File.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recorder.Timestamp = header.Timestamp
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (recorder *Recorder) WriteData(data string) (err error) {
|
||||
now := int(time.Now().UnixNano())
|
||||
|
||||
delta := float64(now-recorder.Timestamp*1000*1000*1000) / 1000 / 1000 / 1000
|
||||
|
||||
row := make([]interface{}, 0)
|
||||
row = append(row, delta)
|
||||
row = append(row, "o")
|
||||
row = append(row, data)
|
||||
|
||||
var s []byte
|
||||
if s, err = json.Marshal(row); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := recorder.File.Write(s); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := recorder.File.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateRecording(recordingPath string, h int, w int) (*Recorder, error) {
|
||||
recorder, err := NewRecorder(recordingPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := &Header{
|
||||
Title: "",
|
||||
Version: 2,
|
||||
Height: 42,
|
||||
Width: 150,
|
||||
Env: Env{Shell: "/bin/bash", Term: "xterm-256color"},
|
||||
Timestamp: int(time.Now().Unix()),
|
||||
}
|
||||
|
||||
if err := recorder.WriteHeader(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recorder, nil
|
||||
}
|
54
pkg/term/ssh.go
Normal file
54
pkg/term/ssh.go
Normal file
@ -0,0 +1,54 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func NewSshClient(ip string, port int, username, password, privateKey, passphrase string) (*ssh.Client, error) {
|
||||
var authMethod ssh.AuthMethod
|
||||
if username == "-" || username == "" {
|
||||
username = "root"
|
||||
}
|
||||
if password == "-" {
|
||||
password = ""
|
||||
}
|
||||
if privateKey == "-" {
|
||||
privateKey = ""
|
||||
}
|
||||
if passphrase == "-" {
|
||||
passphrase = ""
|
||||
}
|
||||
|
||||
var err error
|
||||
if privateKey != "" {
|
||||
var key ssh.Signer
|
||||
if len(passphrase) > 0 {
|
||||
key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
key, err = ssh.ParsePrivateKey([]byte(privateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
authMethod = ssh.PublicKeys(key)
|
||||
} else {
|
||||
authMethod = ssh.Password(password)
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
Timeout: 1 * time.Second,
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
return ssh.Dial("tcp", addr, config)
|
||||
}
|
176
pkg/term/test/test_ssh.go
Normal file
176
pkg/term/test/test_ssh.go
Normal file
@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/log"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type SSHTerminal struct {
|
||||
Session *ssh.Session
|
||||
exitMsg string
|
||||
stdout io.Reader
|
||||
stdin io.Writer
|
||||
stderr io.Reader
|
||||
}
|
||||
|
||||
func main() {
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: "root",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password("root"),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", "172.16.101.32:22", sshConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
err = New(client)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SSHTerminal) updateTerminalSize() {
|
||||
|
||||
go func() {
|
||||
// SIGWINCH is sent to the process when the window size of the terminal has
|
||||
// changed.
|
||||
sigwinchCh := make(chan os.Signal, 1)
|
||||
//signal.Notify(sigwinchCh, syscall.SIN)
|
||||
|
||||
fd := int(os.Stdin.Fd())
|
||||
termWidth, termHeight, err := terminal.GetSize(fd)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
// The client updated the size of the local PTY. This change needs to occur
|
||||
// on the server side PTY as well.
|
||||
case sigwinch := <-sigwinchCh:
|
||||
if sigwinch == nil {
|
||||
return
|
||||
}
|
||||
currTermWidth, currTermHeight, err := terminal.GetSize(fd)
|
||||
|
||||
// Terminal size has not changed, don't do anything.
|
||||
if currTermHeight == termHeight && currTermWidth == termWidth {
|
||||
continue
|
||||
}
|
||||
|
||||
err = t.Session.WindowChange(currTermHeight, currTermWidth)
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to send window-change reqest: %s.", err)
|
||||
continue
|
||||
}
|
||||
|
||||
termWidth, termHeight = currTermWidth, currTermHeight
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (t *SSHTerminal) interactiveSession() error {
|
||||
|
||||
defer func() {
|
||||
if t.exitMsg == "" {
|
||||
log.Info(os.Stdout, "the connection was closed on the remote side on ", time.Now().Format(time.RFC822))
|
||||
} else {
|
||||
log.Info(os.Stdout, t.exitMsg)
|
||||
}
|
||||
}()
|
||||
|
||||
fd := int(os.Stdin.Fd())
|
||||
state, err := terminal.MakeRaw(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer terminal.Restore(fd, state)
|
||||
|
||||
termWidth, termHeight, err := terminal.GetSize(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
termType := os.Getenv("TERM")
|
||||
if termType == "" {
|
||||
termType = "xterm-256color"
|
||||
}
|
||||
|
||||
err = t.Session.RequestPty(termType, termHeight, termWidth, ssh.TerminalModes{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.updateTerminalSize()
|
||||
|
||||
t.stdin, err = t.Session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.stdout, err = t.Session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.stderr, err = t.Session.StderrPipe()
|
||||
|
||||
go io.Copy(os.Stderr, t.stderr)
|
||||
go io.Copy(os.Stdout, t.stdout)
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
_, err = t.stdin.Write(buf[:n])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
t.exitMsg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = t.Session.Shell()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = t.Session.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(client *ssh.Client) error {
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
s := SSHTerminal{
|
||||
Session: session,
|
||||
}
|
||||
|
||||
return s.interactiveSession()
|
||||
}
|
19
pkg/totp/totp.go
Normal file
19
pkg/totp/totp.go
Normal file
@ -0,0 +1,19 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
otp_t "github.com/pquerna/otp"
|
||||
totp_t "github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
type GenerateOpts totp_t.GenerateOpts
|
||||
|
||||
func NewTOTP(opt GenerateOpts) (*otp_t.Key, error) {
|
||||
return totp_t.Generate(totp_t.GenerateOpts(opt))
|
||||
}
|
||||
|
||||
func Validate(code string, secret string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
return totp_t.Validate(code, secret)
|
||||
}
|
Reference in New Issue
Block a user