项目开源
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptSettings">
|
||||||
|
<option name="languageLevel" value="ES6" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/ssh_manage.iml" filepath="$PROJECT_DIR$/.idea/ssh_manage.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/ssh_manage.iml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
71
README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# ssh_manage
|
||||||
|
go版本多用户webssh管理工具
|
||||||
|
|
||||||
|
项目仅用于学习交流,未经允许禁止任何其他用途
|
||||||
|
|
||||||
|
ssh2ws部分代码修改自https://github.com/hequan2017/go-webssh
|
||||||
|
|
||||||
|
服务端不保存用户明文密码,且不保存解密秘钥,如需对其他用户开放,请不要修改此部分代码,以免造成不必要的损失!
|
||||||
|
|
||||||
|
|
||||||
|
## 开发框架
|
||||||
|
Gin + gorm
|
||||||
|
|
||||||
|
##开发计划
|
||||||
|
✔ ssh功能
|
||||||
|
|
||||||
|
× 服务器文件管理
|
||||||
|
|
||||||
|
## 在线演示
|
||||||
|
[点击进入SSH云管理平台](https://www.do18.cn)
|
||||||
|
|
||||||
|
##环境
|
||||||
|
> Mysql
|
||||||
|
> Redis
|
||||||
|
> Go
|
||||||
|
|
||||||
|
##配置文件
|
||||||
|
> 修改config.toml的相关参数,短信接口使用阿里云短信
|
||||||
|
```toml
|
||||||
|
#配置文件
|
||||||
|
[Web]
|
||||||
|
model = "release" #debug release test
|
||||||
|
port = "0.0.0.0:8082" #服务要运行的端口
|
||||||
|
|
||||||
|
[Database]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 3306
|
||||||
|
username = "root" #数据库账号
|
||||||
|
password = "root" #数据库密码
|
||||||
|
dbname = "ssh" #数据库名
|
||||||
|
poolsize = 10 #Mysql连接池大小
|
||||||
|
|
||||||
|
[Redis]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 6379
|
||||||
|
password = "" #没有则不填
|
||||||
|
poolsize = 10 #Redis连接池大小
|
||||||
|
|
||||||
|
[Alisms]
|
||||||
|
accessid = "—"
|
||||||
|
accesskey = "-"
|
||||||
|
signname = "-" #短信签名
|
||||||
|
template = "-" #模板代码
|
||||||
|
|
||||||
|
```
|
||||||
|
##运行
|
||||||
|
###(Mysql会在首次使用时自动初始化)
|
||||||
|
```shell script
|
||||||
|
go build & ./ssh_manage
|
||||||
|
go run server.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端
|
||||||
|
> Lauyi + Xterm.js
|
||||||
|
|
||||||
|
|
||||||
|
##补充说明
|
||||||
|
如需要使用Nginx等进行反代,请确保可以正常代理websocket
|
||||||
|
|
||||||
|
##免责声明
|
||||||
|
本软件按“原样”提供,不提供任何形式的明示或暗示担保,包括但不限于对适销性,特定目的的适用性和非侵权性的担保。无论是由于软件,使用或其他方式产生的,与之有关或与之有关的合同,侵权或其他形式的任何索赔,损害或其他责任,作者或版权所有者概不负责。
|
||||||
65
common/aes.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AesEncryptCBC(origData []byte, key []byte) string {
|
||||||
|
// 分组秘钥
|
||||||
|
// NewCipher该函数限制了输入k的长度必须为16, 24或者32
|
||||||
|
key = Md5(key)[:24]
|
||||||
|
block, _ := aes.NewCipher(key)
|
||||||
|
blockSize := block.BlockSize() // 获取秘钥块的长度
|
||||||
|
origData = pkcs7Padding(origData, blockSize) // 补全码
|
||||||
|
//fmt.Println(blockSize)
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式
|
||||||
|
//fmt.Println(string(Md5(key)[:blockSize]))
|
||||||
|
encrypted := make([]byte, len(origData)) // 创建数组
|
||||||
|
blockMode.CryptBlocks(encrypted, origData) // 加密
|
||||||
|
return base64.StdEncoding.EncodeToString(encrypted)
|
||||||
|
}
|
||||||
|
func AesDecryptCBC(enc string, key []byte) string {
|
||||||
|
key = Md5(key)[:24]
|
||||||
|
encrypted,_ := base64.StdEncoding.DecodeString(enc)
|
||||||
|
block, _ := aes.NewCipher(key) // 分组秘钥
|
||||||
|
blockSize := block.BlockSize() // 获取秘钥块的长度
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
|
||||||
|
decrypted := make([]byte, len(encrypted)) // 创建数组
|
||||||
|
blockMode.CryptBlocks(decrypted, encrypted) // 解密
|
||||||
|
decrypted = pkcs7UnPadding(decrypted) // 去除补全码
|
||||||
|
return string(decrypted)
|
||||||
|
}
|
||||||
|
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(ciphertext)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padtext...)
|
||||||
|
}
|
||||||
|
func pkcs5UnPadding(origData []byte) []byte {
|
||||||
|
length := len(origData)
|
||||||
|
unpadding := int(origData[length-1])
|
||||||
|
return origData[:(length - unpadding)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(ciphertext)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7UnPadding(origData []byte) []byte {
|
||||||
|
length := len(origData)
|
||||||
|
unpadding := int(origData[length-1])
|
||||||
|
return origData[:(length - unpadding)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Md5(str []byte) []byte {
|
||||||
|
h := md5.New()
|
||||||
|
h.Write(str)
|
||||||
|
//fmt.Println(hex.EncodeToString(h.Sum(nil)))
|
||||||
|
return []byte(hex.EncodeToString(h.Sum(nil)))
|
||||||
|
}
|
||||||
1
common/config.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package common
|
||||||
33
common/core/helper.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JsonError(c *gin.Context, msg interface{}) {
|
||||||
|
c.AbortWithStatusJSON(200, gin.H{"ok": false, "msg": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleError(c *gin.Context, err error) bool {
|
||||||
|
if err != nil {
|
||||||
|
//logrus.WithError(err).Error("gin context http handler error")
|
||||||
|
JsonError(c, err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func WshandleError(ws *websocket.Conn, err error) bool {
|
||||||
|
if err != nil {
|
||||||
|
log.Println("handler ws ERROR:",err.Error())
|
||||||
|
dt := time.Now().Add(time.Second)
|
||||||
|
if err := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); err != nil {
|
||||||
|
log.Println("websocket writes control message failed:",err.Error())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
8
common/core/server.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Ip string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Passwd string
|
||||||
|
}
|
||||||
102
common/core/ssh.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSshClient(server Server) (*ssh.Client, error) {
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
Timeout: time.Second * 5,
|
||||||
|
User: server.User,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), //这个可以, 但是不够安全
|
||||||
|
//HostKeyCallback: hostKeyCallBackFunc(h.Host),
|
||||||
|
}
|
||||||
|
//if h.Type == "password" {
|
||||||
|
config.Auth = []ssh.AuthMethod{ssh.Password(server.Passwd)}
|
||||||
|
//} else {
|
||||||
|
// config.Auth = []ssh.AuthMethod{publicKeyAuthFunc(h.Key)}
|
||||||
|
//}
|
||||||
|
addr := fmt.Sprintf("%s:%d", server.Ip, server.Port)
|
||||||
|
c, err := ssh.Dial("tcp", addr, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
func hostKeyCallBackFunc(host string) ssh.HostKeyCallback {
|
||||||
|
hostPath, err := homedir.Expand("~/.ssh/known_hosts")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("find known_hosts's home dir failed", err)
|
||||||
|
}
|
||||||
|
file, err := os.Open(hostPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("can't find known_host file:", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var hostKey ssh.PublicKey
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Split(scanner.Text(), " ")
|
||||||
|
if len(fields) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(fields[0], host) {
|
||||||
|
var err error
|
||||||
|
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error parsing %q: %v", fields[2], err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hostKey == nil {
|
||||||
|
log.Fatalf("no hostkey for %s,%v", host, err)
|
||||||
|
}
|
||||||
|
return ssh.FixedHostKey(hostKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicKeyAuthFunc(kPath string) ssh.AuthMethod {
|
||||||
|
keyPath, err := homedir.Expand(kPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("find key's home dir failed", err)
|
||||||
|
}
|
||||||
|
key, err := ioutil.ReadFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("ssh key file read failed", err)
|
||||||
|
}
|
||||||
|
// Create the Signer for this private key.
|
||||||
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("ssh key signer failed", err)
|
||||||
|
}
|
||||||
|
return ssh.PublicKeys(signer)
|
||||||
|
}
|
||||||
|
func runCommand(client *ssh.Client, command string) (stdout string, err error) {
|
||||||
|
session, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
//log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
session.Stdout = &buf
|
||||||
|
err = session.Run(command)
|
||||||
|
if err != nil {
|
||||||
|
//log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stdout = string(buf.Bytes())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
196
common/core/ssh_shell_conn.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// copy data from WebSocket to ssh server
|
||||||
|
// and copy data from ssh server to WebSocket
|
||||||
|
|
||||||
|
// write data to WebSocket
|
||||||
|
// the data comes from ssh server.
|
||||||
|
type wsBufferWriter struct {
|
||||||
|
buffer bytes.Buffer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement Write interface to write bytes from ssh server into bytes.Buffer.
|
||||||
|
func (w *wsBufferWriter) Write(p []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.buffer.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
wsMsgCmd = "cmd"
|
||||||
|
wsMsgResize = "resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wsMsg struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Cmd string `json:"cmd"`
|
||||||
|
Cols int `json:"cols"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to ssh server using ssh session.
|
||||||
|
type SshConn struct {
|
||||||
|
// calling Write() to write data into ssh server
|
||||||
|
StdinPipe io.WriteCloser
|
||||||
|
// Write() be called to receive data from ssh server
|
||||||
|
ComboOutput *wsBufferWriter
|
||||||
|
Session *ssh.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
//flushComboOutput flush ssh.session combine output into websocket response
|
||||||
|
func flushComboOutput(w *wsBufferWriter, wsConn *websocket.Conn) error {
|
||||||
|
if w.buffer.Len() != 0 {
|
||||||
|
err := wsConn.WriteMessage(websocket.TextMessage, w.buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.buffer.Reset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup ssh shell session
|
||||||
|
// set Session and StdinPipe here,
|
||||||
|
// and the Session.Stdout and Session.Sdterr are also set.
|
||||||
|
func NewSshConn(cols, rows int, sshClient *ssh.Client) (*SshConn, error) {
|
||||||
|
sshSession, err := sshClient.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// we set stdin, then we can write data to ssh server via this stdin.
|
||||||
|
// but, as for reading data from ssh server, we can set Session.Stdout and Session.Stderr
|
||||||
|
// to receive data from ssh server, and write back to somewhere.
|
||||||
|
stdinP, err := sshSession.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
comboWriter := new(wsBufferWriter)
|
||||||
|
//ssh.stdout and stderr will write output into comboWriter
|
||||||
|
sshSession.Stdout = comboWriter
|
||||||
|
sshSession.Stderr = comboWriter
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 1, // disable echo
|
||||||
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||||
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||||
|
}
|
||||||
|
// Request pseudo terminal
|
||||||
|
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Start remote shell
|
||||||
|
if err := sshSession.Shell(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SshConn) Close() {
|
||||||
|
if s.Session != nil {
|
||||||
|
s.Session.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//ReceiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
|
||||||
|
func (ssConn *SshConn) ReceiveWsMsg(wsConn *websocket.Conn, exitCh chan bool) {
|
||||||
|
//tells other go routine quit
|
||||||
|
defer setQuit(exitCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
//read websocket msg
|
||||||
|
_, wsData, err := wsConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
//logrus.WithError(err).Error("reading webSocket message failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//unmashal bytes into struct
|
||||||
|
msgObj := wsMsg{
|
||||||
|
Type: "cmd",
|
||||||
|
Cmd: "",
|
||||||
|
Rows: 50,
|
||||||
|
Cols: 180,
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||||
|
log.Println("unmarshal websocket message failed:",string(wsData))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch msgObj.Type {
|
||||||
|
case wsMsgResize:
|
||||||
|
//handle xterm.js size change
|
||||||
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||||
|
if err := ssConn.Session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
||||||
|
log.Println("ssh pty change windows size failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case wsMsgCmd:
|
||||||
|
//handle xterm.js stdin
|
||||||
|
//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
|
||||||
|
decodeBytes := []byte(msgObj.Cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("websock cmd string base64 decoding failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := ssConn.StdinPipe.Write(decodeBytes); err != nil {
|
||||||
|
log.Println("ws cmd bytes write to ssh.stdin pipe failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (ssConn *SshConn) SendComboOutput(wsConn *websocket.Conn, exitCh chan bool) {
|
||||||
|
//tells other go routine quit
|
||||||
|
defer setQuit(exitCh)
|
||||||
|
|
||||||
|
//every 120ms write combine output bytes into websocket response
|
||||||
|
tick := time.NewTicker(time.Millisecond * time.Duration(120))
|
||||||
|
//for range time.Tick(120 * time.Millisecond){}
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
//write combine output bytes into websocket response
|
||||||
|
if err := flushComboOutput(ssConn.ComboOutput, wsConn); err != nil {
|
||||||
|
if err == io.EOF{
|
||||||
|
log.Println("Exit")
|
||||||
|
}
|
||||||
|
log.Println(err.Error())
|
||||||
|
//logrus.WithError(err).Error("ssh sending combo output to webSocket failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ssConn *SshConn) SessionWait(quitChan chan bool) {
|
||||||
|
if err := ssConn.Session.Wait(); err != nil {
|
||||||
|
log.Println("ssh session wait failed")
|
||||||
|
setQuit(quitChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setQuit(ch chan bool) {
|
||||||
|
ch <- true
|
||||||
|
}
|
||||||
47
common/jwt.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jwt_ket = []byte("ss_jwt_token")
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
Userid uint
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReleaseToken(id uint) (token string, err error) {
|
||||||
|
expire_time := time.Now().Add(7 * 24 * time.Hour)
|
||||||
|
claims := &Claims{
|
||||||
|
Userid: id,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
ExpiresAt: expire_time.Unix(),
|
||||||
|
IssuedAt: time.Now().Unix(),
|
||||||
|
Issuer: "admin",
|
||||||
|
Subject: "user",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token_obj := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
token, err = token_obj.SignedString(jwt_ket)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseToken(token string) (*Claims, error) {
|
||||||
|
|
||||||
|
//用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
|
||||||
|
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return jwt_ket, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if tokenClaims != nil {
|
||||||
|
// 从tokenClaims中获取到Claims对象,并使用断言,将该对象转换为我们自己定义的Claims
|
||||||
|
// 要传入指针,项目中结构体都是用指针传递,节省空间。
|
||||||
|
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
|
||||||
|
}
|
||||||
44
common/sms.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"ssh_manage/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var aliconfig = config.Config.Alisms
|
||||||
|
|
||||||
|
func VerifyMobileFormat(mobileNum string) bool {
|
||||||
|
regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
|
||||||
|
reg := regexp.MustCompile(regular)
|
||||||
|
return reg.MatchString(mobileNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sendsms(phone string) (captcha string,err error) {
|
||||||
|
captcha = createCaptcha()
|
||||||
|
client, err := dysmsapi.NewClientWithAccessKey("cn-hangzhou", aliconfig.Accessid, aliconfig.Accesskey)
|
||||||
|
request := dysmsapi.CreateSendBatchSmsRequest()
|
||||||
|
request.Scheme = "https"
|
||||||
|
request.PhoneNumberJson = fmt.Sprintf("[\"%s\"]", phone)
|
||||||
|
request.SignNameJson = fmt.Sprintf("[\"%s\"]", aliconfig.Signname)
|
||||||
|
request.TemplateCode = aliconfig.Template
|
||||||
|
request.TemplateParamJson = fmt.Sprintf("[{\"code\":\"%s\"}]", captcha)
|
||||||
|
response, err := client.SendBatchSms(request)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Println(err.Error())
|
||||||
|
//}
|
||||||
|
if response.Code != "OK" {
|
||||||
|
err = errors.New("短信服务器错误")
|
||||||
|
log.Println(response)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCaptcha() string {
|
||||||
|
return fmt.Sprintf("%08v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100000000))
|
||||||
|
}
|
||||||
39
common/verify.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verifyImpl interface {
|
||||||
|
Verify() (key, code string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(v verifyImpl) (is_verify bool) {
|
||||||
|
phone, code := v.Verify()
|
||||||
|
cache := database.Cache.Get()
|
||||||
|
defer cache.Close()
|
||||||
|
s_code, err := redis.String(cache.Do("GET", phone))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Verify Err:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if code != s_code {
|
||||||
|
log.Println(fmt.Sprintf("手机号:%s -- 验证码:%s 校验失败", phone, code))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIp(ip string) bool {
|
||||||
|
addr := strings.Trim(ip, " ")
|
||||||
|
regStr := `^(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`
|
||||||
|
if match, _ := regexp.MatchString(regStr, addr); match {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
24
config.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#配置文件
|
||||||
|
[Web]
|
||||||
|
model = "release" #debug release test
|
||||||
|
port = "0.0.0.0:8082" #服务要运行的端口
|
||||||
|
|
||||||
|
[Database]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 3306
|
||||||
|
username = "root" #数据库账号
|
||||||
|
password = "root" #数据库密码
|
||||||
|
dbname = "ssh" #数据库名
|
||||||
|
poolsize = 10 #Mysql连接池大小
|
||||||
|
|
||||||
|
[Redis]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 6379
|
||||||
|
password = "" #没有则不填
|
||||||
|
poolsize = 10 #Redis连接池大小
|
||||||
|
|
||||||
|
[Alisms]
|
||||||
|
accessid = "—"
|
||||||
|
accesskey = "-"
|
||||||
|
signname = "-" #短信签名
|
||||||
|
template = "-" #模板代码
|
||||||
55
config/config.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
//订制配置文件解析载体
|
||||||
|
type config struct {
|
||||||
|
Web *webset
|
||||||
|
Database *database
|
||||||
|
Redis *redis
|
||||||
|
Alisms *alisms
|
||||||
|
}
|
||||||
|
|
||||||
|
type alisms struct {
|
||||||
|
Accessid string
|
||||||
|
Accesskey string
|
||||||
|
Signname string
|
||||||
|
Template string
|
||||||
|
}
|
||||||
|
|
||||||
|
type redis struct {
|
||||||
|
//Driver string
|
||||||
|
Poolsize int
|
||||||
|
Host string
|
||||||
|
Port uint
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
//订制Database块
|
||||||
|
type database struct {
|
||||||
|
//Driver string
|
||||||
|
Poolsize int
|
||||||
|
Host string
|
||||||
|
Port uint
|
||||||
|
Username string
|
||||||
|
Dbname string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type webset struct {
|
||||||
|
Model string
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Config *config = new(config)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
//读取配置文件
|
||||||
|
_, err := toml.DecodeFile("config.toml", Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
37
controller/add.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/errcode"
|
||||||
|
"ssh_manage/model"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Addser(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
var info Apiform.Addser
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "数据错误"
|
||||||
|
if c.ShouldBind(&info) == nil {
|
||||||
|
if(common.CheckIp(info.Ip)){
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
result := db.DB.Create(&model.Server{Ip: info.Ip,Port: info.Port,Username: info.Username,Password: info.Password,Nickname: info.Nickname,BindUser: uid})
|
||||||
|
if result.RowsAffected == 1 && result.Error == nil{
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "保存成功"
|
||||||
|
}else{
|
||||||
|
resp.Code = errcode.S_Db_err
|
||||||
|
resp.Msg = "保存失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(200,resp)
|
||||||
|
}
|
||||||
178
controller/info.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Gre-Z/common/jtime"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/errcode"
|
||||||
|
"ssh_manage/model"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Info(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
var limit Apiform.Slist
|
||||||
|
if c.MustGet("uid").(uint) > 0 {
|
||||||
|
if c.ShouldBind(&limit) == nil {
|
||||||
|
//var user model.User
|
||||||
|
var list Apiform.List_resp
|
||||||
|
var server model.Server
|
||||||
|
server.BindUser = uid
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
db.DB.Model(&model.Server{}).Where(&server).Count(&list.Count).Offset((limit.Page - 1) * limit.Limit).Limit(limit.Limit).Find(&list.List)
|
||||||
|
//db.DB.Model(&user).Related(&servers,"Servers").Count(&list.Count).Offset((limit.Page - 1) * limit.Limit).Limit(limit.Limit).Find(&list.List)
|
||||||
|
resp.Code = 200
|
||||||
|
resp.Data = list
|
||||||
|
resp.Msg = "查询成功"
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "数据格式错误"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Verify_err
|
||||||
|
resp.Msg = "Token信息错误"
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdataNick(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
var edit Apiform.Edit
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
//nickname, name_exist := c.GetPostForm("nickname")
|
||||||
|
//sidstr, sid_exist := c.GetPostForm("id")
|
||||||
|
//sid, err := strconv.Atoi(sidstr)
|
||||||
|
//log.Println(c.ShouldBind(&edit))
|
||||||
|
if c.ShouldBind(&edit) == nil {
|
||||||
|
//server.Nickname = nickname
|
||||||
|
var server model.Server
|
||||||
|
server.ID = edit.ID
|
||||||
|
server.BindUser = uid
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
result := db.DB.Model(&model.Server{}).Where(&server).Update(model.Server{Nickname: edit.Nickname, Ip: edit.Ip, Port: edit.Port, Username: edit.Username})
|
||||||
|
if result.RowsAffected == 1 && result.Error == nil {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "保存成功"
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Db_err
|
||||||
|
resp.Msg = "修改失败"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "提交字段错误"
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resetpass(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
var edit Apiform.EditPass
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
//nickname, name_exist := c.GetPostForm("nickname")
|
||||||
|
//sidstr, sid_exist := c.GetPostForm("id")
|
||||||
|
//sid, err := strconv.Atoi(sidstr)
|
||||||
|
//log.Println(c.ShouldBind(&edit))
|
||||||
|
if c.ShouldBind(&edit) == nil {
|
||||||
|
//server.Nickname = nickname
|
||||||
|
var server model.Server
|
||||||
|
server.ID = edit.ID
|
||||||
|
server.BindUser = uid
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
result := db.DB.Model(&model.Server{}).Where(&server).Update(model.Server{Password: edit.Password})
|
||||||
|
if result.RowsAffected == 1 && result.Error == nil {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "保存成功"
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Db_err
|
||||||
|
resp.Msg = "修改失败"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "提交字段错误"
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Del(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
var del Apiform.Edit
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
if c.ShouldBind(&del) == nil {
|
||||||
|
//server.Nickname = nickname
|
||||||
|
var server model.Server
|
||||||
|
server.ID = del.ID
|
||||||
|
server.BindUser = uid
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
result := db.DB.Where(&server).Delete(&model.Server{})
|
||||||
|
if result.RowsAffected == 1 && result.Error == nil {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "删除成功"
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Db_err
|
||||||
|
resp.Msg = "操作失败"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "提交字段错误"
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTerm(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
var term Apiform.GetTerm
|
||||||
|
new_token := c.MustGet("token").(string)
|
||||||
|
if new_token != "" { //更新Token逻辑
|
||||||
|
resp.Token = new_token
|
||||||
|
}
|
||||||
|
uid := c.MustGet("uid").(uint)
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "表单错误"
|
||||||
|
if c.ShouldBind(&term) == nil {
|
||||||
|
var server model.Server
|
||||||
|
server.ID = term.ID
|
||||||
|
server.BindUser = uid
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
result := db.DB.Model(&model.Server{}).First(&server)
|
||||||
|
if result.RowsAffected == 1 && result.Error == nil {
|
||||||
|
db.DB.Model(&model.Server{}).Where(&server).Update(model.Server{BeforeTime: jtime.JsonTime{time.Now()}})
|
||||||
|
sid, err := term.Decode(server)
|
||||||
|
//log.Println(sid)
|
||||||
|
if err == nil {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Data = sid
|
||||||
|
resp.Msg = "OK"
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Verify_err
|
||||||
|
resp.Msg = "秘钥解密失败"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Db_err
|
||||||
|
resp.Msg = "服务器信息检索失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(200,resp)
|
||||||
|
}
|
||||||
44
controller/login.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/errcode"
|
||||||
|
"ssh_manage/model"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(c *gin.Context) {
|
||||||
|
//common.Sendsms()
|
||||||
|
//log.Print(db.DB.Exec("select * from products"))
|
||||||
|
//token := c.MustGet("token").(string)
|
||||||
|
//c.JSON(200, gin.H{"token": token})
|
||||||
|
var resp Apiform.Resp
|
||||||
|
resp.Code = errcode.C_from_err
|
||||||
|
resp.Msg = "手机号和验证码不能为空!"
|
||||||
|
var user Apiform.Login
|
||||||
|
if c.ShouldBind(&user) == nil {
|
||||||
|
if common.Verify(&user) {
|
||||||
|
var userinfo model.User
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
db.DB.Where(model.User{Phone: user.Phone}).FirstOrCreate(&userinfo)
|
||||||
|
new_token, err := common.ReleaseToken(userinfo.ID)
|
||||||
|
if err == nil {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "登陆成功"
|
||||||
|
resp.Data = userinfo
|
||||||
|
resp.Token = new_token
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_auth_err
|
||||||
|
resp.Msg = "Token创建失败"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.S_Verify_err
|
||||||
|
resp.Msg = "验证码校验失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//log.Printf(c.ClientIP())
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
64
controller/middleware/Auth.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/errcode"
|
||||||
|
"ssh_manage/model"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Auth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
jwt_token := c.GetHeader("Authorization")
|
||||||
|
//log.Println(jwt_token)
|
||||||
|
//log.Println(strings.HasPrefix(jwt_token, "Bearer "))
|
||||||
|
if jwt_token == "" || !strings.HasPrefix(jwt_token, "Bearer ") {
|
||||||
|
resp.Code = errcode.S_auth_fmt_err
|
||||||
|
resp.Msg = "Token不正确"
|
||||||
|
c.JSON(200, resp)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jwt_token = jwt_token[7:]
|
||||||
|
claims, err := common.ParseToken(jwt_token)
|
||||||
|
if err != nil {
|
||||||
|
resp.Code = errcode.S_auth_err
|
||||||
|
resp.Msg = "Token错误,请重新登录"
|
||||||
|
c.JSON(200, resp)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
valid := claims.Valid()
|
||||||
|
if valid != nil {
|
||||||
|
resp.Code = errcode.S_auth_err
|
||||||
|
resp.Msg = "用户登录超时,请重新登录"
|
||||||
|
c.JSON(200, resp)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var userinfo model.User
|
||||||
|
db := database.Get()
|
||||||
|
defer db.Close()
|
||||||
|
userinfo.ID = claims.Userid
|
||||||
|
db.DB.Where(userinfo).First(&userinfo)
|
||||||
|
if userinfo.Phone == 0 {
|
||||||
|
resp.Code = errcode.S_auth_err
|
||||||
|
resp.Msg = "用户不存在,请重新登录"
|
||||||
|
c.JSON(200, resp)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("uid", claims.Userid)
|
||||||
|
c.Set("token", "")
|
||||||
|
new_token, err := common.ReleaseToken(claims.Userid)
|
||||||
|
if time.Now().Add(24*time.Hour).Unix() > claims.ExpiresAt { //如果过期时间小于一天,则更新客户端token
|
||||||
|
c.Set("token", new_token)
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
controller/middleware/ws.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Cors() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
log.Println("Ws Md")
|
||||||
|
method := c.Request.Method //请求方法
|
||||||
|
origin := c.Request.Header.Get("Origin") //请求头部
|
||||||
|
var headerKeys []string // 声明请求头keys
|
||||||
|
for k, _ := range c.Request.Header {
|
||||||
|
headerKeys = append(headerKeys, k)
|
||||||
|
}
|
||||||
|
headerStr := strings.Join(headerKeys, ", ")
|
||||||
|
if headerStr != "" {
|
||||||
|
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
|
||||||
|
} else {
|
||||||
|
headerStr = "access-control-allow-origin, access-control-allow-headers"
|
||||||
|
}
|
||||||
|
if origin != "" {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
|
||||||
|
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
|
||||||
|
// header的类型
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
|
||||||
|
// 允许跨域设置 可以返回其他子段
|
||||||
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
|
||||||
|
c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒
|
||||||
|
c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true
|
||||||
|
c.Set("content-type", "application/json") // 设置返回格式是json
|
||||||
|
}
|
||||||
|
|
||||||
|
//放行所有OPTIONS方法
|
||||||
|
if method == "OPTIONS" {
|
||||||
|
c.JSON(http.StatusOK, "Options Request!")
|
||||||
|
}
|
||||||
|
// 处理请求
|
||||||
|
c.Next() // 处理请求
|
||||||
|
}
|
||||||
|
}
|
||||||
30
controller/send.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/errcode"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Send(c *gin.Context) {
|
||||||
|
var resp Apiform.Resp
|
||||||
|
var send Apiform.Send
|
||||||
|
resp.Code = errcode.C_phone_err
|
||||||
|
resp.Msg = "手机号未提交!"
|
||||||
|
if c.ShouldBind(&send) == nil {
|
||||||
|
if common.VerifyMobileFormat(send.Phone) {
|
||||||
|
if err := send.SendCaptcha(c.ClientIP()); err != nil {
|
||||||
|
resp.Code = errcode.S_send_err
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_nil_err
|
||||||
|
resp.Msg = "发送成功!"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Code = errcode.C_phone_err
|
||||||
|
resp.Msg = "手机号验证失败!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
130
controller/term.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/common/core"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/model/Apiform"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upGrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024 * 1024 * 10,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auth_msg struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle webSocket connection.
|
||||||
|
// first,we establish a ssh connection to ssh server when a webSocket comes;
|
||||||
|
// then we deliver ssh data via ssh connection between browser and ssh server.
|
||||||
|
// That is, read webSocket data from browser (e.g. 'ls' command) and send data to ssh server via ssh connection;
|
||||||
|
// the other hand, read returned ssh data from ssh server and write back to browser via webSocket API.
|
||||||
|
func WsSsh(c *gin.Context) {
|
||||||
|
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if core.HandleError(c, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer wsConn.Close()
|
||||||
|
|
||||||
|
cols, err := strconv.Atoi(c.DefaultQuery("cols", "120"))
|
||||||
|
if core.WshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := strconv.Atoi(c.DefaultQuery("rows", "32"))
|
||||||
|
if core.WshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ser_info Apiform.SerInfo //接收反序列化数据
|
||||||
|
var auth Apiform.WsAuth
|
||||||
|
|
||||||
|
if c.ShouldBindUri(&auth) != nil{
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage,[]byte("参数错误\r\n"))
|
||||||
|
wsConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, wsData, err := wsConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
wsConn.Close()
|
||||||
|
//logrus.WithError(err).Error("reading webSocket message failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//unmashal bytes into struct
|
||||||
|
msgObj := Auth_msg{}
|
||||||
|
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||||
|
log.Println("Auth : unmarshal websocket message failed:", string(wsData))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token := msgObj.Token
|
||||||
|
claims, err := common.ParseToken(token)
|
||||||
|
valid := claims.Valid()
|
||||||
|
if valid != nil || err != nil {
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("身份验证失败\r\n"))
|
||||||
|
wsConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache := database.Cache.Get()
|
||||||
|
defer cache.Close()
|
||||||
|
//log.Println(auth)
|
||||||
|
s_info,err := redis.Bytes(cache.Do("GET", auth.Sid))
|
||||||
|
//log.Println(string(s_info))
|
||||||
|
if err != nil || len(s_info) == 0{
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("连接超时,请重试!\r\n"))
|
||||||
|
wsConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if json.Unmarshal(s_info,&ser_info) != nil{
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("服务器信息获取失败,请重试!\r\n"))
|
||||||
|
wsConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//log.Println(ser_info)
|
||||||
|
if claims.Userid != ser_info.BindUser{ //验证权限
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("权限验证失败,请重试!\r\n"))
|
||||||
|
wsConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
//break
|
||||||
|
}
|
||||||
|
client, err := core.NewSshClient(core.Server{ser_info.Ip, ser_info.Port, ser_info.Username, ser_info.Password})
|
||||||
|
if core.WshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
//startTime := time.Now()
|
||||||
|
ssConn, err := core.NewSshConn(cols, rows, client)
|
||||||
|
|
||||||
|
if core.WshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ssConn.Close()
|
||||||
|
|
||||||
|
quitChan := make(chan bool, 2)
|
||||||
|
|
||||||
|
// most messages are ssh output, not webSocket input
|
||||||
|
go ssConn.ReceiveWsMsg(wsConn, quitChan)
|
||||||
|
go ssConn.SendComboOutput(wsConn, quitChan)
|
||||||
|
//go ssConn.SessionWait(quitChan)
|
||||||
|
|
||||||
|
<-quitChan //任意协程退出则结束
|
||||||
|
fmt.Println("Exit")
|
||||||
|
log.Println("websocket finished")
|
||||||
|
}
|
||||||
43
database/cache.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
redigo "github.com/garyburd/redigo/redis"
|
||||||
|
"ssh_manage/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var redis_conf = config.Config.Redis
|
||||||
|
|
||||||
|
var Cache *redigo.Pool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var addr = fmt.Sprintf("%s:%d",redis_conf.Host,redis_conf.Port)
|
||||||
|
var password = redis_conf.Password
|
||||||
|
Cache = poolInitRedis(addr, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolInitRedis(server string, password string) *redigo.Pool {
|
||||||
|
return &redigo.Pool{
|
||||||
|
MaxIdle: 2, //空闲数
|
||||||
|
IdleTimeout: 240 * time.Second,
|
||||||
|
MaxActive: redis_conf.Poolsize, //最大数
|
||||||
|
Dial: func() (redigo.Conn, error) {
|
||||||
|
c, err := redigo.Dial("tcp", server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
},
|
||||||
|
TestOnBorrow: func(c redigo.Conn, t time.Time) error {
|
||||||
|
_, err := c.Do("PING")
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
42
database/conn.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
|
"log"
|
||||||
|
"ssh_manage/config"
|
||||||
|
"ssh_manage/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dbconf = config.Config.Database
|
||||||
|
|
||||||
|
var pool *sqlpool
|
||||||
|
|
||||||
|
type Mydb struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pool = newpool(newDb, dbconf.Poolsize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDb() *gorm.DB {
|
||||||
|
db, err := gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",dbconf.Username,dbconf.Password,dbconf.Host,dbconf.Port,dbconf.Dbname))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("db open err :%s", err.Error())
|
||||||
|
}
|
||||||
|
if !db.HasTable(&model.User{}){
|
||||||
|
log.Println("init table")
|
||||||
|
db.CreateTable(&model.Server{},&model.User{})
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Mydb) Close() {
|
||||||
|
pool.put(s.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() *Mydb {
|
||||||
|
return &Mydb{pool.get()}
|
||||||
|
}
|
||||||
46
database/pool.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
//type pool interface {
|
||||||
|
// newpool(newdb func()*gorm.DB,size int) *sqlpool
|
||||||
|
// get() (db *gorm.DB)
|
||||||
|
// put(db *gorm.DB)
|
||||||
|
//}
|
||||||
|
|
||||||
|
type sqlpool struct {
|
||||||
|
new func() *gorm.DB
|
||||||
|
db []*gorm.DB
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newpool(newdb func() *gorm.DB, size int) *sqlpool {
|
||||||
|
return &sqlpool{newdb, make([]*gorm.DB, 0, size), sync.Mutex{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlpool) get() (db *gorm.DB) {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
//log.Printf("before len:%d", len(s.db))
|
||||||
|
if len(s.db) > 0 {
|
||||||
|
db = s.db[len(s.db)-1]
|
||||||
|
s.db = s.db[:len(s.db)-1]
|
||||||
|
} else {
|
||||||
|
db = s.new()
|
||||||
|
}
|
||||||
|
//log.Printf("after len:%d", len(s.db))
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlpool) put(db *gorm.DB) {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
if len(s.db) < cap(s.db) {
|
||||||
|
s.db = append(s.db, db)
|
||||||
|
} else {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
13
errcode/Err.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package errcode
|
||||||
|
|
||||||
|
const (
|
||||||
|
C_nil_err = 200 //成功
|
||||||
|
C_from_err = 100 //表单错误
|
||||||
|
C_phone_err = 101 //手机号错误
|
||||||
|
C_code_err = 102 //验证码错误
|
||||||
|
S_send_err = 300 //发送验证码错误
|
||||||
|
S_auth_err = 301 //Token权限校验失败
|
||||||
|
S_auth_fmt_err = 302 //Header Token格式错误
|
||||||
|
S_Verify_err = 303 //验证码校验失败
|
||||||
|
S_Db_err = 304
|
||||||
|
)
|
||||||
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module ssh_manage
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
github.com/Gre-Z/common v0.0.0-20191024025434-2dbc6bd196f9
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.623
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/garyburd/redigo v1.6.2
|
||||||
|
github.com/gin-gonic/gin v1.6.3
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
github.com/jinzhu/gorm v1.9.16
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
github.com/satori/go.uuid v1.2.0
|
||||||
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd
|
||||||
|
)
|
||||||
113
go.sum
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Gre-Z/common v0.0.0-20191024025434-2dbc6bd196f9 h1:q+nKJ4E5tIiGVcXcRqwPxjxRmGHtC+wWohKHtCHzJmE=
|
||||||
|
github.com/Gre-Z/common v0.0.0-20191024025434-2dbc6bd196f9/go.mod h1:b5J9o16Ec1LAwq6QVv+pNECF3sxcin2dMKprGZX43mA=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.623 h1:iKyKSF67tLwsVSs6okuvVdr9C9kV7y4OGNTCx16IKfg=
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.623/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
|
github.com/garyburd/redigo v1.6.2 h1:yE/pwKCrbLpLpQICzYTeZ7JsTA/C53wFTJHaEtRqniM=
|
||||||
|
github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||||
|
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||||
|
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||||
|
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
|
||||||
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||||
|
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
117
model/Apiform/Api.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package Apiform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
"log"
|
||||||
|
"ssh_manage/common"
|
||||||
|
"ssh_manage/database"
|
||||||
|
"ssh_manage/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Login struct {
|
||||||
|
Phone int `form:"phone" binding:"required"`
|
||||||
|
Code string `form:"code" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Send struct {
|
||||||
|
Phone string `form:"phone" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Slist struct {
|
||||||
|
Page int `form:"page" binding:"required"`
|
||||||
|
Limit int `form:"limit" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type List_resp struct {
|
||||||
|
List []model.Server
|
||||||
|
Count uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTerm struct {
|
||||||
|
ID uint `form:"id" binding:"required"`
|
||||||
|
Password string `form:"setpass" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WsAuth struct {
|
||||||
|
Sid string `uri:"sid" binding:"required,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Edit struct {
|
||||||
|
ID uint `form:"id" binding:"required"`
|
||||||
|
Nickname string `form:"nickname"`
|
||||||
|
Ip string `form:"ip"`
|
||||||
|
Port int `form:"port"`
|
||||||
|
Username string `form:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditPass struct {
|
||||||
|
ID uint `form:"id" binding:"required"`
|
||||||
|
Password string `form:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Addser struct {
|
||||||
|
Nickname string `form:"nickname"`
|
||||||
|
Ip string `form:"ip" binding:"required"`
|
||||||
|
Port int `form:"port" binding:"required"`
|
||||||
|
Username string `form:"username" binding:"required"`
|
||||||
|
Password string `form:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerInfo struct {
|
||||||
|
ID uint
|
||||||
|
Ip string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
BindUser uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Send) SendCaptcha(ip string) (err error) {
|
||||||
|
cache := database.Cache.Get()
|
||||||
|
defer cache.Close()
|
||||||
|
ipexists, _ := redis.Bool(cache.Do("EXISTS", ip))
|
||||||
|
phoneexists, _ := redis.Bool(cache.Do("EXISTS", s.Phone+"_time"))
|
||||||
|
if ipexists || phoneexists {
|
||||||
|
err = errors.New("请勿频繁发送验证码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache.Send("MULTI") //开启事务操作
|
||||||
|
cache.Send("SETEX", s.Phone+"_time", 60*2, nil) //记录手机号与IP,防止重复发送
|
||||||
|
cache.Send("SETEX", ip, 60*2, nil)
|
||||||
|
capcha, err := common.Sendsms(s.Phone)
|
||||||
|
if err != nil {
|
||||||
|
cache.Do("DISCARD") //发送失败则取消事务
|
||||||
|
log.Println(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache.Send("SETEX", s.Phone, 60*5, capcha) //延长过期时间,用于校验
|
||||||
|
cache.Do("EXEC") //提交事务
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Login) Verify() (key, code string) {
|
||||||
|
return fmt.Sprintf("%d", l.Phone), l.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GetTerm) Decode(server model.Server) (sid string, err error) {
|
||||||
|
sid = uuid.Must(uuid.NewV4(), nil).String()
|
||||||
|
//log.Println(server)
|
||||||
|
s_pass := common.AesDecryptCBC(server.Password, []byte(t.Password))
|
||||||
|
if s_pass == "" {
|
||||||
|
return "", errors.New("秘钥验证失败")
|
||||||
|
} else {
|
||||||
|
var serinfo = SerInfo{server.ID,server.Ip,server.Port,server.Username,s_pass,server.BindUser}
|
||||||
|
//server.Password = s_pass //用于建立连接
|
||||||
|
cache := database.Cache.Get()
|
||||||
|
defer cache.Close()
|
||||||
|
s_info, _ := json.Marshal(serinfo)
|
||||||
|
//log.Println(string(s_info))
|
||||||
|
cache.Do("SETEX", sid, 10, s_info) //缓存10s,用于建立连接和验证权限
|
||||||
|
//log.Println(sid)
|
||||||
|
}
|
||||||
|
return sid, nil
|
||||||
|
}
|
||||||
8
model/Apiform/Response.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package Apiform
|
||||||
|
|
||||||
|
type Resp struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
12
model/Model.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Gre-Z/common/jtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
ID uint `gorm:"primary_key"`
|
||||||
|
CreatedAt jtime.JsonTime
|
||||||
|
UpdatedAt jtime.JsonTime
|
||||||
|
DeletedAt jtime.JsonTime `sql:"index"`
|
||||||
|
}
|
||||||
16
model/Server.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Gre-Z/common/jtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Model
|
||||||
|
Nickname string
|
||||||
|
Ip string `grom:"size:15"`
|
||||||
|
Port int
|
||||||
|
Username string `grom:"size:255"`
|
||||||
|
Password string `gorm:"type:longtext" json:"-"` //存放加密后的登录密码,解密密码存放到用户浏览器
|
||||||
|
BindUser uint `json:"-"`
|
||||||
|
BeforeTime jtime.JsonTime
|
||||||
|
}
|
||||||
8
model/User.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Model
|
||||||
|
Phone int `gorm:"not null;unique;type:bigint"`
|
||||||
|
Email string `gorm:"unique"`
|
||||||
|
Servers []Server `gorm:"ForeignKey:BindUser"`
|
||||||
|
}
|
||||||
68
serve.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"ssh_manage/config"
|
||||||
|
_ "ssh_manage/config"
|
||||||
|
"ssh_manage/controller"
|
||||||
|
"ssh_manage/controller/middleware"
|
||||||
|
_ "ssh_manage/database" //初始化Mysql/Redis连接池
|
||||||
|
)
|
||||||
|
|
||||||
|
var run_mode = config.Config.Web.Model
|
||||||
|
var web_port = config.Config.Web.Port
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
gin.SetMode(run_mode)
|
||||||
|
gin.DisableConsoleColor()
|
||||||
|
router := gin.Default()
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
router.LoadHTMLGlob("view/*")
|
||||||
|
router.Static("/static", "./static")
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.Redirect(302, "/login")
|
||||||
|
})
|
||||||
|
router.GET("/login", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "index.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/console", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "console.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/servers", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "s_list.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/add", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "add.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/setpass", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "reset.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/openterm", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "open_term.html", nil)
|
||||||
|
})
|
||||||
|
router.GET("/term", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "term.html", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
api := router.Group("/v1")
|
||||||
|
{
|
||||||
|
api.POST("/login", controller.Login)
|
||||||
|
api.POST("/send", controller.Send)
|
||||||
|
api.GET("/term/:sid", controller.WsSsh)
|
||||||
|
api.Use(middleware.Auth()).GET("/userinfo", controller.Info)
|
||||||
|
api.Use(middleware.Auth()).POST("/nickname", controller.UpdataNick)
|
||||||
|
api.Use(middleware.Auth()).POST("/addser", controller.Addser)
|
||||||
|
api.Use(middleware.Auth()).POST("/repass", controller.Resetpass)
|
||||||
|
api.Use(middleware.Auth()).POST("/delete", controller.Del)
|
||||||
|
api.Use(middleware.Auth()).POST("/getterm", controller.GetTerm)
|
||||||
|
}
|
||||||
|
if err := router.Run(web_port); err != nil {
|
||||||
|
log.Panicf("Web Serve Start Err : %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
364
static/assets/css/admin.css
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
/*
|
||||||
|
* 自定义管理界面,基于LayUI框架构建
|
||||||
|
* Layui link: http://www.layui.com
|
||||||
|
* Custom style author: Bie Jun
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 字体图标
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {font-family: "AI";
|
||||||
|
src: url('../font/ai.eot?t=1522660080732'); /* IE9*/
|
||||||
|
src: url('../font/ai.eot?t=1522660080732#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
|
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
|
||||||
|
url('../font/ai.ttf?t=1522660080732') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
|
url('../font/ai.svg?t=1522660080732#AI') format('svg'); /* iOS 4.1- */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai {
|
||||||
|
font-family:"AI" !important;
|
||||||
|
font-size:16px;
|
||||||
|
font-style:normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-enter:before { content: "\e766"; }
|
||||||
|
.ai-menufold:before { content: "\e636"; }
|
||||||
|
.ai-menuunfold:before { content: "\e6c0"; }
|
||||||
|
|
||||||
|
|
||||||
|
/* 预定义整体布局配色,你可以修改这里的色值为自己喜欢的颜色 */
|
||||||
|
|
||||||
|
.layui-layout-body{
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-header{
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-side,
|
||||||
|
.layui-nav,
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item.layui-nav-itemed > a,
|
||||||
|
.layui-nav-tree .layui-nav-item a:hover{
|
||||||
|
background-color: #001529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-child,
|
||||||
|
.custom-admin .layui-nav-child a{
|
||||||
|
background-color: #000c17!important;
|
||||||
|
}
|
||||||
|
.custom-admin .layui-nav-child dd.layui-this a{
|
||||||
|
background-color: #177ce3;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
background-image: -moz-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -webkit-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -o-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav,
|
||||||
|
.custom-header .layui-nav .layui-nav-item a{
|
||||||
|
color: rgba(0,0,0,.8);
|
||||||
|
}
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item a{
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
transition: all .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item a:hover,
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item a:hover .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-itemed > a .layui-nav-more:before{
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item a > i{
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-tree .layui-nav-item a > em{
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-footer{
|
||||||
|
background-color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠边栏 */
|
||||||
|
|
||||||
|
.layui-layout.fold-side-bar .layui-side.custom-admin,
|
||||||
|
.layui-layout.fold-side-bar .layui-side.custom-admin .layui-nav-tree{
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout.fold-side-bar .layui-side.custom-admin .layui-side-scroll{
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout.fold-side-bar .custom-header .layui-nav.layui-layout-left,
|
||||||
|
.layui-layout.fold-side-bar .layui-body,
|
||||||
|
.layui-layout.fold-side-bar .layui-footer{
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-side-bar .custom-admin .custom-logo img{
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-side-bar .layui-side.custom-admin .layui-nav-more,
|
||||||
|
.fold-side-bar .custom-admin .layui-nav-item a > em,
|
||||||
|
.fold-side-bar .custom-admin .custom-logo h1,
|
||||||
|
.fold-side-bar .layui-side.custom-admin .layui-nav-child{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局样式重定义 */
|
||||||
|
|
||||||
|
html, body , .app-container{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout{
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left:0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container .layui-tab-content{
|
||||||
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container .layui-tab-content .layui-tab-item,
|
||||||
|
.app-container .layui-tab-item > iframe{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.custom-header{
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tab{
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: rgba(0, 21, 41, 0.08) 0px 1px 4px;
|
||||||
|
border:none;
|
||||||
|
border-top:1px solid #f6f6f6;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-tab-title.custom-tab .layui-this{
|
||||||
|
background-color: #f0f4fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-tab-title.custom-tab .layui-this:after{
|
||||||
|
border: none;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #177ce3;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
background-image: -moz-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -webkit-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -o-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-side.custom-admin,
|
||||||
|
.layui-layout-admin .layui-side.custom-admin .layui-nav-tree{
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-side.custom-admin .layui-side-scroll{
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-body,
|
||||||
|
.layui-layout-admin .layui-footer,
|
||||||
|
.layui-layout-admin .custom-header .layui-nav.layui-layout-left{
|
||||||
|
left: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-side.custom-admin{
|
||||||
|
top: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||||
|
box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-item{
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav-child{
|
||||||
|
top: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-itemed .layui-nav-child dd a{
|
||||||
|
padding-left: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-body{
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layout-admin .layui-footer{
|
||||||
|
border-top: 1px solid #dfdfdf;
|
||||||
|
}
|
||||||
|
.layui-layout-admin .layui-footer p{
|
||||||
|
color: rgba(0,0,0,.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-side.custom-admin,
|
||||||
|
.layui-body,
|
||||||
|
.custom-header,
|
||||||
|
.custom-header .layui-nav.layui-layout-left,
|
||||||
|
.layui-footer{
|
||||||
|
transition: all .3s;
|
||||||
|
-webkit-transition: all .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px){
|
||||||
|
.layui-layout-admin .layui-side.custom-admin {
|
||||||
|
transform: translate3d(-265px,0,0);
|
||||||
|
-webkit-transform: translate3d(-265px,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-body,
|
||||||
|
.layui-layout-admin .layui-footer,
|
||||||
|
.layui-layout-admin .custom-header .layui-nav.layui-layout-left{
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-side-bar-xs .layui-side.custom-admin{
|
||||||
|
-webkit-transform: translate(0,0,0);
|
||||||
|
transform: translate3d(0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-side-bar-xs .layui-body,
|
||||||
|
.fold-side-bar-xs .layui-footer,
|
||||||
|
.fold-side-bar-xs .custom-header{
|
||||||
|
transform: translate3d(260px,0,0);
|
||||||
|
-webkit-transform: translate3d(260px,0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-more,
|
||||||
|
.custom-admin .layui-nav .layui-nav-more{
|
||||||
|
width: 25px;
|
||||||
|
top: 0;
|
||||||
|
border:none;
|
||||||
|
overflow: visible;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-more{
|
||||||
|
top: 0;
|
||||||
|
right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .slide-sidebar a.icon-font{
|
||||||
|
padding: 0;
|
||||||
|
color: #2c2e2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .slide-sidebar a.icon-font > i{
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav-itemed .layui-nav-more:before{
|
||||||
|
font-family: layui-icon!important;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6d747a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav-itemed .layui-nav-more:before{
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav .layui-nav-more:before,
|
||||||
|
.custom-admin .layui-nav .layui-nav-more:before{
|
||||||
|
content: '\e61a';
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-admin .layui-nav-itemed > a .layui-nav-more:before{
|
||||||
|
content: '\e619';
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo{
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #002140;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.custom-logo img{
|
||||||
|
margin-left: 20px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo h1{
|
||||||
|
display: inline;
|
||||||
|
margin: 0 20px 0 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-nav-bar{
|
||||||
|
height: 3px;
|
||||||
|
background-color: #177ce3;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
background-image: -moz-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -webkit-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -o-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-nav-bar{
|
||||||
|
top: 0!important;
|
||||||
|
}
|
||||||
|
.layui-nav-tree .layui-nav-bar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header .layui-tab-title li
|
||||||
|
|
||||||
|
.custom-header .layui-tab-title li.layui-this .layui-tab-close{
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-mask{
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0,0,0,.3);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
2
static/assets/css/layui.css
Normal file
2
static/assets/css/layui.mobile.css
Normal file
163
static/assets/css/login.css
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
html,body{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {font-family: "AI";
|
||||||
|
src: url('../font/ai.eot?t=1522660080732'); /* IE9*/
|
||||||
|
src: url('../font/ai.eot?t=1522660080732#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
|
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
|
||||||
|
url('../font/ai.ttf?t=1522660080732') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
|
url('../font/ai.svg?t=1522660080732#AI') format('svg'); /* iOS 4.1- */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai {
|
||||||
|
font-family:"AI" !important;
|
||||||
|
font-size:16px;
|
||||||
|
font-style:normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-enter:before { content: "\e766"; }
|
||||||
|
|
||||||
|
.login-wrap{
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container{
|
||||||
|
width: 100%;
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form{
|
||||||
|
width: 320px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form .input-group{
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.login-form .input-group .input-field{
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 3px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 15px;
|
||||||
|
display: block;
|
||||||
|
border:none;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form .input-group .input-label{
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: left;
|
||||||
|
cursor: text;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form .input-group .input-label > .label-title{
|
||||||
|
padding: 17px 8px;
|
||||||
|
color: #5d6779;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form .input-group .input-label:before{
|
||||||
|
position: absolute;
|
||||||
|
content:'';
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
border-bottom:1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form .input-group .input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.login-form .input-group .input-field:focus + .input-label > .label-title,
|
||||||
|
.input-group.field-focus .input-label > .label-title {
|
||||||
|
-webkit-animation: fieldUp 0.3s forwards;
|
||||||
|
animation: fieldUp 0.3s forwards;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button{
|
||||||
|
position: relative;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding:8px;
|
||||||
|
background-color: #29adeb;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
background-image: -moz-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -webkit-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -o-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
box-shadow: 0px 7px 10px 0px rgba(23, 124, 227, 0.25);
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
border:none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: padding-right .4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button i{
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover{
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
.login-button:hover i{
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:focus{
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fieldUp {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
static/assets/css/modules/code.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** layui-v2.5.6 MIT License By https://www.layui.com */
|
||||||
|
html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #e2e2e2;border-left-width:6px;background-color:#F2F2F2;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:32px;line-height:32px;border-bottom:1px solid #e2e2e2}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 5px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}
|
||||||
2
static/assets/css/modules/laydate/default/laydate.css
Normal file
BIN
static/assets/css/modules/layer/default/icon-ext.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
static/assets/css/modules/layer/default/icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
2
static/assets/css/modules/layer/default/layer.css
Normal file
BIN
static/assets/css/modules/layer/default/loading-0.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
static/assets/css/modules/layer/default/loading-1.gif
Normal file
|
After Width: | Height: | Size: 701 B |
BIN
static/assets/css/modules/layer/default/loading-2.gif
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
136
static/assets/css/view.css
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
.layui-view-body{
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-content{
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-tab-title{
|
||||||
|
border-bottom-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
.layui-card .layui-tab-brief .layui-tab-title li{
|
||||||
|
margin:0 15px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-form-checked i, .layui-form-checked:hover i,
|
||||||
|
.layui-form-radio>i:hover, .layui-form-radioed>i,
|
||||||
|
.layui-breadcrumb a:hover,
|
||||||
|
.layui-laypage a:hover,
|
||||||
|
.layui-tab-brief>.layui-tab-title .layui-this{
|
||||||
|
color: #177ce3!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-btn-primary:hover,
|
||||||
|
.layui-form-onswitch,
|
||||||
|
.layui-form-checked[lay-skin=primary] i,
|
||||||
|
.layui-form-checkbox[lay-skin=primary]:hover i,
|
||||||
|
.layui-form-checked, .layui-form-checked:hover,
|
||||||
|
.layui-tab-brief>.layui-tab-more li.layui-this:after,
|
||||||
|
.layui-tab-brief>.layui-tab-title .layui-this:after{
|
||||||
|
border-color: #177ce3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-checkbox-disbaled[lay-skin=primary]:hover i {
|
||||||
|
border-color: #d2d2d2!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-form-onswitch,
|
||||||
|
.layui-form-checked[lay-skin=primary] i,
|
||||||
|
.layui-form-select dl dd.layui-this,
|
||||||
|
.layui-laypage .layui-laypage-curr .layui-laypage-em,
|
||||||
|
.layui-form-checked span, .layui-form-checked:hover span{
|
||||||
|
background-color: #177ce3;
|
||||||
|
}
|
||||||
|
.layui-btn-blue{
|
||||||
|
background-color: #177ce3;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
background-image: -moz-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -webkit-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: -o-linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
background-image: linear-gradient(left,#29adeb,#177ce3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-form-checkbox[lay-skin=primary]:hover span{
|
||||||
|
background: 0 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-page-header{
|
||||||
|
margin: -20px -20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-page-header .pagewrap{
|
||||||
|
padding: 15px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-page-header .title{
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card{
|
||||||
|
padding: 20px 24px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .chart-header{
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .metawrap{
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .metawrap .meta{
|
||||||
|
color: rgba(0,0,0,.45);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .metawrap .total{
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .chart-body{
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .chart-footer{
|
||||||
|
padding-top: 9px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .field{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .field span{
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card .field span:last-child{
|
||||||
|
margin-left: 8px;
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box{
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
BIN
static/assets/font/ai.eot
Normal file
42
static/assets/font/ai.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<!--
|
||||||
|
2013-9-30: Created.
|
||||||
|
-->
|
||||||
|
<svg>
|
||||||
|
<metadata>
|
||||||
|
Created by iconfont
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
<font id="AI" horiz-adv-x="1024" >
|
||||||
|
<font-face
|
||||||
|
font-family="AI"
|
||||||
|
font-weight="500"
|
||||||
|
font-stretch="normal"
|
||||||
|
units-per-em="1024"
|
||||||
|
ascent="896"
|
||||||
|
descent="-128"
|
||||||
|
/>
|
||||||
|
<missing-glyph />
|
||||||
|
|
||||||
|
<glyph glyph-name="x" unicode="x" horiz-adv-x="1001"
|
||||||
|
d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
|
||||||
|
t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
|
||||||
|
t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<glyph glyph-name="arrow-right-circle-s-o" unicode="" d="M480 896c-265.088 0-480-214.912-480-480s214.912-480 480-480 480 214.912 480 480-214.912 480-480 480zM480 0c-229.76 0-416 186.24-416 416s186.24 416 416 416 416-186.24 416-416-186.24-416-416-416zM498.752 596.992l148.992-148.992h-455.744v-64h455.744l-148.992-148.992 45.248-45.312 226.304 226.304-226.304 226.304z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
|
||||||
|
<glyph glyph-name="menufold" unicode="" d="M96.992 695.008h828q15.008 0 25.504 10.496t10.496 25.504-10.496 25.504-25.504 10.496H96.992q-15.008 0-25.504-10.496t-10.496-25.504 10.496-25.504 25.504-10.496z m826.016-245.024H358.016q-15.008 0-25.504-10.496t-10.496-25.504 10.496-25.504 25.504-10.496h564.992q15.008 0 25.504 10.496t10.496 25.504-10.496 25.504-25.504 10.496z m1.984-377.984H100q-15.008 0-25.504-10.496T64 36t10.496-25.504 25.504-10.496h824.992q15.008 0 25.504 10.496t10.496 25.504-10.496 25.504-25.504 10.496zM195.008 280.992q11.008-10.016 24.992-10.016 15.008 0 26.016 11.008 10.016 10.016 10.016 24.992t-10.016 24.992l-96 94.016 94.016 88q11.008 11.008 11.488 26.016t-10.016 25.504-25.504 11.008-24.992-9.504l-120.992-114.016q-12-11.008-12-26.016t11.008-26.016z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
|
||||||
|
<glyph glyph-name="menuunfold" unicode="" d="M924.9 695.2H96.8c-19.9 0-36 16.1-36 36s16.1 36 36 36h828.1c19.9 0 36-16.1 36-36s-16.2-36-36-36z m-826.1-245h565.1c19.9 0 36-16.1 36-36s-16.1-36-36-36H98.8c-19.9 0-36 16.1-36 36s16.1 36 36 36z m-2-378h825.1c19.9 0 36-16.1 36-36s-16.1-36-36-36H96.8c-19.9 0-36 16.1-36 36s16.1 36 36 36zM826.6 281c-7-6.9-16.1-10.3-25.2-10.3-9.3 0-18.6 3.6-25.7 10.8-13.9 14.2-13.8 37 0.4 50.9l95.4 93.8-93.7 88.3c-14.5 13.6-15.2 36.4-1.5 50.9 13.6 14.5 36.4 15.1 50.9 1.5l120.8-114c7.1-6.7 11.2-16 11.3-25.8 0.1-9.8-3.8-19.2-10.8-26.1L826.6 281z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</font>
|
||||||
|
</defs></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/assets/font/ai.ttf
Normal file
BIN
static/assets/font/ai.woff
Normal file
BIN
static/assets/font/iconfont.eot
Normal file
554
static/assets/font/iconfont.svg
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
static/assets/font/iconfont.ttf
Normal file
BIN
static/assets/font/iconfont.woff
Normal file
BIN
static/assets/font/iconfont.woff2
Normal file
BIN
static/assets/images/face/0.gif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
static/assets/images/face/1.gif
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
static/assets/images/face/10.gif
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/assets/images/face/11.gif
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/assets/images/face/12.gif
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/assets/images/face/13.gif
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
static/assets/images/face/14.gif
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
static/assets/images/face/15.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/assets/images/face/16.gif
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
static/assets/images/face/17.gif
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/assets/images/face/18.gif
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/assets/images/face/19.gif
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/assets/images/face/2.gif
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/assets/images/face/20.gif
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/assets/images/face/21.gif
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/assets/images/face/22.gif
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
static/assets/images/face/23.gif
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/assets/images/face/24.gif
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
static/assets/images/face/25.gif
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/assets/images/face/26.gif
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
static/assets/images/face/27.gif
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/assets/images/face/28.gif
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/assets/images/face/29.gif
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/assets/images/face/3.gif
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
static/assets/images/face/30.gif
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/assets/images/face/31.gif
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/assets/images/face/32.gif
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/assets/images/face/33.gif
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/assets/images/face/34.gif
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/assets/images/face/35.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/assets/images/face/36.gif
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
static/assets/images/face/37.gif
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/assets/images/face/38.gif
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/assets/images/face/39.gif
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
static/assets/images/face/4.gif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/assets/images/face/40.gif
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/assets/images/face/41.gif
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/assets/images/face/42.gif
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/assets/images/face/43.gif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
static/assets/images/face/44.gif
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/assets/images/face/45.gif
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/assets/images/face/46.gif
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/assets/images/face/47.gif
Normal file
|
After Width: | Height: | Size: 2.3 KiB |