mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-17 15:10:12 +01:00
❤️ 完整开源界面和内核 https://github.com/siyuan-note/siyuan/issues/5013
This commit is contained in:
parent
e650b8100c
commit
f40ed985e1
1214 changed files with 345766 additions and 9 deletions
318
kernel/server/serve.go
Normal file
318
kernel/server/serve.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/88250/melody"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mssola/user_agent"
|
||||
"github.com/siyuan-note/siyuan/kernel/api"
|
||||
"github.com/siyuan-note/siyuan/kernel/cmd"
|
||||
"github.com/siyuan-note/siyuan/kernel/model"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
var cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
|
||||
|
||||
func Serve(fastMode bool) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
ginServer := gin.New()
|
||||
ginServer.MaxMultipartMemory = 1024 * 1024 * 1024 * 4
|
||||
ginServer.Use(gin.Recovery())
|
||||
ginServer.Use(cors.Default())
|
||||
ginServer.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm"})))
|
||||
|
||||
cookieStore.Options(sessions.Options{
|
||||
Path: "/",
|
||||
Secure: util.SSL,
|
||||
//MaxAge: 60 * 60 * 24 * 7, // 默认是 Session
|
||||
HttpOnly: true,
|
||||
})
|
||||
ginServer.Use(sessions.Sessions("siyuan", cookieStore))
|
||||
|
||||
if "dev" == util.Mode {
|
||||
serveDebug(ginServer)
|
||||
}
|
||||
|
||||
serveAssets(ginServer)
|
||||
serveAppearance(ginServer)
|
||||
serveWebSocket(ginServer)
|
||||
serveExport(ginServer)
|
||||
serveWidgets(ginServer)
|
||||
serveEmojis(ginServer)
|
||||
api.ServeAPI(ginServer)
|
||||
|
||||
var addr string
|
||||
if model.Conf.System.NetworkServe || "docker" == util.Container {
|
||||
addr = "0.0.0.0:" + util.ServerPort
|
||||
} else {
|
||||
addr = "127.0.0.1:" + util.ServerPort
|
||||
}
|
||||
util.LogInfof("kernel is booting [%s]", "http://"+addr)
|
||||
util.HttpServing = true
|
||||
if err := ginServer.Run(addr); nil != err {
|
||||
if !fastMode {
|
||||
util.LogErrorf("boot kernel failed: %s", err)
|
||||
os.Exit(util.ExitCodeUnavailablePort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveExport(ginServer *gin.Engine) {
|
||||
ginServer.Static("/export/", filepath.Join(util.TempDir, "export"))
|
||||
}
|
||||
|
||||
func serveWidgets(ginServer *gin.Engine) {
|
||||
ginServer.Static("/widgets/", filepath.Join(util.DataDir, "widgets"))
|
||||
}
|
||||
|
||||
func serveEmojis(ginServer *gin.Engine) {
|
||||
ginServer.Static("/emojis/", filepath.Join(util.DataDir, "emojis"))
|
||||
}
|
||||
|
||||
func serveAppearance(ginServer *gin.Engine) {
|
||||
siyuan := ginServer.Group("", model.CheckAuth)
|
||||
|
||||
siyuan.Handle("GET", "/", func(c *gin.Context) {
|
||||
userAgentHeader := c.GetHeader("User-Agent")
|
||||
if strings.Contains(userAgentHeader, "Electron") {
|
||||
c.Redirect(302, "/stage/build/app/?r="+gulu.Rand.String(7))
|
||||
return
|
||||
}
|
||||
|
||||
ua := user_agent.New(userAgentHeader)
|
||||
if ua.Mobile() {
|
||||
c.Redirect(302, "/stage/build/mobile/?r="+gulu.Rand.String(7))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, "/stage/build/desktop/?r="+gulu.Rand.String(7))
|
||||
})
|
||||
|
||||
appearancePath := util.AppearancePath
|
||||
if "dev" == util.Mode {
|
||||
appearancePath = filepath.Join(util.WorkingDir, "appearance")
|
||||
}
|
||||
siyuan.GET("/appearance/*filepath", func(c *gin.Context) {
|
||||
filePath := filepath.Join(appearancePath, strings.TrimPrefix(c.Request.URL.Path, "/appearance/"))
|
||||
if strings.HasSuffix(c.Request.URL.Path, "/theme.js") && !gulu.File.IsExist(filePath) {
|
||||
// 主题 js 不存在时生成空内容返回
|
||||
c.Data(200, "application/x-javascript", nil)
|
||||
return
|
||||
}
|
||||
c.File(filePath)
|
||||
})
|
||||
|
||||
siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
|
||||
siyuan.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
|
||||
|
||||
siyuan.GET("/check-auth", serveCheckAuth)
|
||||
}
|
||||
|
||||
func serveCheckAuth(c *gin.Context) {
|
||||
data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html"))
|
||||
if nil != err {
|
||||
util.LogErrorf("load auth page failed: %s", err)
|
||||
c.Status(500)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
||||
}
|
||||
|
||||
func serveAssets(ginServer *gin.Engine) {
|
||||
ginServer.POST("/upload", model.CheckAuth, model.Upload)
|
||||
|
||||
ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) {
|
||||
requestPath := context.Param("path")
|
||||
relativePath := path.Join("assets", requestPath)
|
||||
p, err := model.GetAssetAbsPath(relativePath)
|
||||
if nil != err {
|
||||
context.Status(404)
|
||||
return
|
||||
}
|
||||
http.ServeFile(context.Writer, context.Request, p)
|
||||
return
|
||||
})
|
||||
ginServer.GET("/history/:dir/assets/*name", model.CheckAuth, func(context *gin.Context) {
|
||||
dir := context.Param("dir")
|
||||
name := context.Param("name")
|
||||
relativePath := path.Join(dir, "assets", name)
|
||||
p := filepath.Join(util.WorkspaceDir, "history", relativePath)
|
||||
http.ServeFile(context.Writer, context.Request, p)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func serveDebug(ginServer *gin.Engine) {
|
||||
ginServer.GET("/debug/pprof/", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/allocs", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/block", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/goroutine", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/heap", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/mutex", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/threadcreate", gin.WrapF(pprof.Index))
|
||||
ginServer.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
|
||||
ginServer.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
|
||||
ginServer.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
|
||||
ginServer.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
|
||||
}
|
||||
|
||||
func serveWebSocket(ginServer *gin.Engine) {
|
||||
util.WebSocketServer.Config.MaxMessageSize = 1024 * 1024 * 8
|
||||
if "docker" == util.Container { // Docker 容器运行时启用 WebSocket 传输压缩
|
||||
util.WebSocketServer.Config.EnableCompression = true
|
||||
util.WebSocketServer.Config.CompressionLevel = 4
|
||||
}
|
||||
|
||||
ginServer.GET("/ws", func(c *gin.Context) {
|
||||
if err := util.WebSocketServer.HandleRequest(c.Writer, c.Request); nil != err {
|
||||
util.LogErrorf("handle command failed: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandlePong(func(session *melody.Session) {
|
||||
//model.Logger.Debugf("pong")
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandleConnect(func(s *melody.Session) {
|
||||
//util.LogInfof("ws check auth for [%s]", s.Request.RequestURI)
|
||||
authOk := true
|
||||
|
||||
if "" != model.Conf.AccessAuthCode {
|
||||
session, err := cookieStore.Get(s.Request, "siyuan")
|
||||
if nil != err {
|
||||
authOk = false
|
||||
util.LogErrorf("get cookie failed: %s", err)
|
||||
} else {
|
||||
val := session.Values["data"]
|
||||
if nil == val {
|
||||
authOk = false
|
||||
} else {
|
||||
sess := map[string]interface{}{}
|
||||
err = gulu.JSON.UnmarshalJSON([]byte(val.(string)), &sess)
|
||||
if nil != err {
|
||||
authOk = false
|
||||
util.LogErrorf("unmarshal cookie failed: %s", err)
|
||||
} else {
|
||||
authOk = sess["AccessAuthCode"].(string) == model.Conf.AccessAuthCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !authOk {
|
||||
s.CloseWithMsg([]byte(" unauthenticated"))
|
||||
//util.LogWarnf("closed a unauthenticated session [%s]", util.GetRemoteAddr(s))
|
||||
return
|
||||
}
|
||||
|
||||
util.AddPushChan(s)
|
||||
//sessionId, _ := s.Get("id")
|
||||
//util.LogInfof("ws [%s] connected", sessionId)
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandleDisconnect(func(s *melody.Session) {
|
||||
util.RemovePushChan(s)
|
||||
//sessionId, _ := s.Get("id")
|
||||
//model.Logger.Debugf("ws [%s] disconnected", sessionId)
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandleError(func(s *melody.Session, err error) {
|
||||
//sessionId, _ := s.Get("id")
|
||||
//util.LogDebugf("ws [%s] failed: %s", sessionId, err)
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandleClose(func(s *melody.Session, i int, str string) error {
|
||||
//sessionId, _ := s.Get("id")
|
||||
//util.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str)
|
||||
return nil
|
||||
})
|
||||
|
||||
util.WebSocketServer.HandleMessage(func(s *melody.Session, msg []byte) {
|
||||
start := time.Now()
|
||||
util.LogTracef("request [%s]", shortReqMsg(msg))
|
||||
request := map[string]interface{}{}
|
||||
if err := gulu.JSON.UnmarshalJSON(msg, &request); nil != err {
|
||||
result := util.NewResult()
|
||||
result.Code = -1
|
||||
result.Msg = "Bad Request"
|
||||
responseData, _ := gulu.JSON.MarshalJSON(result)
|
||||
s.Write(responseData)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := s.Get("app"); !ok {
|
||||
result := util.NewResult()
|
||||
result.Code = -1
|
||||
result.Msg = "Bad Request"
|
||||
s.Write(result.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
cmdStr := request["cmd"].(string)
|
||||
cmdId := request["reqId"].(float64)
|
||||
param := request["param"].(map[string]interface{})
|
||||
command := cmd.NewCommand(cmdStr, cmdId, param, s)
|
||||
if nil == command {
|
||||
result := util.NewResult()
|
||||
result.Code = -1
|
||||
result.Msg = "can not find command [" + cmdStr + "]"
|
||||
s.Write(result.Bytes())
|
||||
return
|
||||
}
|
||||
if util.ReadOnly && !command.IsRead() {
|
||||
result := util.NewResult()
|
||||
result.Code = -1
|
||||
result.Msg = model.Conf.Language(34)
|
||||
s.Write(result.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
end := time.Now()
|
||||
util.LogTracef("parse cmd [%s] consumed [%d]ms", command.Name(), end.Sub(start).Milliseconds())
|
||||
|
||||
cmd.Exec(command)
|
||||
})
|
||||
}
|
||||
|
||||
func shortReqMsg(msg []byte) []byte {
|
||||
s := gulu.Str.FromBytes(msg)
|
||||
max := 128
|
||||
if len(s) > max {
|
||||
count := 0
|
||||
for i := range s {
|
||||
count++
|
||||
if count > max {
|
||||
return gulu.Str.ToBytes(s[:i] + "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue