serve.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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. "fmt"
  19. "net"
  20. "net/http"
  21. "net/http/httputil"
  22. "net/http/pprof"
  23. "net/url"
  24. "os"
  25. "path"
  26. "path/filepath"
  27. "strings"
  28. "time"
  29. "github.com/88250/gulu"
  30. "github.com/gin-contrib/gzip"
  31. "github.com/gin-contrib/sessions"
  32. "github.com/gin-contrib/sessions/cookie"
  33. "github.com/gin-gonic/gin"
  34. "github.com/mssola/user_agent"
  35. "github.com/olahol/melody"
  36. "github.com/siyuan-note/logging"
  37. "github.com/siyuan-note/siyuan/kernel/api"
  38. "github.com/siyuan-note/siyuan/kernel/cmd"
  39. "github.com/siyuan-note/siyuan/kernel/model"
  40. "github.com/siyuan-note/siyuan/kernel/util"
  41. )
  42. var cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
  43. func Serve(fastMode bool) {
  44. gin.SetMode(gin.ReleaseMode)
  45. ginServer := gin.New()
  46. ginServer.MaxMultipartMemory = 1024 * 1024 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023
  47. ginServer.Use(gin.Recovery())
  48. ginServer.Use(corsMiddleware()) // 后端服务支持 CORS 预检请求验证 https://github.com/siyuan-note/siyuan/pull/5593
  49. ginServer.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm"})))
  50. cookieStore.Options(sessions.Options{
  51. Path: "/",
  52. Secure: util.SSL,
  53. //MaxAge: 60 * 60 * 24 * 7, // 默认是 Session
  54. HttpOnly: true,
  55. })
  56. ginServer.Use(sessions.Sessions("siyuan", cookieStore))
  57. if "dev" == util.Mode {
  58. serveDebug(ginServer)
  59. }
  60. serveAssets(ginServer)
  61. serveAppearance(ginServer)
  62. serveWebSocket(ginServer)
  63. serveExport(ginServer)
  64. serveWidgets(ginServer)
  65. serveEmojis(ginServer)
  66. serveTemplates(ginServer)
  67. api.ServeAPI(ginServer)
  68. if !fastMode && "prod" == util.Mode && util.ContainerStd == util.Container {
  69. killRunningKernel()
  70. }
  71. var host string
  72. if model.Conf.System.NetworkServe || util.ContainerDocker == util.Container {
  73. host = "0.0.0.0"
  74. } else {
  75. host = "127.0.0.1"
  76. }
  77. ln, err := net.Listen("tcp", host+":"+util.ServerPort)
  78. if nil != err {
  79. if !fastMode {
  80. logging.LogErrorf("boot kernel failed: %s", err)
  81. os.Exit(util.ExitCodeUnavailablePort)
  82. }
  83. // fast 模式下启动失败则直接返回
  84. return
  85. }
  86. _, port, err := net.SplitHostPort(ln.Addr().String())
  87. if nil != err {
  88. if !fastMode {
  89. logging.LogErrorf("boot kernel failed: %s", err)
  90. os.Exit(util.ExitCodeUnavailablePort)
  91. }
  92. }
  93. util.ServerPort = port
  94. pid := fmt.Sprintf("%d", os.Getpid())
  95. if !fastMode {
  96. rewritePortJSON(pid, port)
  97. }
  98. logging.LogInfof("kernel [pid=%s] is booting [%s]", pid, "http://"+util.LocalHost+":"+port)
  99. util.HttpServing = true
  100. go func() {
  101. if util.FixedPort != port {
  102. // 启动一个 6806 端口的反向代理服务器,这样浏览器扩展才能直接使用 127.0.0.1:6806,不用配置端口
  103. serverURL, _ := url.Parse("http://" + host + ":" + port)
  104. proxy := httputil.NewSingleHostReverseProxy(serverURL)
  105. logging.LogInfof("kernel reverse proxy server [%s] is booting", util.FixedPort)
  106. if proxyErr := http.ListenAndServe(host+":"+util.FixedPort, proxy); nil != proxyErr {
  107. logging.LogErrorf("boot kernel reverse proxy server [%s] failed: %s", serverURL, proxyErr)
  108. }
  109. // 反代服务器启动失败不影响核心服务器启动
  110. }
  111. }()
  112. if err = http.Serve(ln, ginServer); nil != err {
  113. if !fastMode {
  114. logging.LogErrorf("boot kernel failed: %s", err)
  115. os.Exit(util.ExitCodeUnavailablePort)
  116. }
  117. }
  118. }
  119. func rewritePortJSON(pid, port string) {
  120. portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")
  121. pidPorts := map[string]string{}
  122. var data []byte
  123. var err error
  124. if gulu.File.IsExist(portJSON) {
  125. data, err = os.ReadFile(portJSON)
  126. if nil != err {
  127. logging.LogWarnf("read port.json failed: %s", err)
  128. } else {
  129. if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); nil != err {
  130. logging.LogWarnf("unmarshal port.json failed: %s", err)
  131. }
  132. }
  133. }
  134. pidPorts[pid] = port
  135. if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); nil != err {
  136. logging.LogWarnf("marshal port.json failed: %s", err)
  137. } else {
  138. if err = os.WriteFile(portJSON, data, 0644); nil != err {
  139. logging.LogWarnf("write port.json failed: %s", err)
  140. }
  141. }
  142. }
  143. func serveExport(ginServer *gin.Engine) {
  144. ginServer.Static("/export/", filepath.Join(util.TempDir, "export"))
  145. }
  146. func serveWidgets(ginServer *gin.Engine) {
  147. ginServer.Static("/widgets/", filepath.Join(util.DataDir, "widgets"))
  148. }
  149. func serveEmojis(ginServer *gin.Engine) {
  150. ginServer.Static("/emojis/", filepath.Join(util.DataDir, "emojis"))
  151. }
  152. func serveTemplates(ginServer *gin.Engine) {
  153. ginServer.Static("/templates/", filepath.Join(util.DataDir, "templates"))
  154. }
  155. func serveAppearance(ginServer *gin.Engine) {
  156. siyuan := ginServer.Group("", model.CheckAuth)
  157. siyuan.Handle("GET", "/", func(c *gin.Context) {
  158. userAgentHeader := c.GetHeader("User-Agent")
  159. if strings.Contains(userAgentHeader, "Electron") {
  160. c.Redirect(302, "/stage/build/app/?r="+gulu.Rand.String(7))
  161. return
  162. }
  163. ua := user_agent.New(userAgentHeader)
  164. if ua.Mobile() {
  165. c.Redirect(302, "/stage/build/mobile/?r="+gulu.Rand.String(7))
  166. return
  167. }
  168. c.Redirect(302, "/stage/build/desktop/?r="+gulu.Rand.String(7))
  169. })
  170. appearancePath := util.AppearancePath
  171. if "dev" == util.Mode {
  172. appearancePath = filepath.Join(util.WorkingDir, "appearance")
  173. }
  174. siyuan.GET("/appearance/*filepath", func(c *gin.Context) {
  175. filePath := filepath.Join(appearancePath, strings.TrimPrefix(c.Request.URL.Path, "/appearance/"))
  176. if strings.HasSuffix(c.Request.URL.Path, "/theme.js") {
  177. if !gulu.File.IsExist(filePath) {
  178. // 主题 js 不存在时生成空内容返回
  179. c.Data(200, "application/x-javascript", nil)
  180. return
  181. }
  182. } else if strings.Contains(c.Request.URL.Path, "/langs/") && strings.HasSuffix(c.Request.URL.Path, ".json") {
  183. lang := path.Base(c.Request.URL.Path)
  184. lang = strings.TrimSuffix(lang, ".json")
  185. if "zh_CN" != lang && "en_US" != lang {
  186. // 多语言配置缺失项使用对应英文配置项补齐 https://github.com/siyuan-note/siyuan/issues/5322
  187. enUSFilePath := filepath.Join(appearancePath, "langs", "en_US.json")
  188. enUSData, err := os.ReadFile(enUSFilePath)
  189. if nil != err {
  190. logging.LogFatalf("read en_US.json [%s] failed: %s", enUSFilePath, err)
  191. return
  192. }
  193. enUSMap := map[string]interface{}{}
  194. if err = gulu.JSON.UnmarshalJSON(enUSData, &enUSMap); nil != err {
  195. logging.LogFatalf("unmarshal en_US.json [%s] failed: %s", enUSFilePath, err)
  196. return
  197. }
  198. for {
  199. data, err := os.ReadFile(filePath)
  200. if nil != err {
  201. c.JSON(200, enUSMap)
  202. return
  203. }
  204. langMap := map[string]interface{}{}
  205. if err = gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
  206. logging.LogErrorf("unmarshal json [%s] failed: %s", filePath, err)
  207. c.JSON(200, enUSMap)
  208. return
  209. }
  210. for enUSDataKey, enUSDataValue := range enUSMap {
  211. if _, ok := langMap[enUSDataKey]; !ok {
  212. langMap[enUSDataKey] = enUSDataValue
  213. }
  214. }
  215. c.JSON(200, langMap)
  216. return
  217. }
  218. }
  219. }
  220. c.File(filePath)
  221. })
  222. siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
  223. siyuan.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
  224. siyuan.GET("/check-auth", serveCheckAuth)
  225. }
  226. func serveCheckAuth(c *gin.Context) {
  227. data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html"))
  228. if nil != err {
  229. logging.LogErrorf("load auth page failed: %s", err)
  230. c.Status(500)
  231. return
  232. }
  233. c.Data(http.StatusOK, "text/html; charset=utf-8", data)
  234. }
  235. func serveAssets(ginServer *gin.Engine) {
  236. ginServer.POST("/upload", model.CheckAuth, model.Upload)
  237. ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) {
  238. requestPath := context.Param("path")
  239. relativePath := path.Join("assets", requestPath)
  240. p, err := model.GetAssetAbsPath(relativePath)
  241. if nil != err {
  242. context.Status(404)
  243. return
  244. }
  245. http.ServeFile(context.Writer, context.Request, p)
  246. return
  247. })
  248. ginServer.GET("/history/*path", model.CheckAuth, func(context *gin.Context) {
  249. p := filepath.Join(util.HistoryDir, context.Param("path"))
  250. http.ServeFile(context.Writer, context.Request, p)
  251. return
  252. })
  253. }
  254. func serveDebug(ginServer *gin.Engine) {
  255. ginServer.GET("/debug/pprof/", gin.WrapF(pprof.Index))
  256. ginServer.GET("/debug/pprof/allocs", gin.WrapF(pprof.Index))
  257. ginServer.GET("/debug/pprof/block", gin.WrapF(pprof.Index))
  258. ginServer.GET("/debug/pprof/goroutine", gin.WrapF(pprof.Index))
  259. ginServer.GET("/debug/pprof/heap", gin.WrapF(pprof.Index))
  260. ginServer.GET("/debug/pprof/mutex", gin.WrapF(pprof.Index))
  261. ginServer.GET("/debug/pprof/threadcreate", gin.WrapF(pprof.Index))
  262. ginServer.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
  263. ginServer.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
  264. ginServer.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
  265. ginServer.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
  266. }
  267. func serveWebSocket(ginServer *gin.Engine) {
  268. util.WebSocketServer.Config.MaxMessageSize = 1024 * 1024 * 8
  269. ginServer.GET("/ws", func(c *gin.Context) {
  270. if err := util.WebSocketServer.HandleRequest(c.Writer, c.Request); nil != err {
  271. logging.LogErrorf("handle command failed: %s", err)
  272. }
  273. })
  274. util.WebSocketServer.HandlePong(func(session *melody.Session) {
  275. //logging.LogInfof("pong")
  276. })
  277. util.WebSocketServer.HandleConnect(func(s *melody.Session) {
  278. //logging.LogInfof("ws check auth for [%s]", s.Request.RequestURI)
  279. authOk := true
  280. if "" != model.Conf.AccessAuthCode {
  281. session, err := cookieStore.Get(s.Request, "siyuan")
  282. if nil != err {
  283. authOk = false
  284. logging.LogErrorf("get cookie failed: %s", err)
  285. } else {
  286. val := session.Values["data"]
  287. if nil == val {
  288. authOk = false
  289. } else {
  290. sess := map[string]interface{}{}
  291. err = gulu.JSON.UnmarshalJSON([]byte(val.(string)), &sess)
  292. if nil != err {
  293. authOk = false
  294. logging.LogErrorf("unmarshal cookie failed: %s", err)
  295. } else {
  296. authOk = sess["AccessAuthCode"].(string) == model.Conf.AccessAuthCode
  297. }
  298. }
  299. }
  300. }
  301. if !authOk {
  302. // 用于授权页保持连接,避免非常驻内存内核自动退出 https://github.com/siyuan-note/insider/issues/1099
  303. authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan&id=auth")
  304. }
  305. if !authOk {
  306. s.CloseWithMsg([]byte(" unauthenticated"))
  307. //logging.LogWarnf("closed an unauthenticated session [%s]", util.GetRemoteAddr(s))
  308. return
  309. }
  310. util.AddPushChan(s)
  311. //sessionId, _ := s.Get("id")
  312. //logging.LogInfof("ws [%s] connected", sessionId)
  313. })
  314. util.WebSocketServer.HandleDisconnect(func(s *melody.Session) {
  315. util.RemovePushChan(s)
  316. //sessionId, _ := s.Get("id")
  317. //logging.LogInfof("ws [%s] disconnected", sessionId)
  318. })
  319. util.WebSocketServer.HandleError(func(s *melody.Session, err error) {
  320. //sessionId, _ := s.Get("id")
  321. //logging.LogDebugf("ws [%s] failed: %s", sessionId, err)
  322. })
  323. util.WebSocketServer.HandleClose(func(s *melody.Session, i int, str string) error {
  324. //sessionId, _ := s.Get("id")
  325. //logging.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str)
  326. return nil
  327. })
  328. util.WebSocketServer.HandleMessage(func(s *melody.Session, msg []byte) {
  329. start := time.Now()
  330. logging.LogTracef("request [%s]", shortReqMsg(msg))
  331. request := map[string]interface{}{}
  332. if err := gulu.JSON.UnmarshalJSON(msg, &request); nil != err {
  333. result := util.NewResult()
  334. result.Code = -1
  335. result.Msg = "Bad Request"
  336. responseData, _ := gulu.JSON.MarshalJSON(result)
  337. s.Write(responseData)
  338. return
  339. }
  340. if _, ok := s.Get("app"); !ok {
  341. result := util.NewResult()
  342. result.Code = -1
  343. result.Msg = "Bad Request"
  344. s.Write(result.Bytes())
  345. return
  346. }
  347. cmdStr := request["cmd"].(string)
  348. cmdId := request["reqId"].(float64)
  349. param := request["param"].(map[string]interface{})
  350. command := cmd.NewCommand(cmdStr, cmdId, param, s)
  351. if nil == command {
  352. result := util.NewResult()
  353. result.Code = -1
  354. result.Msg = "can not find command [" + cmdStr + "]"
  355. s.Write(result.Bytes())
  356. return
  357. }
  358. if util.ReadOnly && !command.IsRead() {
  359. result := util.NewResult()
  360. result.Code = -1
  361. result.Msg = model.Conf.Language(34)
  362. s.Write(result.Bytes())
  363. return
  364. }
  365. end := time.Now()
  366. logging.LogTracef("parse cmd [%s] consumed [%d]ms", command.Name(), end.Sub(start).Milliseconds())
  367. cmd.Exec(command)
  368. })
  369. }
  370. func shortReqMsg(msg []byte) []byte {
  371. s := gulu.Str.FromBytes(msg)
  372. max := 128
  373. if len(s) > max {
  374. count := 0
  375. for i := range s {
  376. count++
  377. if count > max {
  378. return gulu.Str.ToBytes(s[:i] + "...")
  379. }
  380. }
  381. }
  382. return msg
  383. }
  384. func corsMiddleware() gin.HandlerFunc {
  385. return func(c *gin.Context) {
  386. c.Header("Access-Control-Allow-Origin", "*")
  387. c.Header("Access-Control-Allow-Credentials", "true")
  388. c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
  389. c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
  390. if c.Request.Method == "OPTIONS" {
  391. c.AbortWithStatus(204)
  392. return
  393. }
  394. c.Next()
  395. }
  396. }