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