serve.go 17 KB

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