-
Notifications
You must be signed in to change notification settings - Fork 1
/
service.go
242 lines (209 loc) · 5.83 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"io"
"net/http"
"os"
"runtime"
"strconv"
"time"
)
/**
设备信息表
*/
var deviceInfoList map[string]*DeviceInfo = make(map[string]*DeviceInfo)
/**
默认ssh地址,只支持本地
*/
const defaultSshIp = "127.0.0.1"
/**
websocket接口
*/
func WebsshApi(c *gin.Context) {
conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Err(err).Send()
} else {
//客户端连接后获取参数
deviceId := c.Query("deviceId")
user := c.Query("user")
pwd := c.Query("pwd")
portStr := c.Query("port")
host := c.Query("host")
if len(deviceId) == 0 || len(user) == 0 || len(pwd) == 0 || len(portStr) == 0 {
logger.Warn().Msg("websocket参数为空")
conn.Close()
} else {
port, err := strconv.Atoi(portStr)
if err != nil {
logger.Warn().Msg("websocket端口参数不合法")
conn.Close()
} else {
//如果有host参数,那么就使用host参数作为连接的ssh主机地址
ip := defaultSshIp
if len(host) > 0 {
ip = host
}
deviceInfoList[deviceId] = &DeviceInfo{
DeviceId: deviceId,
SshPort: port,
Ip: ip,
SshUser: user,
SshPwd: pwd,
}
logger.Debug().Str("deviceId", deviceId).Str("ip", c.ClientIP()).Str("ua", c.Request.UserAgent()).Msg("websocket连接成功,开始建立ws<->ssh隧道")
go Ws2ssh(conn, deviceId)
}
}
}
}
/**
根据设备id获取ssh连接配置
应该从数据库读取
*/
func getSshConfigByDeviceId(deviceId string) (string, *ssh.ClientConfig) {
deviceInfo := deviceInfoList[deviceId]
if deviceInfo == nil {
return "", nil
}
sshConfig := &ssh.ClientConfig{
User: deviceInfo.SshUser,
Auth: []ssh.AuthMethod{
ssh.Password(deviceInfo.SshPwd),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
ClientVersion: "",
Timeout: 10 * time.Second,
}
return fmt.Sprintf("%s:%d", deviceInfo.Ip, deviceInfo.SshPort), sshConfig
}
/**
建立ssh连接
*/
func SSHConnect(deviceId string) (*ssh.Session, io.WriteCloser, error) {
addr, sshConfig := getSshConfigByDeviceId(deviceId)
//建立与SSH服务器的连接
sshClient, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh连接失败")
return nil, nil, err
}
//https://tools.ietf.org/html/rfc4254#page-10
session, err := sshClient.NewSession()
if err != nil {
sshClient.Close()
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh会话创建失败")
return nil, nil, err
}
modes := ssh.TerminalModes{
ssh.ECHO: 1, //打开回显
ssh.TTY_OP_ISPEED: 14400, //输入速率 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, //输出速率 14.4kbaud
ssh.VSTATUS: 1,
}
var termWidth, termHeight int
if runtime.GOOS == "windows" {
termWidth = 80
termHeight = 30
} else {
//使用VT100终端来实现tab键提示,上下键查看历史命令,clear键清屏等操作
//VT100 start
//windows下不支持VT100
fd := int(os.Stdin.Fd())
oldState, err := terminal.MakeRaw(fd)
if err != nil {
logger.Err(err).Msg("VT100终端错误")
}
defer terminal.Restore(fd, oldState)
//VT100 end
termWidth, termHeight, err = terminal.GetSize(fd)
}
//打开伪终端
//https://tools.ietf.org/html/rfc4254#page-11
err = session.RequestPty("xterm", termHeight, termWidth, modes)
if err != nil {
session.Close()
sshClient.Close()
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh伪终端创建失败")
return nil, nil, err
}
pipeInput, err := session.StdinPipe()
if err != nil {
session.Close()
sshClient.Close()
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh输入管道打开失败")
return nil, nil, err
}
//启动一个远程shell
//https://tools.ietf.org/html/rfc4254#page-13
err = session.Shell()
if err != nil {
session.Close()
sshClient.Close()
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh shell打开失败")
return nil, nil, err
}
go func() {
//等待远程命令结束或远程shell退出
err = session.Wait()
if err != nil {
logger.Err(err).Str("deviceId", deviceId).Str("ssh-addr", addr).Msg("ssh会话断开")
}
}()
return session, pipeInput, nil
}
/**
打通websocket 到 ssh之间的连接
*/
func Ws2ssh(wsConn *websocket.Conn, deviceId string) {
session, pipeInput, err := SSHConnect(deviceId)
if err != nil {
wsConn.Close()
return
}
session.Stdout = &MyOutput{WsConn: wsConn}
go StreamBind(wsConn, deviceId, session, pipeInput)
}
/**
流绑定
*/
func StreamBind(wsConn *websocket.Conn, deviceId string, session *ssh.Session, pipeInput io.WriteCloser) {
defer wsConn.Close()
defer session.Close()
for {
msgType, msg, err := wsConn.ReadMessage()
if err != nil {
logger.Err(err).Str("deviceId", deviceId).Msg("读取websocket数据失败,断开流绑定")
break
} else {
logger.Debug().Str("deviceId", deviceId).Int("msgType", msgType).Int("消息长度", len(msg)).Msg("websocket收到消息")
}
n, err := pipeInput.Write(msg)
if err != nil {
logger.Err(err).Str("deviceId", deviceId).Msg("数据发送ssh会话失败,断开流绑定")
break
} else {
logger.Debug().Str("deviceId", deviceId).Int("sendLen", n).Msg("发送给ssh数据")
}
}
logger.Info().Str("deviceId", deviceId).Msg("连接断开,关闭websocket和ssh会话")
}
func main() {
//启动websocket服务
h := gin.Default()
//内部使用的api
h.GET("/api/log/debug", SetLogDebugLevel)
h.GET("/api/log/info", SetLogInfoLevel)
//对外提供websocket api
h.GET("/api/v1/webssh", WebsshApi)
err := h.Run()
if err != nil {
logger.Err(err).Msg("服务启动失败")
}
}