serve.go 20 KB

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