A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 896 lines 27 kB view raw
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}