提交 v1.3.0 beta
This commit is contained in:
303
server/common/guacamole/guacd.go
Normal file
303
server/common/guacamole/guacd.go
Normal file
@ -0,0 +1,303 @@
|
||||
package guacamole
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
EnableRecording = "enable-recording"
|
||||
RecordingPath = "recording-path"
|
||||
CreateRecordingPath = "create-recording-path"
|
||||
|
||||
FontName = "font-name"
|
||||
FontSize = "font-size"
|
||||
ColorScheme = "color-scheme"
|
||||
Backspace = "backspace"
|
||||
TerminalType = "terminal-type"
|
||||
|
||||
PreConnectionId = "preconnection-id"
|
||||
PreConnectionBlob = "preconnection-blob"
|
||||
|
||||
EnableDrive = "enable-drive"
|
||||
DriveName = "drive-name"
|
||||
DrivePath = "drive-path"
|
||||
EnableWallpaper = "enable-wallpaper"
|
||||
EnableTheming = "enable-theming"
|
||||
EnableFontSmoothing = "enable-font-smoothing"
|
||||
EnableFullWindowDrag = "enable-full-window-drag"
|
||||
EnableDesktopComposition = "enable-desktop-composition"
|
||||
EnableMenuAnimations = "enable-menu-animations"
|
||||
DisableBitmapCaching = "disable-bitmap-caching"
|
||||
DisableOffscreenCaching = "disable-offscreen-caching"
|
||||
// DisableGlyphCaching Deprecated
|
||||
DisableGlyphCaching = "disable-glyph-caching"
|
||||
ForceLossless = "force-lossless"
|
||||
|
||||
Domain = "domain"
|
||||
RemoteApp = "remote-app"
|
||||
RemoteAppDir = "remote-app-dir"
|
||||
RemoteAppArgs = "remote-app-args"
|
||||
|
||||
ColorDepth = "color-depth"
|
||||
Cursor = "cursor"
|
||||
SwapRedBlue = "swap-red-blue"
|
||||
DestHost = "dest-host"
|
||||
DestPort = "dest-port"
|
||||
|
||||
UsernameRegex = "username-regex"
|
||||
PasswordRegex = "password-regex"
|
||||
LoginSuccessRegex = "login-success-regex"
|
||||
LoginFailureRegex = "login-failure-regex"
|
||||
|
||||
Namespace = "namespace"
|
||||
Pod = "pod"
|
||||
Container = "container"
|
||||
UesSSL = "use-ssl"
|
||||
ClientCert = "client-cert"
|
||||
ClientKey = "client-key"
|
||||
CaCert = "ca-cert"
|
||||
IgnoreCert = "ignore-cert"
|
||||
)
|
||||
|
||||
const Delimiter = ';'
|
||||
const Version = "VERSION_1_4_0"
|
||||
|
||||
type Configuration struct {
|
||||
ConnectionID string
|
||||
Protocol string
|
||||
Parameters map[string]string
|
||||
}
|
||||
|
||||
func NewConfiguration() (config *Configuration) {
|
||||
config = &Configuration{}
|
||||
config.Parameters = make(map[string]string)
|
||||
return config
|
||||
}
|
||||
|
||||
func (opt *Configuration) SetParameter(name, value string) {
|
||||
opt.Parameters[name] = value
|
||||
}
|
||||
|
||||
func (opt *Configuration) UnSetParameter(name string) {
|
||||
delete(opt.Parameters, name)
|
||||
}
|
||||
|
||||
func (opt *Configuration) GetParameter(name string) string {
|
||||
return opt.Parameters[name]
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
Opcode string
|
||||
Args []string
|
||||
ProtocolForm string
|
||||
}
|
||||
|
||||
func NewInstruction(opcode string, args ...string) (ret Instruction) {
|
||||
ret.Opcode = opcode
|
||||
ret.Args = args
|
||||
return ret
|
||||
}
|
||||
|
||||
func (opt *Instruction) String() string {
|
||||
if len(opt.ProtocolForm) > 0 {
|
||||
return opt.ProtocolForm
|
||||
}
|
||||
|
||||
opt.ProtocolForm = fmt.Sprintf("%d.%s", len(opt.Opcode), opt.Opcode)
|
||||
for _, value := range opt.Args {
|
||||
opt.ProtocolForm += fmt.Sprintf(",%d.%s", len(value), value)
|
||||
}
|
||||
opt.ProtocolForm += string(Delimiter)
|
||||
return opt.ProtocolForm
|
||||
}
|
||||
|
||||
func (opt *Instruction) Parse(content string) Instruction {
|
||||
if strings.LastIndex(content, ";") > 0 {
|
||||
content = strings.TrimRight(content, ";")
|
||||
}
|
||||
messages := strings.Split(content, ",")
|
||||
|
||||
var args = make([]string, len(messages))
|
||||
for i := range messages {
|
||||
lm := strings.Split(messages[i], ".")
|
||||
args[i] = lm[1]
|
||||
}
|
||||
return NewInstruction(args[0], args[1:]...)
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
writer *bufio.Writer
|
||||
UUID string
|
||||
Config *Configuration
|
||||
IsOpen bool
|
||||
}
|
||||
|
||||
func NewTunnel(address string, config *Configuration) (ret *Tunnel, err error) {
|
||||
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret = &Tunnel{}
|
||||
ret.conn = conn
|
||||
ret.reader = bufio.NewReader(conn)
|
||||
ret.writer = bufio.NewWriter(conn)
|
||||
ret.Config = config
|
||||
|
||||
selectArg := config.ConnectionID
|
||||
if selectArg == "" {
|
||||
selectArg = config.Protocol
|
||||
}
|
||||
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args, err := ret.expect("args")
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
width := config.GetParameter("width")
|
||||
height := config.GetParameter("height")
|
||||
dpi := config.GetParameter("dpi")
|
||||
|
||||
// send size
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parameters := make([]string, len(args.Args))
|
||||
for i := range args.Args {
|
||||
argName := args.Args[i]
|
||||
if strings.Contains(argName, "VERSION") {
|
||||
parameters[i] = Version
|
||||
continue
|
||||
}
|
||||
parameters[i] = config.GetParameter(argName)
|
||||
}
|
||||
// send connect
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ready, err := ret.expect("ready")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(ready.Args) == 0 {
|
||||
_ = conn.Close()
|
||||
return nil, errors.New("no connection id received")
|
||||
}
|
||||
|
||||
ret.UUID = ready.Args[0]
|
||||
ret.IsOpen = true
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (opt *Tunnel) WriteInstructionAndFlush(instruction Instruction) error {
|
||||
if _, err := opt.WriteAndFlush([]byte(instruction.String())); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) {
|
||||
//fmt.Printf("-> %v\n", string(p))
|
||||
nn, err := opt.writer.Write(p)
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
err = opt.writer.Flush()
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (opt *Tunnel) ReadInstruction() (instruction Instruction, err error) {
|
||||
msg, err := opt.Read()
|
||||
if err != nil {
|
||||
return instruction, err
|
||||
}
|
||||
return instruction.Parse(string(msg)), err
|
||||
}
|
||||
|
||||
func (opt *Tunnel) Read() (p []byte, err error) {
|
||||
data, err := opt.reader.ReadBytes(Delimiter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s := string(data)
|
||||
//fmt.Printf("<- %v \n", s)
|
||||
if s == "rate=44100,channels=2;" {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
if s == "rate=22050,channels=2;" {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
if s == "5.audio,1.1,31.audio/L16;" {
|
||||
s += "rate=44100,channels=2;"
|
||||
}
|
||||
return []byte(s), err
|
||||
}
|
||||
|
||||
func (opt *Tunnel) expect(opcode string) (instruction Instruction, err error) {
|
||||
instruction, err = opt.ReadInstruction()
|
||||
if err != nil {
|
||||
return instruction, err
|
||||
}
|
||||
|
||||
if opcode != instruction.Opcode {
|
||||
msg := fmt.Sprintf(`expected "%s" instruction but instead received "%s"`, opcode, instruction.Opcode)
|
||||
return instruction, errors.New(msg)
|
||||
}
|
||||
return instruction, nil
|
||||
}
|
||||
|
||||
func (opt *Tunnel) Close() error {
|
||||
opt.IsOpen = false
|
||||
return opt.conn.Close()
|
||||
}
|
||||
|
||||
func Disconnect(ws *websocket.Conn, code int, reason string) {
|
||||
// guacd 无法处理中文字符,所以进行了base64编码。
|
||||
encodeReason := base64.StdEncoding.EncodeToString([]byte(reason))
|
||||
err := NewInstruction("error", encodeReason, strconv.Itoa(code))
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
|
||||
disconnect := NewInstruction("disconnect")
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
|
||||
}
|
56
server/common/jsontime.go
Normal file
56
server/common/jsontime.go
Normal file
@ -0,0 +1,56 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JsonTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func NewJsonTime(t time.Time) JsonTime {
|
||||
return JsonTime{
|
||||
Time: t,
|
||||
}
|
||||
}
|
||||
|
||||
func NowJsonTime() JsonTime {
|
||||
return JsonTime{
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (j JsonTime) MarshalJSON() ([]byte, error) {
|
||||
var stamp = fmt.Sprintf("\"%s\"", j.Format("2006-01-02 15:04:05"))
|
||||
return []byte(stamp), nil
|
||||
}
|
||||
|
||||
func (j *JsonTime) UnmarshalJSON(b []byte) error {
|
||||
s := strings.ReplaceAll(string(b), "\"", "")
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*j = NewJsonTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JsonTime) Value() (driver.Value, error) {
|
||||
var zeroTime time.Time
|
||||
if j.Time.UnixNano() == zeroTime.UnixNano() {
|
||||
return nil, nil
|
||||
}
|
||||
return j.Time, nil
|
||||
}
|
||||
|
||||
func (j *JsonTime) Scan(v interface{}) error {
|
||||
value, ok := v.(time.Time)
|
||||
if ok {
|
||||
*j = JsonTime{Time: value}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("can not convert %v to timestamp", v)
|
||||
}
|
3
server/common/maps/maps.go
Normal file
3
server/common/maps/maps.go
Normal file
@ -0,0 +1,3 @@
|
||||
package maps
|
||||
|
||||
type Map map[string]interface{}
|
79
server/common/nt/const.go
Normal file
79
server/common/nt/const.go
Normal file
@ -0,0 +1,79 @@
|
||||
package nt
|
||||
|
||||
import (
|
||||
"next-terminal/server/common/guacamole"
|
||||
)
|
||||
|
||||
const Token = "X-Auth-Token"
|
||||
|
||||
type Key string
|
||||
|
||||
const (
|
||||
DB Key = "db"
|
||||
|
||||
SSH = "ssh"
|
||||
RDP = "rdp"
|
||||
VNC = "vnc"
|
||||
Telnet = "telnet"
|
||||
K8s = "kubernetes"
|
||||
|
||||
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
|
||||
Native = "native" // 接入模式:原生
|
||||
Terminal = "terminal" // 接入模式:终端
|
||||
|
||||
TypeUser = "user" // 普通用户
|
||||
TypeAdmin = "admin" // 管理员
|
||||
|
||||
SourceLdap = "ldap" // 从LDAP同步的用户
|
||||
|
||||
StatusEnabled = "enabled"
|
||||
StatusDisabled = "disabled"
|
||||
|
||||
SocksProxyEnable = "socks-proxy-enable"
|
||||
SocksProxyHost = "socks-proxy-host"
|
||||
SocksProxyPort = "socks-proxy-port"
|
||||
SocksProxyUsername = "socks-proxy-username"
|
||||
SocksProxyPassword = "socks-proxy-password"
|
||||
|
||||
LoginToken = "login-token"
|
||||
AccessToken = "access-token"
|
||||
ShareSession = "share-session"
|
||||
|
||||
Anonymous = "anonymous"
|
||||
|
||||
StorageLogActionRm = "rm" // 删除
|
||||
StorageLogActionUpload = "upload" // 上传
|
||||
StorageLogActionDownload = "download" // 下载
|
||||
StorageLogActionMkdir = "mkdir" // 创建文件夹
|
||||
StorageLogActionRename = "rename" // 重命名
|
||||
)
|
||||
|
||||
var SSHParameterNames = []string{guacamole.FontName, guacamole.FontSize, guacamole.ColorScheme, guacamole.Backspace, guacamole.TerminalType, SshMode, SocksProxyEnable, SocksProxyHost, SocksProxyPort, SocksProxyUsername, SocksProxyPassword}
|
||||
var RDPParameterNames = []string{guacamole.Domain, guacamole.RemoteApp, guacamole.RemoteAppDir, guacamole.RemoteAppArgs, guacamole.EnableDrive, guacamole.DrivePath, guacamole.ColorDepth, guacamole.ForceLossless, guacamole.PreConnectionId, guacamole.PreConnectionBlob}
|
||||
var VNCParameterNames = []string{guacamole.ColorDepth, guacamole.Cursor, guacamole.SwapRedBlue, guacamole.DestHost, guacamole.DestPort}
|
||||
var TelnetParameterNames = []string{guacamole.FontName, guacamole.FontSize, guacamole.ColorScheme, guacamole.Backspace, guacamole.TerminalType, guacamole.UsernameRegex, guacamole.PasswordRegex, guacamole.LoginSuccessRegex, guacamole.LoginFailureRegex}
|
||||
var KubernetesParameterNames = []string{guacamole.FontName, guacamole.FontSize, guacamole.ColorScheme, guacamole.Backspace, guacamole.TerminalType, guacamole.Namespace, guacamole.Pod, guacamole.Container, guacamole.UesSSL, guacamole.ClientCert, guacamole.ClientKey, guacamole.CaCert, guacamole.IgnoreCert}
|
8
server/common/nt/errors.go
Normal file
8
server/common/nt/errors.go
Normal file
@ -0,0 +1,8 @@
|
||||
package nt
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNameAlreadyUsed = errors.New("name already used")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
)
|
34
server/common/sets/set.go
Normal file
34
server/common/sets/set.go
Normal file
@ -0,0 +1,34 @@
|
||||
package sets
|
||||
|
||||
func NewStringSet() *Set {
|
||||
return &Set{data: make(map[string]struct{})}
|
||||
}
|
||||
|
||||
type Set struct {
|
||||
data map[string]struct{}
|
||||
}
|
||||
|
||||
func (s *Set) Add(key ...string) {
|
||||
for _, k := range key {
|
||||
s.data[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) Remove(key ...string) {
|
||||
for _, k := range key {
|
||||
delete(s.data, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) Contains(key string) bool {
|
||||
_, ok := s.data[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Set) ToArray() []string {
|
||||
var keys []string
|
||||
for key, _ := range s.data {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
1
server/common/slices/slices.go
Normal file
1
server/common/slices/slices.go
Normal file
@ -0,0 +1 @@
|
||||
package slices
|
30
server/common/taskrunner/task_runner.go
Normal file
30
server/common/taskrunner/task_runner.go
Normal file
@ -0,0 +1,30 @@
|
||||
package taskrunner
|
||||
|
||||
import "sync"
|
||||
|
||||
type Runner struct {
|
||||
wg sync.WaitGroup
|
||||
errors []error
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func (r *Runner) Add(f func() error) {
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
if err := f(); err != nil {
|
||||
r.addError(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *Runner) addError(err error) {
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
r.errors = append(r.errors, err)
|
||||
}
|
||||
|
||||
func (r *Runner) Wait() []error {
|
||||
r.wg.Wait()
|
||||
return r.errors
|
||||
}
|
121
server/common/term/next_terminal.go
Normal file
121
server/common/term/next_terminal.go
Normal file
@ -0,0 +1,121 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"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
|
||||
StdoutReader *bufio.Reader
|
||||
}
|
||||
|
||||
func NewNextTerminal(ip string, port int, username, password, privateKey, passphrase string, rows, cols int, recording, term string, pipe bool) (*NextTerminal, error) {
|
||||
sshClient, err := NewSshClient(ip, port, username, password, privateKey, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newNT(sshClient, pipe, recording, term, rows, cols)
|
||||
}
|
||||
|
||||
func NewNextTerminalUseSocks(ip string, port int, username, password, privateKey, passphrase string, rows, cols int, recording, term string, pipe bool, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword string) (*NextTerminal, error) {
|
||||
sshClient, err := NewSshClientUseSocks(ip, port, username, password, privateKey, passphrase, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newNT(sshClient, pipe, recording, term, rows, cols)
|
||||
}
|
||||
|
||||
func newNT(sshClient *ssh.Client, pipe bool, recording string, term string, rows int, cols int) (*NextTerminal, error) {
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stdoutReader *bufio.Reader
|
||||
if pipe {
|
||||
stdoutPipe, err := sshSession.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdoutReader = bufio.NewReader(stdoutPipe)
|
||||
}
|
||||
|
||||
var stdinPipe io.WriteCloser
|
||||
if pipe {
|
||||
stdinPipe, err = sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var recorder *Recorder
|
||||
if recording != "" {
|
||||
recorder, err = NewRecorder(recording, term, rows, cols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
terminal := NextTerminal{
|
||||
SshClient: sshClient,
|
||||
SshSession: sshSession,
|
||||
Recorder: recorder,
|
||||
StdinPipe: stdinPipe,
|
||||
StdoutReader: stdoutReader,
|
||||
}
|
||||
|
||||
return &terminal, nil
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Write(p []byte) (int, error) {
|
||||
if ret.StdinPipe == nil {
|
||||
return 0, errors.New("pipe is not open")
|
||||
}
|
||||
return ret.StdinPipe.Write(p)
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Close() {
|
||||
|
||||
if ret.SftpClient != nil {
|
||||
_ = ret.SftpClient.Close()
|
||||
}
|
||||
|
||||
if ret.SshSession != nil {
|
||||
_ = ret.SshSession.Close()
|
||||
}
|
||||
|
||||
if ret.SshClient != nil {
|
||||
_ = ret.SshClient.Close()
|
||||
}
|
||||
|
||||
if ret.Recorder != nil {
|
||||
ret.Recorder.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) WindowChange(h int, w int) error {
|
||||
return ret.SshSession.WindowChange(h, w)
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) RequestPty(term string, h, w int) error {
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
return ret.SshSession.RequestPty(term, h, w, modes)
|
||||
}
|
||||
|
||||
func (ret *NextTerminal) Shell() error {
|
||||
return ret.SshSession.Shell()
|
||||
}
|
113
server/common/term/recorder.go
Normal file
113
server/common/term/recorder.go
Normal file
@ -0,0 +1,113 @@
|
||||
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 (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 NewRecorder(recordingPath, term string, h int, w int) (recorder *Recorder, err error) {
|
||||
recorder = &Recorder{}
|
||||
parentDirectory := utils.GetParentDirectory(recordingPath)
|
||||
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(recordingPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recorder.File = file
|
||||
|
||||
header := &Header{
|
||||
Title: "",
|
||||
Version: 2,
|
||||
Height: h,
|
||||
Width: w,
|
||||
Env: Env{Shell: "/bin/bash", Term: term},
|
||||
Timestamp: int(time.Now().Unix()),
|
||||
}
|
||||
|
||||
if err := recorder.WriteHeader(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recorder, nil
|
||||
}
|
123
server/common/term/ssh.go
Normal file
123
server/common/term/ssh.go
Normal file
@ -0,0 +1,123 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
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: 3 * time.Second,
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
return ssh.Dial("tcp", addr, config)
|
||||
}
|
||||
|
||||
func NewSshClientUseSocks(ip string, port int, username, password, privateKey, passphrase string, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword 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: 3 * time.Second,
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
socksProxyAddr := fmt.Sprintf("%s:%s", socksProxyHost, socksProxyPort)
|
||||
|
||||
socks5, err := proxy.SOCKS5("tcp", socksProxyAddr,
|
||||
&proxy.Auth{User: socksProxyUsername, Password: socksProxyPassword},
|
||||
&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
conn, err := socks5.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientConn, channels, requests, err := ssh.NewClientConn(conn, addr, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ssh.NewClient(clientConn, channels, requests), nil
|
||||
}
|
175
server/common/term/test/test_ssh.go
Normal file
175
server/common/term/test/test_ssh.go
Normal file
@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"next-terminal/server/log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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
server/common/totp.go
Normal file
19
server/common/totp.go
Normal file
@ -0,0 +1,19 @@
|
||||
package common
|
||||
|
||||
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