A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 495 lines 14 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 model 18 19import ( 20 "image/color" 21 "net/http" 22 "net/url" 23 "os" 24 "strconv" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/88250/gulu" 30 ginSessions "github.com/gin-contrib/sessions" 31 "github.com/gin-gonic/gin" 32 "github.com/gorilla/websocket" 33 "github.com/siyuan-note/logging" 34 "github.com/siyuan-note/siyuan/kernel/util" 35 "github.com/steambap/captcha" 36) 37 38var ( 39 BasicAuthHeaderKey = "WWW-Authenticate" 40 BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\"" 41) 42 43func LogoutAuth(c *gin.Context) { 44 ret := gulu.Ret.NewResult() 45 defer c.JSON(http.StatusOK, ret) 46 47 if "" == Conf.AccessAuthCode { 48 ret.Code = -1 49 ret.Msg = Conf.Language(86) 50 ret.Data = map[string]interface{}{"closeTimeout": 5000} 51 return 52 } 53 54 session := util.GetSession(c) 55 util.RemoveWorkspaceSession(session) 56 if err := session.Save(c); err != nil { 57 logging.LogErrorf("saves session failed: " + err.Error()) 58 ret.Code = -1 59 ret.Msg = "save session failed" 60 } 61} 62 63func LoginAuth(c *gin.Context) { 64 ret := gulu.Ret.NewResult() 65 defer c.JSON(http.StatusOK, ret) 66 67 arg, ok := util.JsonArg(c, ret) 68 if !ok { 69 return 70 } 71 72 var inputCaptcha string 73 session := util.GetSession(c) 74 workspaceSession := util.GetWorkspaceSession(session) 75 if util.NeedCaptcha() { 76 captchaArg := arg["captcha"] 77 if nil == captchaArg { 78 ret.Code = 1 79 ret.Msg = Conf.Language(21) 80 logging.LogWarnf("invalid captcha") 81 return 82 } 83 inputCaptcha = captchaArg.(string) 84 if "" == inputCaptcha { 85 ret.Code = 1 86 ret.Msg = Conf.Language(21) 87 logging.LogWarnf("invalid captcha") 88 return 89 } 90 91 if strings.ToLower(workspaceSession.Captcha) != strings.ToLower(inputCaptcha) { 92 ret.Code = 1 93 ret.Msg = Conf.Language(22) 94 logging.LogWarnf("invalid captcha") 95 96 workspaceSession.Captcha = gulu.Rand.String(7) // https://github.com/siyuan-note/siyuan/issues/13147 97 if err := session.Save(c); err != nil { 98 logging.LogErrorf("save session failed: " + err.Error()) 99 c.Status(http.StatusInternalServerError) 100 return 101 } 102 return 103 } 104 } 105 106 authCode := arg["authCode"].(string) 107 authCode = strings.TrimSpace(authCode) 108 authCode = util.RemoveInvalid(authCode) 109 110 if Conf.AccessAuthCode != authCode { 111 ret.Code = -1 112 ret.Msg = Conf.Language(83) 113 logging.LogWarnf("invalid auth code [ip=%s]", util.GetRemoteAddr(c.Request)) 114 115 util.WrongAuthCount++ 116 workspaceSession.Captcha = gulu.Rand.String(7) 117 if util.NeedCaptcha() { 118 ret.Code = 1 // 需要渲染验证码 119 } 120 121 if err := session.Save(c); err != nil { 122 logging.LogErrorf("save session failed: " + err.Error()) 123 c.Status(http.StatusInternalServerError) 124 return 125 } 126 return 127 } 128 129 workspaceSession.AccessAuthCode = authCode 130 util.WrongAuthCount = 0 131 workspaceSession.Captcha = gulu.Rand.String(7) 132 133 maxAge := 0 // Default session expiration (browser session) 134 if rememberMe, ok := arg["rememberMe"].(bool); ok && rememberMe { 135 // Add a 'Remember me' checkbox when logging in to save a session https://github.com/siyuan-note/siyuan/pull/14964 136 maxAge = 60 * 60 * 24 * 30 // 30 days 137 } 138 ginSessions.Default(c).Options(ginSessions.Options{ 139 Path: "/", 140 Secure: util.SSL, 141 MaxAge: maxAge, 142 HttpOnly: true, 143 }) 144 145 logging.LogInfof("auth success [ip=%s, maxAge=%d]", util.GetRemoteAddr(c.Request), maxAge) 146 if err := session.Save(c); err != nil { 147 logging.LogErrorf("save session failed: " + err.Error()) 148 c.Status(http.StatusInternalServerError) 149 return 150 } 151} 152 153func GetCaptcha(c *gin.Context) { 154 img, err := captcha.New(100, 26, func(options *captcha.Options) { 155 options.CharPreset = "ABCDEFGHKLMNPQRSTUVWXYZ23456789" 156 options.Noise = 0.5 157 options.CurveNumber = 0 158 options.BackgroundColor = color.White 159 }) 160 if err != nil { 161 logging.LogErrorf("generates captcha failed: " + err.Error()) 162 c.Status(http.StatusInternalServerError) 163 return 164 } 165 166 session := util.GetSession(c) 167 workspaceSession := util.GetWorkspaceSession(session) 168 workspaceSession.Captcha = img.Text 169 if err = session.Save(c); err != nil { 170 logging.LogErrorf("save session failed: " + err.Error()) 171 c.Status(http.StatusInternalServerError) 172 return 173 } 174 175 if err = img.WriteImage(c.Writer); err != nil { 176 logging.LogErrorf("writes captcha image failed: " + err.Error()) 177 c.Status(http.StatusInternalServerError) 178 return 179 } 180 c.Status(http.StatusOK) 181} 182 183func CheckReadonly(c *gin.Context) { 184 if util.ReadOnly || IsReadOnlyRole(GetGinContextRole(c)) { 185 result := util.NewResult() 186 result.Code = -1 187 result.Msg = Conf.Language(34) 188 result.Data = map[string]interface{}{"closeTimeout": 5000} 189 c.JSON(http.StatusOK, result) 190 c.Abort() 191 return 192 } 193} 194 195func CheckAuth(c *gin.Context) { 196 // 已通过 JWT 认证 197 if role := GetGinContextRole(c); IsValidRole(role, []Role{ 198 RoleAdministrator, 199 RoleEditor, 200 RoleReader, 201 }) { 202 c.Next() 203 return 204 } 205 206 // 通过 API token (header: Authorization) 207 if authHeader := c.GetHeader("Authorization"); "" != authHeader { 208 var token string 209 if strings.HasPrefix(authHeader, "Token ") { 210 token = strings.TrimPrefix(authHeader, "Token ") 211 } else if strings.HasPrefix(authHeader, "token ") { 212 token = strings.TrimPrefix(authHeader, "token ") 213 } else if strings.HasPrefix(authHeader, "Bearer ") { 214 token = strings.TrimPrefix(authHeader, "Bearer ") 215 } else if strings.HasPrefix(authHeader, "bearer ") { 216 token = strings.TrimPrefix(authHeader, "bearer ") 217 } 218 219 if "" != token { 220 if Conf.Api.Token == token { 221 c.Set(RoleContextKey, RoleAdministrator) 222 c.Next() 223 return 224 } 225 226 c.JSON(http.StatusUnauthorized, map[string]interface{}{"code": -1, "msg": "Auth failed [header: Authorization]"}) 227 c.Abort() 228 return 229 } 230 } 231 232 // 通过 API token (query-params: token) 233 if token := c.Query("token"); "" != token { 234 if Conf.Api.Token == token { 235 c.Set(RoleContextKey, RoleAdministrator) 236 c.Next() 237 return 238 } 239 240 c.JSON(http.StatusUnauthorized, map[string]interface{}{"code": -1, "msg": "Auth failed [query: token]"}) 241 c.Abort() 242 return 243 } 244 245 //logging.LogInfof("check auth for [%s]", c.Request.RequestURI) 246 localhost := util.IsLocalHost(c.Request.RemoteAddr) 247 248 // 未设置访问授权码 249 if "" == Conf.AccessAuthCode { 250 // Skip the empty access authorization code check https://github.com/siyuan-note/siyuan/issues/9709 251 if util.SiyuanAccessAuthCodeBypass { 252 c.Set(RoleContextKey, RoleAdministrator) 253 c.Next() 254 return 255 } 256 257 // Authenticate requests with the Origin header other than 127.0.0.1 https://github.com/siyuan-note/siyuan/issues/9180 258 clientIP := c.ClientIP() 259 host := c.GetHeader("Host") 260 origin := c.GetHeader("Origin") 261 forwardedHost := c.GetHeader("X-Forwarded-Host") 262 if !localhost || 263 ("" != clientIP && !util.IsLocalHostname(clientIP)) || 264 ("" != host && !util.IsLocalHost(host)) || 265 ("" != origin && !util.IsLocalOrigin(origin) && !strings.HasPrefix(origin, "chrome-extension://")) || 266 ("" != forwardedHost && !util.IsLocalHost(forwardedHost)) { 267 c.JSON(http.StatusUnauthorized, map[string]interface{}{"code": -1, "msg": "Auth failed: for security reasons, please set [Access authorization code] when using non-127.0.0.1 access\n\n为安全起见,使用非 127.0.0.1 访问时请设置 [访问授权码]"}) 268 c.Abort() 269 return 270 } 271 272 c.Set(RoleContextKey, RoleAdministrator) 273 c.Next() 274 return 275 } 276 277 // 放过 /appearance/ 等(不要扩大到 /stage/ 否则鉴权会有问题) 278 if strings.HasPrefix(c.Request.RequestURI, "/appearance/") || 279 strings.HasPrefix(c.Request.RequestURI, "/stage/build/export/") || 280 strings.HasPrefix(c.Request.RequestURI, "/stage/protyle/") { 281 c.Next() 282 return 283 } 284 285 // 放过来自本机的某些请求 286 if localhost { 287 if strings.HasPrefix(c.Request.RequestURI, "/assets/") || strings.HasPrefix(c.Request.RequestURI, "/export/") { 288 c.Set(RoleContextKey, RoleAdministrator) 289 c.Next() 290 return 291 } 292 if strings.HasPrefix(c.Request.RequestURI, "/api/system/exit") { 293 c.Set(RoleContextKey, RoleAdministrator) 294 c.Next() 295 return 296 } 297 if strings.HasPrefix(c.Request.RequestURI, "/api/system/getNetwork") || strings.HasPrefix(c.Request.RequestURI, "/api/system/getWorkspaceInfo") { 298 c.Set(RoleContextKey, RoleAdministrator) 299 c.Next() 300 return 301 } 302 if strings.HasPrefix(c.Request.RequestURI, "/api/sync/performSync") { 303 if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container || util.ContainerHarmony == util.Container { 304 c.Set(RoleContextKey, RoleAdministrator) 305 c.Next() 306 return 307 } 308 } 309 } 310 311 // 通过 Cookie 312 session := util.GetSession(c) 313 workspaceSession := util.GetWorkspaceSession(session) 314 if workspaceSession.AccessAuthCode == Conf.AccessAuthCode { 315 c.Set(RoleContextKey, RoleAdministrator) 316 c.Next() 317 return 318 } 319 320 // 通过 BasicAuth (header: Authorization) 321 if username, password, ok := c.Request.BasicAuth(); ok { 322 // 使用访问授权码作为密码 323 if util.WorkspaceName == username && Conf.AccessAuthCode == password { 324 c.Set(RoleContextKey, RoleAdministrator) 325 c.Next() 326 return 327 } 328 } 329 330 // WebDAV BasicAuth Authenticate 331 if strings.HasPrefix(c.Request.RequestURI, "/webdav") || 332 strings.HasPrefix(c.Request.RequestURI, "/caldav") || 333 strings.HasPrefix(c.Request.RequestURI, "/carddav") { 334 c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue) 335 c.AbortWithStatus(http.StatusUnauthorized) 336 return 337 } 338 339 // 跳过访问授权页 340 if "/check-auth" == c.Request.URL.Path { 341 c.Next() 342 return 343 } 344 345 if workspaceSession.AccessAuthCode != Conf.AccessAuthCode { 346 userAgentHeader := c.GetHeader("User-Agent") 347 if strings.HasPrefix(userAgentHeader, "SiYuan/") || strings.HasPrefix(userAgentHeader, "Mozilla/") { 348 if "GET" != c.Request.Method || c.IsWebsocket() { 349 c.JSON(http.StatusUnauthorized, map[string]interface{}{"code": -1, "msg": Conf.Language(156)}) 350 c.Abort() 351 return 352 } 353 354 location := url.URL{} 355 queryParams := url.Values{} 356 queryParams.Set("to", c.Request.URL.String()) 357 location.RawQuery = queryParams.Encode() 358 location.Path = "/check-auth" 359 360 c.Redirect(http.StatusFound, location.String()) 361 c.Abort() 362 return 363 } 364 365 c.JSON(http.StatusUnauthorized, map[string]interface{}{"code": -1, "msg": "Auth failed [session]"}) 366 c.Abort() 367 return 368 } 369 370 c.Set(RoleContextKey, RoleAdministrator) 371 c.Next() 372} 373 374func CheckAdminRole(c *gin.Context) { 375 if IsAdminRoleContext(c) { 376 c.Next() 377 } else { 378 c.AbortWithStatus(http.StatusForbidden) 379 } 380} 381 382func CheckEditRole(c *gin.Context) { 383 if IsValidRole(GetGinContextRole(c), []Role{ 384 RoleAdministrator, 385 RoleEditor, 386 }) { 387 c.Next() 388 } else { 389 c.AbortWithStatus(http.StatusForbidden) 390 } 391} 392 393func CheckReadRole(c *gin.Context) { 394 if IsValidRole(GetGinContextRole(c), []Role{ 395 RoleAdministrator, 396 RoleEditor, 397 RoleReader, 398 }) { 399 c.Next() 400 } else { 401 c.AbortWithStatus(http.StatusForbidden) 402 } 403} 404 405var timingAPIs = map[string]int{ 406 "/api/search/fullTextSearchBlock": 200, // Monitor the search performance and suggest solutions https://github.com/siyuan-note/siyuan/issues/7873 407} 408 409func Timing(c *gin.Context) { 410 p := c.Request.URL.Path 411 tip, ok := timingAPIs[p] 412 if !ok { 413 c.Next() 414 return 415 } 416 417 timing := 15 * 1000 418 if timingEnv := os.Getenv("SIYUAN_PERFORMANCE_TIMING"); "" != timingEnv { 419 val, err := strconv.Atoi(timingEnv) 420 if err == nil { 421 timing = val 422 } 423 } 424 425 now := time.Now().UnixMilli() 426 c.Next() 427 elapsed := int(time.Now().UnixMilli() - now) 428 if timing < elapsed { 429 logging.LogWarnf("[%s] elapsed [%dms]", c.Request.RequestURI, elapsed) 430 util.PushMsg(Conf.Language(tip), 7000) 431 } 432} 433 434func Recover(c *gin.Context) { 435 defer logging.Recover() 436 c.Next() 437} 438 439var ( 440 requestingLock = sync.Mutex{} 441 requesting = map[string]*sync.Mutex{} 442) 443 444func ControlConcurrency(c *gin.Context) { 445 if websocket.IsWebSocketUpgrade(c.Request) { 446 c.Next() 447 return 448 } 449 450 reqPath := c.Request.URL.Path 451 452 // Improve the concurrency of the kernel data reading interfaces https://github.com/siyuan-note/siyuan/issues/10149 453 if strings.HasPrefix(reqPath, "/stage/") || 454 strings.HasPrefix(reqPath, "/assets/") || 455 strings.HasPrefix(reqPath, "/emojis/") || 456 strings.HasPrefix(reqPath, "/plugins/") || 457 strings.HasPrefix(reqPath, "/public/") || 458 strings.HasPrefix(reqPath, "/snippets/") || 459 strings.HasPrefix(reqPath, "/templates/") || 460 strings.HasPrefix(reqPath, "/widgets/") || 461 strings.HasPrefix(reqPath, "/appearance/") || 462 strings.HasPrefix(reqPath, "/export/") || 463 strings.HasPrefix(reqPath, "/history/") || 464 strings.HasPrefix(reqPath, "/api/query/") || 465 strings.HasPrefix(reqPath, "/api/search/") || 466 strings.HasPrefix(reqPath, "/api/network/") || 467 strings.HasPrefix(reqPath, "/api/broadcast/") || 468 strings.HasPrefix(reqPath, "/es/") { 469 c.Next() 470 return 471 } 472 473 parts := strings.Split(reqPath, "/") 474 function := parts[len(parts)-1] 475 if strings.HasPrefix(function, "get") || 476 strings.HasPrefix(function, "list") || 477 strings.HasPrefix(function, "search") || 478 strings.HasPrefix(function, "render") || 479 strings.HasPrefix(function, "ls") { 480 c.Next() 481 return 482 } 483 484 requestingLock.Lock() 485 mutex := requesting[reqPath] 486 if nil == mutex { 487 mutex = &sync.Mutex{} 488 requesting[reqPath] = mutex 489 } 490 requestingLock.Unlock() 491 492 mutex.Lock() 493 defer mutex.Unlock() 494 c.Next() 495}