项目开源

This commit is contained in:
OldCat 2020-11-16 16:41:02 +08:00
parent ad2426548e
commit de893826ae
182 changed files with 4228 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
package common

33
common/core/helper.go Normal file
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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()
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

163
static/assets/css/login.css Normal file
View 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%);
}
}

View 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}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

136
static/assets/css/view.css Normal file
View 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

Binary file not shown.

42
static/assets/font/ai.svg Normal file
View 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="&#59238;" 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="&#58934;" 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="&#59072;" 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

Binary file not shown.

BIN
static/assets/font/ai.woff Normal file

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Some files were not shown because too many files have changed in this diff Show More