// 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 . 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 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023 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 }