serve.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  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/emersion/go-webdav/caldav"
  33. "github.com/emersion/go-webdav/carddav"
  34. "github.com/gin-contrib/gzip"
  35. "github.com/gin-contrib/sessions"
  36. "github.com/gin-contrib/sessions/memstore"
  37. "github.com/gin-gonic/gin"
  38. "github.com/mssola/useragent"
  39. "github.com/olahol/melody"
  40. "github.com/siyuan-note/logging"
  41. "github.com/siyuan-note/siyuan/kernel/api"
  42. "github.com/siyuan-note/siyuan/kernel/cmd"
  43. "github.com/siyuan-note/siyuan/kernel/model"
  44. "github.com/siyuan-note/siyuan/kernel/server/proxy"
  45. "github.com/siyuan-note/siyuan/kernel/util"
  46. "golang.org/x/net/webdav"
  47. )
  48. const (
  49. MethodMkCol = "MKCOL"
  50. MethodCopy = "COPY"
  51. MethodMove = "MOVE"
  52. MethodLock = "LOCK"
  53. MethodUnlock = "UNLOCK"
  54. MethodPropFind = "PROPFIND"
  55. MethodPropPatch = "PROPPATCH"
  56. MethodReport = "REPORT"
  57. )
  58. var (
  59. // 这里用的是内存存储,意味着重启后所有 session 会丢失,需要重新登录
  60. sessionStore = memstore.NewStore([]byte("ATN51UlxVq1Gcvdf"))
  61. HttpMethods = []string{
  62. http.MethodGet,
  63. http.MethodHead,
  64. http.MethodPost,
  65. http.MethodPut,
  66. http.MethodPatch,
  67. http.MethodDelete,
  68. http.MethodConnect,
  69. http.MethodOptions,
  70. http.MethodTrace,
  71. }
  72. WebDavMethods = []string{
  73. http.MethodOptions,
  74. http.MethodHead,
  75. http.MethodGet,
  76. http.MethodPost,
  77. http.MethodPut,
  78. http.MethodDelete,
  79. MethodMkCol,
  80. MethodCopy,
  81. MethodMove,
  82. MethodLock,
  83. MethodUnlock,
  84. MethodPropFind,
  85. MethodPropPatch,
  86. }
  87. CalDavMethods = []string{
  88. http.MethodOptions,
  89. http.MethodHead,
  90. http.MethodGet,
  91. http.MethodPost,
  92. http.MethodPut,
  93. http.MethodDelete,
  94. MethodMkCol,
  95. MethodCopy,
  96. MethodMove,
  97. // MethodLock,
  98. // MethodUnlock,
  99. MethodPropFind,
  100. MethodPropPatch,
  101. MethodReport,
  102. }
  103. CardDavMethods = []string{
  104. http.MethodOptions,
  105. http.MethodHead,
  106. http.MethodGet,
  107. http.MethodPost,
  108. http.MethodPut,
  109. http.MethodDelete,
  110. MethodMkCol,
  111. MethodCopy,
  112. MethodMove,
  113. // MethodLock,
  114. // MethodUnlock,
  115. MethodPropFind,
  116. MethodPropPatch,
  117. MethodReport,
  118. }
  119. )
  120. func Serve(fastMode bool) {
  121. gin.SetMode(gin.ReleaseMode)
  122. ginServer := gin.New()
  123. ginServer.UseH2C = true
  124. ginServer.MaxMultipartMemory = 1024 * 1024 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023
  125. ginServer.Use(
  126. model.ControlConcurrency, // 请求串行化 Concurrency control when requesting the kernel API https://github.com/siyuan-note/siyuan/issues/9939
  127. model.Timing,
  128. model.Recover,
  129. corsMiddleware(), // 后端服务支持 CORS 预检请求验证 https://github.com/siyuan-note/siyuan/pull/5593
  130. jwtMiddleware, // 解析 JWT https://github.com/siyuan-note/siyuan/issues/11364
  131. gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm", ".flac"})),
  132. )
  133. sessionStore.Options(sessions.Options{
  134. Path: "/",
  135. Secure: util.SSL,
  136. //MaxAge: 60 * 60 * 24 * 7, // 默认是 Session
  137. HttpOnly: true,
  138. })
  139. ginServer.Use(sessions.Sessions("siyuan", sessionStore))
  140. serveDebug(ginServer)
  141. serveAssets(ginServer)
  142. serveAppearance(ginServer)
  143. serveWebSocket(ginServer)
  144. serveWebDAV(ginServer)
  145. serveCalDAV(ginServer)
  146. serveCardDAV(ginServer)
  147. serveExport(ginServer)
  148. serveWidgets(ginServer)
  149. servePlugins(ginServer)
  150. serveEmojis(ginServer)
  151. serveTemplates(ginServer)
  152. servePublic(ginServer)
  153. serveSnippets(ginServer)
  154. serveRepoDiff(ginServer)
  155. serveCheckAuth(ginServer)
  156. serveFixedStaticFiles(ginServer)
  157. api.ServeAPI(ginServer)
  158. var host string
  159. if model.Conf.System.NetworkServe || util.ContainerDocker == util.Container {
  160. host = "0.0.0.0"
  161. } else {
  162. host = "127.0.0.1"
  163. }
  164. ln, err := net.Listen("tcp", host+":"+util.ServerPort)
  165. if err != nil {
  166. if !fastMode {
  167. logging.LogErrorf("boot kernel failed: %s", err)
  168. os.Exit(logging.ExitCodeUnavailablePort)
  169. }
  170. // fast 模式下启动失败则直接返回
  171. return
  172. }
  173. _, port, err := net.SplitHostPort(ln.Addr().String())
  174. if err != nil {
  175. if !fastMode {
  176. logging.LogErrorf("boot kernel failed: %s", err)
  177. os.Exit(logging.ExitCodeUnavailablePort)
  178. }
  179. }
  180. util.ServerPort = port
  181. util.ServerURL, err = url.Parse("http://127.0.0.1:" + port)
  182. if err != nil {
  183. logging.LogErrorf("parse server url failed: %s", err)
  184. }
  185. pid := fmt.Sprintf("%d", os.Getpid())
  186. if !fastMode {
  187. rewritePortJSON(pid, port)
  188. }
  189. logging.LogInfof("kernel [pid=%s] http server [%s] is booting", pid, host+":"+port)
  190. util.HttpServing = true
  191. go util.HookUILoaded()
  192. go func() {
  193. time.Sleep(1 * time.Second)
  194. go proxy.InitFixedPortService(host)
  195. go proxy.InitPublishService()
  196. // 反代服务器启动失败不影响核心服务器启动
  197. }()
  198. if err = http.Serve(ln, ginServer.Handler()); err != nil {
  199. if !fastMode {
  200. logging.LogErrorf("boot kernel failed: %s", err)
  201. os.Exit(logging.ExitCodeUnavailablePort)
  202. }
  203. }
  204. }
  205. func rewritePortJSON(pid, port string) {
  206. portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")
  207. pidPorts := map[string]string{}
  208. var data []byte
  209. var err error
  210. if gulu.File.IsExist(portJSON) {
  211. data, err = os.ReadFile(portJSON)
  212. if err != nil {
  213. logging.LogWarnf("read port.json failed: %s", err)
  214. } else {
  215. if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); err != nil {
  216. logging.LogWarnf("unmarshal port.json failed: %s", err)
  217. }
  218. }
  219. }
  220. pidPorts[pid] = port
  221. if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); err != nil {
  222. logging.LogWarnf("marshal port.json failed: %s", err)
  223. } else {
  224. if err = os.WriteFile(portJSON, data, 0644); err != nil {
  225. logging.LogWarnf("write port.json failed: %s", err)
  226. }
  227. }
  228. }
  229. func serveExport(ginServer *gin.Engine) {
  230. // Potential data export disclosure security vulnerability https://github.com/siyuan-note/siyuan/issues/12213
  231. exportGroup := ginServer.Group("/export/", model.CheckAuth)
  232. exportGroup.Static("/", filepath.Join(util.TempDir, "export"))
  233. }
  234. func serveWidgets(ginServer *gin.Engine) {
  235. ginServer.Static("/widgets/", filepath.Join(util.DataDir, "widgets"))
  236. }
  237. func servePlugins(ginServer *gin.Engine) {
  238. ginServer.Static("/plugins/", filepath.Join(util.DataDir, "plugins"))
  239. }
  240. func serveEmojis(ginServer *gin.Engine) {
  241. ginServer.Static("/emojis/", filepath.Join(util.DataDir, "emojis"))
  242. }
  243. func serveTemplates(ginServer *gin.Engine) {
  244. ginServer.Static("/templates/", filepath.Join(util.DataDir, "templates"))
  245. }
  246. func servePublic(ginServer *gin.Engine) {
  247. // Support directly access `data/public/*` contents via URL link https://github.com/siyuan-note/siyuan/issues/8593
  248. ginServer.Static("/public/", filepath.Join(util.DataDir, "public"))
  249. }
  250. func serveSnippets(ginServer *gin.Engine) {
  251. ginServer.Handle("GET", "/snippets/*filepath", func(c *gin.Context) {
  252. filePath := strings.TrimPrefix(c.Request.URL.Path, "/snippets/")
  253. ext := filepath.Ext(filePath)
  254. name := strings.TrimSuffix(filePath, ext)
  255. confSnippets, err := model.LoadSnippets()
  256. if err != nil {
  257. logging.LogErrorf("load snippets failed: %s", err)
  258. c.Status(http.StatusNotFound)
  259. return
  260. }
  261. for _, s := range confSnippets {
  262. if s.Name == name && ("" != ext && s.Type == ext[1:]) {
  263. c.Header("Content-Type", mime.TypeByExtension(ext))
  264. c.String(http.StatusOK, s.Content)
  265. return
  266. }
  267. }
  268. // 没有在配置文件中命中时在文件系统上查找
  269. filePath = filepath.Join(util.SnippetsPath, filePath)
  270. c.File(filePath)
  271. })
  272. }
  273. func serveAppearance(ginServer *gin.Engine) {
  274. siyuan := ginServer.Group("", model.CheckAuth)
  275. siyuan.Handle("GET", "/", func(c *gin.Context) {
  276. userAgentHeader := c.GetHeader("User-Agent")
  277. logging.LogInfof("serving [/] for user-agent [%s]", userAgentHeader)
  278. // Carry query parameters when redirecting
  279. location := url.URL{}
  280. queryParams := c.Request.URL.Query()
  281. queryParams.Set("r", gulu.Rand.String(7))
  282. location.RawQuery = queryParams.Encode()
  283. if strings.Contains(userAgentHeader, "Electron") {
  284. location.Path = "/stage/build/app/"
  285. } else if strings.Contains(userAgentHeader, "Pad") ||
  286. (strings.ContainsAny(userAgentHeader, "Android") && !strings.Contains(userAgentHeader, "Mobile")) {
  287. // 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
  288. location.Path = "/stage/build/desktop/"
  289. } else {
  290. if idx := strings.Index(userAgentHeader, "Mozilla/"); 0 < idx {
  291. userAgentHeader = userAgentHeader[idx:]
  292. }
  293. ua := useragent.New(userAgentHeader)
  294. if ua.Mobile() {
  295. location.Path = "/stage/build/mobile/"
  296. } else {
  297. location.Path = "/stage/build/desktop/"
  298. }
  299. }
  300. c.Redirect(302, location.String())
  301. })
  302. appearancePath := util.AppearancePath
  303. if "dev" == util.Mode {
  304. appearancePath = filepath.Join(util.WorkingDir, "appearance")
  305. }
  306. siyuan.GET("/appearance/*filepath", func(c *gin.Context) {
  307. filePath := filepath.Join(appearancePath, strings.TrimPrefix(c.Request.URL.Path, "/appearance/"))
  308. if strings.HasSuffix(c.Request.URL.Path, "/theme.js") {
  309. if !gulu.File.IsExist(filePath) {
  310. // 主题 js 不存在时生成空内容返回
  311. c.Data(200, "application/x-javascript", nil)
  312. return
  313. }
  314. } else if strings.Contains(c.Request.URL.Path, "/langs/") && strings.HasSuffix(c.Request.URL.Path, ".json") {
  315. lang := path.Base(c.Request.URL.Path)
  316. lang = strings.TrimSuffix(lang, ".json")
  317. if "zh_CN" != lang && "en_US" != lang {
  318. // 多语言配置缺失项使用对应英文配置项补齐 https://github.com/siyuan-note/siyuan/issues/5322
  319. enUSFilePath := filepath.Join(appearancePath, "langs", "en_US.json")
  320. enUSData, err := os.ReadFile(enUSFilePath)
  321. if err != nil {
  322. logging.LogErrorf("read en_US.json [%s] failed: %s", enUSFilePath, err)
  323. util.ReportFileSysFatalError(err)
  324. return
  325. }
  326. enUSMap := map[string]interface{}{}
  327. if err = gulu.JSON.UnmarshalJSON(enUSData, &enUSMap); err != nil {
  328. logging.LogErrorf("unmarshal en_US.json [%s] failed: %s", enUSFilePath, err)
  329. util.ReportFileSysFatalError(err)
  330. return
  331. }
  332. for {
  333. data, err := os.ReadFile(filePath)
  334. if err != nil {
  335. c.JSON(200, enUSMap)
  336. return
  337. }
  338. langMap := map[string]interface{}{}
  339. if err = gulu.JSON.UnmarshalJSON(data, &langMap); err != nil {
  340. logging.LogErrorf("unmarshal json [%s] failed: %s", filePath, err)
  341. c.JSON(200, enUSMap)
  342. return
  343. }
  344. for enUSDataKey, enUSDataValue := range enUSMap {
  345. if _, ok := langMap[enUSDataKey]; !ok {
  346. langMap[enUSDataKey] = enUSDataValue
  347. }
  348. }
  349. c.JSON(200, langMap)
  350. return
  351. }
  352. }
  353. }
  354. c.File(filePath)
  355. })
  356. siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
  357. }
  358. func serveCheckAuth(ginServer *gin.Engine) {
  359. ginServer.GET("/check-auth", serveAuthPage)
  360. }
  361. func serveAuthPage(c *gin.Context) {
  362. data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html"))
  363. if err != nil {
  364. logging.LogErrorf("load auth page failed: %s", err)
  365. c.Status(500)
  366. return
  367. }
  368. tpl, err := template.New("auth").Parse(string(data))
  369. if err != nil {
  370. logging.LogErrorf("parse auth page failed: %s", err)
  371. c.Status(500)
  372. return
  373. }
  374. keymapHideWindow := "⌥M"
  375. if nil != (*model.Conf.Keymap)["general"] {
  376. switch (*model.Conf.Keymap)["general"].(type) {
  377. case map[string]interface{}:
  378. keymapGeneral := (*model.Conf.Keymap)["general"].(map[string]interface{})
  379. if nil != keymapGeneral["toggleWin"] {
  380. switch keymapGeneral["toggleWin"].(type) {
  381. case map[string]interface{}:
  382. toggleWin := keymapGeneral["toggleWin"].(map[string]interface{})
  383. if nil != toggleWin["custom"] {
  384. keymapHideWindow = toggleWin["custom"].(string)
  385. }
  386. }
  387. }
  388. }
  389. if "" == keymapHideWindow {
  390. keymapHideWindow = "⌥M"
  391. }
  392. }
  393. model := map[string]interface{}{
  394. "l0": model.Conf.Language(173),
  395. "l1": model.Conf.Language(174),
  396. "l2": template.HTML(model.Conf.Language(172)),
  397. "l3": model.Conf.Language(175),
  398. "l4": model.Conf.Language(176),
  399. "l5": model.Conf.Language(177),
  400. "l6": model.Conf.Language(178),
  401. "l7": template.HTML(model.Conf.Language(184)),
  402. "l8": model.Conf.Language(95),
  403. "appearanceMode": model.Conf.Appearance.Mode,
  404. "appearanceModeOS": model.Conf.Appearance.ModeOS,
  405. "workspace": util.WorkspaceName,
  406. "workspacePath": util.WorkspaceDir,
  407. "keymapGeneralToggleWin": keymapHideWindow,
  408. "trayMenuLangs": util.TrayMenuLangs[util.Lang],
  409. "workspaceDir": util.WorkspaceDir,
  410. }
  411. buf := &bytes.Buffer{}
  412. if err = tpl.Execute(buf, model); err != nil {
  413. logging.LogErrorf("execute auth page failed: %s", err)
  414. c.Status(500)
  415. return
  416. }
  417. data = buf.Bytes()
  418. c.Data(http.StatusOK, "text/html; charset=utf-8", data)
  419. }
  420. func serveAssets(ginServer *gin.Engine) {
  421. ginServer.POST("/upload", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, model.Upload)
  422. ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) {
  423. requestPath := context.Param("path")
  424. relativePath := path.Join("assets", requestPath)
  425. p, err := model.GetAssetAbsPath(relativePath)
  426. if err != nil {
  427. if strings.Contains(strings.TrimPrefix(requestPath, "/"), "/") {
  428. // 再使用编码过的路径解析一次 https://github.com/siyuan-note/siyuan/issues/11823
  429. dest := url.PathEscape(strings.TrimPrefix(requestPath, "/"))
  430. dest = strings.ReplaceAll(dest, ":", "%3A")
  431. relativePath = path.Join("assets", dest)
  432. p, err = model.GetAssetAbsPath(relativePath)
  433. }
  434. if err != nil {
  435. context.Status(http.StatusNotFound)
  436. return
  437. }
  438. }
  439. http.ServeFile(context.Writer, context.Request, p)
  440. return
  441. })
  442. ginServer.GET("/history/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
  443. p := filepath.Join(util.HistoryDir, context.Param("path"))
  444. http.ServeFile(context.Writer, context.Request, p)
  445. return
  446. })
  447. }
  448. func serveRepoDiff(ginServer *gin.Engine) {
  449. ginServer.GET("/repo/diff/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
  450. requestPath := context.Param("path")
  451. p := filepath.Join(util.TempDir, "repo", "diff", requestPath)
  452. http.ServeFile(context.Writer, context.Request, p)
  453. return
  454. })
  455. }
  456. func serveDebug(ginServer *gin.Engine) {
  457. if "prod" == util.Mode {
  458. // The production environment will no longer register `/debug/pprof/` https://github.com/siyuan-note/siyuan/issues/10152
  459. return
  460. }
  461. ginServer.GET("/debug/pprof/", gin.WrapF(pprof.Index))
  462. ginServer.GET("/debug/pprof/allocs", gin.WrapF(pprof.Index))
  463. ginServer.GET("/debug/pprof/block", gin.WrapF(pprof.Index))
  464. ginServer.GET("/debug/pprof/goroutine", gin.WrapF(pprof.Index))
  465. ginServer.GET("/debug/pprof/heap", gin.WrapF(pprof.Index))
  466. ginServer.GET("/debug/pprof/mutex", gin.WrapF(pprof.Index))
  467. ginServer.GET("/debug/pprof/threadcreate", gin.WrapF(pprof.Index))
  468. ginServer.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
  469. ginServer.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
  470. ginServer.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
  471. ginServer.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
  472. }
  473. func serveWebSocket(ginServer *gin.Engine) {
  474. util.WebSocketServer.Config.MaxMessageSize = 1024 * 1024 * 8
  475. ginServer.GET("/ws", func(c *gin.Context) {
  476. if err := util.WebSocketServer.HandleRequest(c.Writer, c.Request); err != nil {
  477. logging.LogErrorf("handle command failed: %s", err)
  478. }
  479. })
  480. util.WebSocketServer.HandlePong(func(session *melody.Session) {
  481. //logging.LogInfof("pong")
  482. })
  483. util.WebSocketServer.HandleConnect(func(s *melody.Session) {
  484. //logging.LogInfof("ws check auth for [%s]", s.Request.RequestURI)
  485. authOk := true
  486. if "" != model.Conf.AccessAuthCode {
  487. session, err := sessionStore.Get(s.Request, "siyuan")
  488. if err != nil {
  489. authOk = false
  490. logging.LogErrorf("get cookie failed: %s", err)
  491. } else {
  492. val := session.Values["data"]
  493. if nil == val {
  494. authOk = false
  495. } else {
  496. sess := &util.SessionData{}
  497. err = gulu.JSON.UnmarshalJSON([]byte(val.(string)), sess)
  498. if err != nil {
  499. authOk = false
  500. logging.LogErrorf("unmarshal cookie failed: %s", err)
  501. } else {
  502. workspaceSess := util.GetWorkspaceSession(sess)
  503. authOk = workspaceSess.AccessAuthCode == model.Conf.AccessAuthCode
  504. }
  505. }
  506. }
  507. }
  508. // REF: https://github.com/siyuan-note/siyuan/issues/11364
  509. if !authOk {
  510. if token := model.ParseXAuthToken(s.Request); token != nil {
  511. authOk = token.Valid && model.IsValidRole(model.GetClaimRole(model.GetTokenClaims(token)), []model.Role{
  512. model.RoleAdministrator,
  513. model.RoleEditor,
  514. model.RoleReader,
  515. })
  516. }
  517. }
  518. if !authOk {
  519. // 用于授权页保持连接,避免非常驻内存内核自动退出 https://github.com/siyuan-note/insider/issues/1099
  520. authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan&id=auth")
  521. }
  522. if !authOk {
  523. s.CloseWithMsg([]byte(" unauthenticated"))
  524. logging.LogWarnf("closed an unauthenticated session [%s]", util.GetRemoteAddr(s.Request))
  525. return
  526. }
  527. util.AddPushChan(s)
  528. //sessionId, _ := s.Get("id")
  529. //logging.LogInfof("ws [%s] connected", sessionId)
  530. })
  531. util.WebSocketServer.HandleDisconnect(func(s *melody.Session) {
  532. util.RemovePushChan(s)
  533. //sessionId, _ := s.Get("id")
  534. //logging.LogInfof("ws [%s] disconnected", sessionId)
  535. })
  536. util.WebSocketServer.HandleError(func(s *melody.Session, err error) {
  537. //sessionId, _ := s.Get("id")
  538. //logging.LogWarnf("ws [%s] failed: %s", sessionId, err)
  539. })
  540. util.WebSocketServer.HandleClose(func(s *melody.Session, i int, str string) error {
  541. //sessionId, _ := s.Get("id")
  542. //logging.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str)
  543. return nil
  544. })
  545. util.WebSocketServer.HandleMessage(func(s *melody.Session, msg []byte) {
  546. start := time.Now()
  547. logging.LogTracef("request [%s]", shortReqMsg(msg))
  548. request := map[string]interface{}{}
  549. if err := gulu.JSON.UnmarshalJSON(msg, &request); err != nil {
  550. result := util.NewResult()
  551. result.Code = -1
  552. result.Msg = "Bad Request"
  553. responseData, _ := gulu.JSON.MarshalJSON(result)
  554. s.Write(responseData)
  555. return
  556. }
  557. if _, ok := s.Get("app"); !ok {
  558. result := util.NewResult()
  559. result.Code = -1
  560. result.Msg = "Bad Request"
  561. s.Write(result.Bytes())
  562. return
  563. }
  564. cmdStr := request["cmd"].(string)
  565. cmdId := request["reqId"].(float64)
  566. param := request["param"].(map[string]interface{})
  567. command := cmd.NewCommand(cmdStr, cmdId, param, s)
  568. if nil == command {
  569. result := util.NewResult()
  570. result.Code = -1
  571. result.Msg = "can not find command [" + cmdStr + "]"
  572. s.Write(result.Bytes())
  573. return
  574. }
  575. if !command.IsRead() {
  576. readonly := util.ReadOnly
  577. if !readonly {
  578. if token := model.ParseXAuthToken(s.Request); token != nil {
  579. readonly = token.Valid && model.IsValidRole(model.GetClaimRole(model.GetTokenClaims(token)), []model.Role{
  580. model.RoleReader,
  581. model.RoleVisitor,
  582. })
  583. }
  584. }
  585. if readonly {
  586. result := util.NewResult()
  587. result.Code = -1
  588. result.Msg = model.Conf.Language(34)
  589. s.Write(result.Bytes())
  590. return
  591. }
  592. }
  593. end := time.Now()
  594. logging.LogTracef("parse cmd [%s] consumed [%d]ms", command.Name(), end.Sub(start).Milliseconds())
  595. cmd.Exec(command)
  596. })
  597. }
  598. func serveWebDAV(ginServer *gin.Engine) {
  599. // REF: https://github.com/fungaren/gin-webdav
  600. handler := webdav.Handler{
  601. Prefix: "/webdav/",
  602. FileSystem: webdav.Dir(util.WorkspaceDir),
  603. LockSystem: webdav.NewMemLS(),
  604. Logger: func(r *http.Request, err error) {
  605. if nil != err {
  606. logging.LogErrorf("WebDAV [%s %s]: %s", r.Method, r.URL.String(), err.Error())
  607. }
  608. // logging.LogDebugf("WebDAV [%s %s]", r.Method, r.URL.String())
  609. },
  610. }
  611. ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole)
  612. // ginGroup.Any NOT support extension methods (PROPFIND etc.)
  613. ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) {
  614. if util.ReadOnly {
  615. switch c.Request.Method {
  616. case http.MethodPost,
  617. http.MethodPut,
  618. http.MethodDelete,
  619. MethodMkCol,
  620. MethodCopy,
  621. MethodMove,
  622. MethodLock,
  623. MethodUnlock,
  624. MethodPropPatch:
  625. c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
  626. return
  627. }
  628. }
  629. handler.ServeHTTP(c.Writer, c.Request)
  630. })
  631. }
  632. func serveCalDAV(ginServer *gin.Engine) {
  633. // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
  634. handler := caldav.Handler{
  635. Backend: &model.CalDavBackend{},
  636. Prefix: model.CalDavPrincipalsPath,
  637. }
  638. ginServer.Match(CalDavMethods, "/.well-known/caldav", func(c *gin.Context) {
  639. // logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
  640. handler.ServeHTTP(c.Writer, c.Request)
  641. })
  642. ginGroup := ginServer.Group(model.CalDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
  643. ginGroup.Match(CalDavMethods, "/*path", func(c *gin.Context) {
  644. // logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
  645. if util.ReadOnly {
  646. switch c.Request.Method {
  647. case http.MethodPost,
  648. http.MethodPut,
  649. http.MethodDelete,
  650. MethodMkCol,
  651. MethodCopy,
  652. MethodMove,
  653. MethodLock,
  654. MethodUnlock,
  655. MethodPropPatch:
  656. c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
  657. return
  658. }
  659. }
  660. handler.ServeHTTP(c.Writer, c.Request)
  661. // logging.LogDebugf("CalDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
  662. })
  663. }
  664. func serveCardDAV(ginServer *gin.Engine) {
  665. // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
  666. handler := carddav.Handler{
  667. Backend: &model.CardDavBackend{},
  668. Prefix: model.CardDavPrincipalsPath,
  669. }
  670. ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
  671. // logging.LogDebugf("CardDAV [/.well-known/carddav]")
  672. handler.ServeHTTP(c.Writer, c.Request)
  673. })
  674. ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
  675. ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
  676. if util.ReadOnly {
  677. switch c.Request.Method {
  678. case http.MethodPost,
  679. http.MethodPut,
  680. http.MethodDelete,
  681. MethodMkCol,
  682. MethodCopy,
  683. MethodMove,
  684. MethodLock,
  685. MethodUnlock,
  686. MethodPropPatch:
  687. c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
  688. return
  689. }
  690. }
  691. // TODO: Can't handle Thunderbird's PROPFIND request with prop <current-user-privilege-set/>
  692. handler.ServeHTTP(c.Writer, c.Request)
  693. // logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
  694. })
  695. }
  696. func shortReqMsg(msg []byte) []byte {
  697. s := gulu.Str.FromBytes(msg)
  698. max := 128
  699. if len(s) > max {
  700. count := 0
  701. for i := range s {
  702. count++
  703. if count > max {
  704. return gulu.Str.ToBytes(s[:i] + "...")
  705. }
  706. }
  707. }
  708. return msg
  709. }
  710. func corsMiddleware() gin.HandlerFunc {
  711. allowMethods := strings.Join(HttpMethods, ", ")
  712. allowWebDavMethods := strings.Join(WebDavMethods, ", ")
  713. allowCalDavMethods := strings.Join(CalDavMethods, ", ")
  714. allowCardDavMethods := strings.Join(CardDavMethods, ", ")
  715. return func(c *gin.Context) {
  716. c.Header("Access-Control-Allow-Origin", "*")
  717. c.Header("Access-Control-Allow-Credentials", "true")
  718. c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
  719. c.Header("Access-Control-Allow-Private-Network", "true")
  720. if strings.HasPrefix(c.Request.RequestURI, "/webdav") {
  721. c.Header("Access-Control-Allow-Methods", allowWebDavMethods)
  722. c.Next()
  723. return
  724. }
  725. if strings.HasPrefix(c.Request.RequestURI, "/caldav") {
  726. c.Header("Access-Control-Allow-Methods", allowCalDavMethods)
  727. c.Next()
  728. return
  729. }
  730. if strings.HasPrefix(c.Request.RequestURI, "/carddav") {
  731. c.Header("Access-Control-Allow-Methods", allowCardDavMethods)
  732. c.Next()
  733. return
  734. }
  735. c.Header("Access-Control-Allow-Methods", allowMethods)
  736. switch c.Request.Method {
  737. case http.MethodOptions:
  738. c.Header("Access-Control-Max-Age", "600")
  739. c.AbortWithStatus(204)
  740. return
  741. }
  742. c.Next()
  743. }
  744. }
  745. // jwtMiddleware is a middleware to check jwt token
  746. // REF: https://github.com/siyuan-note/siyuan/issues/11364
  747. func jwtMiddleware(c *gin.Context) {
  748. if token := model.ParseXAuthToken(c.Request); token != nil {
  749. // c.Request.Header.Del(model.XAuthTokenKey)
  750. if token.Valid {
  751. claims := model.GetTokenClaims(token)
  752. c.Set(model.ClaimsContextKey, claims)
  753. c.Set(model.RoleContextKey, model.GetClaimRole(claims))
  754. c.Next()
  755. return
  756. }
  757. }
  758. c.Set(model.RoleContextKey, model.RoleVisitor)
  759. c.Next()
  760. return
  761. }
  762. func serveFixedStaticFiles(ginServer *gin.Engine) {
  763. ginServer.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
  764. ginServer.StaticFile("manifest.json", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
  765. ginServer.StaticFile("manifest.webmanifest", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
  766. ginServer.StaticFile("service-worker.js", filepath.Join(util.WorkingDir, "stage", "service-worker.js"))
  767. }