serve.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package server
  17. import (
  18. "net/http"
  19. "net/http/pprof"
  20. "os"
  21. "path"
  22. "path/filepath"
  23. "strings"
  24. "time"
  25. "github.com/88250/gulu"
  26. "github.com/88250/melody"
  27. "github.com/gin-contrib/gzip"
  28. "github.com/gin-contrib/sessions"
  29. "github.com/gin-contrib/sessions/cookie"
  30. "github.com/gin-gonic/gin"
  31. "github.com/mssola/user_agent"
  32. "github.com/siyuan-note/logging"
  33. "github.com/siyuan-note/siyuan/kernel/api"
  34. "github.com/siyuan-note/siyuan/kernel/cmd"
  35. "github.com/siyuan-note/siyuan/kernel/model"
  36. "github.com/siyuan-note/siyuan/kernel/util"
  37. )
  38. var cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
  39. func CORSMiddleware() gin.HandlerFunc {
  40. return func(c *gin.Context) {
  41. c.Header("Access-Control-Allow-Origin", "*")
  42. c.Header("Access-Control-Allow-Credentials", "true")
  43. c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
  44. c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
  45. if c.Request.Method == "OPTIONS" {
  46. c.AbortWithStatus(204)
  47. return
  48. }
  49. c.Next()
  50. }
  51. }
  52. func Serve(fastMode bool) {
  53. gin.SetMode(gin.ReleaseMode)
  54. ginServer := gin.New()
  55. ginServer.MaxMultipartMemory = 1024 * 1024 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023
  56. ginServer.Use(gin.Recovery())
  57. // 跨域支持验证
  58. // ginServer.Use(cors.Default())
  59. ginServer.Use(CORSMiddleware())
  60. ginServer.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm"})))
  61. cookieStore.Options(sessions.Options{
  62. Path: "/",
  63. Secure: util.SSL,
  64. //MaxAge: 60 * 60 * 24 * 7, // 默认是 Session
  65. HttpOnly: true,
  66. })
  67. ginServer.Use(sessions.Sessions("siyuan", cookieStore))
  68. if "dev" == util.Mode {
  69. serveDebug(ginServer)
  70. }
  71. serveAssets(ginServer)
  72. serveAppearance(ginServer)
  73. serveWebSocket(ginServer)
  74. serveExport(ginServer)
  75. serveWidgets(ginServer)
  76. serveEmojis(ginServer)
  77. api.ServeAPI(ginServer)
  78. var addr string
  79. if model.Conf.System.NetworkServe || "docker" == util.Container {
  80. addr = "0.0.0.0:" + util.ServerPort
  81. } else {
  82. addr = "127.0.0.1:" + util.ServerPort
  83. }
  84. logging.LogInfof("kernel is booting [%s]", "http://"+addr)
  85. util.HttpServing = true
  86. if err := ginServer.Run(addr); nil != err {
  87. if !fastMode {
  88. logging.LogErrorf("boot kernel failed: %s", err)
  89. os.Exit(util.ExitCodeUnavailablePort)
  90. }
  91. }
  92. }
  93. func serveExport(ginServer *gin.Engine) {
  94. ginServer.Static("/export/", filepath.Join(util.TempDir, "export"))
  95. }
  96. func serveWidgets(ginServer *gin.Engine) {
  97. ginServer.Static("/widgets/", filepath.Join(util.DataDir, "widgets"))
  98. }
  99. func serveEmojis(ginServer *gin.Engine) {
  100. ginServer.Static("/emojis/", filepath.Join(util.DataDir, "emojis"))
  101. }
  102. func serveAppearance(ginServer *gin.Engine) {
  103. siyuan := ginServer.Group("", model.CheckAuth)
  104. siyuan.Handle("GET", "/", func(c *gin.Context) {
  105. userAgentHeader := c.GetHeader("User-Agent")
  106. if strings.Contains(userAgentHeader, "Electron") {
  107. c.Redirect(302, "/stage/build/app/?r="+gulu.Rand.String(7))
  108. return
  109. }
  110. ua := user_agent.New(userAgentHeader)
  111. if ua.Mobile() {
  112. c.Redirect(302, "/stage/build/mobile/?r="+gulu.Rand.String(7))
  113. return
  114. }
  115. c.Redirect(302, "/stage/build/desktop/?r="+gulu.Rand.String(7))
  116. })
  117. appearancePath := util.AppearancePath
  118. if "dev" == util.Mode {
  119. appearancePath = filepath.Join(util.WorkingDir, "appearance")
  120. }
  121. siyuan.GET("/appearance/*filepath", func(c *gin.Context) {
  122. filePath := filepath.Join(appearancePath, strings.TrimPrefix(c.Request.URL.Path, "/appearance/"))
  123. if strings.HasSuffix(c.Request.URL.Path, "/theme.js") {
  124. if !gulu.File.IsExist(filePath) {
  125. // 主题 js 不存在时生成空内容返回
  126. c.Data(200, "application/x-javascript", nil)
  127. return
  128. }
  129. } else if strings.Contains(c.Request.URL.Path, "/langs/") && strings.HasSuffix(c.Request.URL.Path, ".json") {
  130. lang := path.Base(c.Request.URL.Path)
  131. lang = strings.TrimSuffix(lang, ".json")
  132. if "zh_CN" != lang && "en_US" != lang {
  133. // 多语言配置缺失项使用对应英文配置项补齐 https://github.com/siyuan-note/siyuan/issues/5322
  134. enUSFilePath := filepath.Join(appearancePath, "langs", "en_US.json")
  135. enUSData, err := os.ReadFile(enUSFilePath)
  136. if nil != err {
  137. logging.LogFatalf("read en_US.json [%s] failed: %s", enUSFilePath, err)
  138. return
  139. }
  140. enUSMap := map[string]interface{}{}
  141. if err = gulu.JSON.UnmarshalJSON(enUSData, &enUSMap); nil != err {
  142. logging.LogFatalf("unmarshal en_US.json [%s] failed: %s", enUSFilePath, err)
  143. return
  144. }
  145. for {
  146. data, err := os.ReadFile(filePath)
  147. if nil != err {
  148. c.JSON(200, enUSMap)
  149. return
  150. }
  151. langMap := map[string]interface{}{}
  152. if err = gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
  153. logging.LogErrorf("unmarshal json [%s] failed: %s", filePath, err)
  154. c.JSON(200, enUSMap)
  155. return
  156. }
  157. for enUSDataKey, enUSDataValue := range enUSMap {
  158. if _, ok := langMap[enUSDataKey]; !ok {
  159. langMap[enUSDataKey] = enUSDataValue
  160. }
  161. }
  162. c.JSON(200, langMap)
  163. return
  164. }
  165. }
  166. }
  167. c.File(filePath)
  168. })
  169. siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
  170. siyuan.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
  171. siyuan.GET("/check-auth", serveCheckAuth)
  172. }
  173. func serveCheckAuth(c *gin.Context) {
  174. data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html"))
  175. if nil != err {
  176. logging.LogErrorf("load auth page failed: %s", err)
  177. c.Status(500)
  178. return
  179. }
  180. c.Data(http.StatusOK, "text/html; charset=utf-8", data)
  181. }
  182. func serveAssets(ginServer *gin.Engine) {
  183. ginServer.POST("/upload", model.CheckAuth, model.Upload)
  184. ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) {
  185. requestPath := context.Param("path")
  186. relativePath := path.Join("assets", requestPath)
  187. p, err := model.GetAssetAbsPath(relativePath)
  188. if nil != err {
  189. context.Status(404)
  190. return
  191. }
  192. http.ServeFile(context.Writer, context.Request, p)
  193. return
  194. })
  195. ginServer.GET("/history/:dir/assets/*name", model.CheckAuth, func(context *gin.Context) {
  196. dir := context.Param("dir")
  197. name := context.Param("name")
  198. relativePath := path.Join(dir, "assets", name)
  199. p := filepath.Join(util.HistoryDir, relativePath)
  200. http.ServeFile(context.Writer, context.Request, p)
  201. return
  202. })
  203. }
  204. func serveDebug(ginServer *gin.Engine) {
  205. ginServer.GET("/debug/pprof/", gin.WrapF(pprof.Index))
  206. ginServer.GET("/debug/pprof/allocs", gin.WrapF(pprof.Index))
  207. ginServer.GET("/debug/pprof/block", gin.WrapF(pprof.Index))
  208. ginServer.GET("/debug/pprof/goroutine", gin.WrapF(pprof.Index))
  209. ginServer.GET("/debug/pprof/heap", gin.WrapF(pprof.Index))
  210. ginServer.GET("/debug/pprof/mutex", gin.WrapF(pprof.Index))
  211. ginServer.GET("/debug/pprof/threadcreate", gin.WrapF(pprof.Index))
  212. ginServer.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
  213. ginServer.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
  214. ginServer.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
  215. ginServer.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
  216. }
  217. func serveWebSocket(ginServer *gin.Engine) {
  218. util.WebSocketServer.Config.MaxMessageSize = 1024 * 1024 * 8
  219. if "docker" == util.Container { // Docker 容器运行时启用 WebSocket 传输压缩
  220. util.WebSocketServer.Config.EnableCompression = true
  221. util.WebSocketServer.Config.CompressionLevel = 4
  222. }
  223. ginServer.GET("/ws", func(c *gin.Context) {
  224. if err := util.WebSocketServer.HandleRequest(c.Writer, c.Request); nil != err {
  225. logging.LogErrorf("handle command failed: %s", err)
  226. }
  227. })
  228. util.WebSocketServer.HandlePong(func(session *melody.Session) {
  229. //model.Logger.Debugf("pong")
  230. })
  231. util.WebSocketServer.HandleConnect(func(s *melody.Session) {
  232. //logging.LogInfof("ws check auth for [%s]", s.Request.RequestURI)
  233. authOk := true
  234. if "" != model.Conf.AccessAuthCode {
  235. session, err := cookieStore.Get(s.Request, "siyuan")
  236. if nil != err {
  237. authOk = false
  238. logging.LogErrorf("get cookie failed: %s", err)
  239. } else {
  240. val := session.Values["data"]
  241. if nil == val {
  242. authOk = false
  243. } else {
  244. sess := map[string]interface{}{}
  245. err = gulu.JSON.UnmarshalJSON([]byte(val.(string)), &sess)
  246. if nil != err {
  247. authOk = false
  248. logging.LogErrorf("unmarshal cookie failed: %s", err)
  249. } else {
  250. authOk = sess["AccessAuthCode"].(string) == model.Conf.AccessAuthCode
  251. }
  252. }
  253. }
  254. }
  255. if !authOk {
  256. s.CloseWithMsg([]byte(" unauthenticated"))
  257. //logging.LogWarnf("closed a unauthenticated session [%s]", util.GetRemoteAddr(s))
  258. return
  259. }
  260. util.AddPushChan(s)
  261. //sessionId, _ := s.Get("id")
  262. //logging.LogInfof("ws [%s] connected", sessionId)
  263. })
  264. util.WebSocketServer.HandleDisconnect(func(s *melody.Session) {
  265. util.RemovePushChan(s)
  266. //sessionId, _ := s.Get("id")
  267. //model.Logger.Debugf("ws [%s] disconnected", sessionId)
  268. })
  269. util.WebSocketServer.HandleError(func(s *melody.Session, err error) {
  270. //sessionId, _ := s.Get("id")
  271. //logging.LogDebugf("ws [%s] failed: %s", sessionId, err)
  272. })
  273. util.WebSocketServer.HandleClose(func(s *melody.Session, i int, str string) error {
  274. //sessionId, _ := s.Get("id")
  275. //logging.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str)
  276. return nil
  277. })
  278. util.WebSocketServer.HandleMessage(func(s *melody.Session, msg []byte) {
  279. start := time.Now()
  280. logging.LogTracef("request [%s]", shortReqMsg(msg))
  281. request := map[string]interface{}{}
  282. if err := gulu.JSON.UnmarshalJSON(msg, &request); nil != err {
  283. result := util.NewResult()
  284. result.Code = -1
  285. result.Msg = "Bad Request"
  286. responseData, _ := gulu.JSON.MarshalJSON(result)
  287. s.Write(responseData)
  288. return
  289. }
  290. if _, ok := s.Get("app"); !ok {
  291. result := util.NewResult()
  292. result.Code = -1
  293. result.Msg = "Bad Request"
  294. s.Write(result.Bytes())
  295. return
  296. }
  297. cmdStr := request["cmd"].(string)
  298. cmdId := request["reqId"].(float64)
  299. param := request["param"].(map[string]interface{})
  300. command := cmd.NewCommand(cmdStr, cmdId, param, s)
  301. if nil == command {
  302. result := util.NewResult()
  303. result.Code = -1
  304. result.Msg = "can not find command [" + cmdStr + "]"
  305. s.Write(result.Bytes())
  306. return
  307. }
  308. if util.ReadOnly && !command.IsRead() {
  309. result := util.NewResult()
  310. result.Code = -1
  311. result.Msg = model.Conf.Language(34)
  312. s.Write(result.Bytes())
  313. return
  314. }
  315. end := time.Now()
  316. logging.LogTracef("parse cmd [%s] consumed [%d]ms", command.Name(), end.Sub(start).Milliseconds())
  317. cmd.Exec(command)
  318. })
  319. }
  320. func shortReqMsg(msg []byte) []byte {
  321. s := gulu.Str.FromBytes(msg)
  322. max := 128
  323. if len(s) > max {
  324. count := 0
  325. for i := range s {
  326. count++
  327. if count > max {
  328. return gulu.Str.ToBytes(s[:i] + "...")
  329. }
  330. }
  331. }
  332. return msg
  333. }