2023-06-24 20:39:55 +08:00
// SiYuan - Refactor your thinking
2022-05-26 15:18:53 +08:00
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
2023-06-30 09:33:04 +08:00
"image/color"
2022-05-26 15:18:53 +08:00
"net/http"
2023-04-18 19:07:58 +08:00
"net/url"
2023-04-05 15:55:07 +08:00
"os"
"strconv"
2022-05-26 15:18:53 +08:00
"strings"
2023-12-22 10:00:40 +08:00
"sync"
2023-04-05 15:22:44 +08:00
"time"
2022-05-26 15:18:53 +08:00
"github.com/88250/gulu"
2025-06-04 15:54:31 +08:00
ginSessions "github.com/gin-contrib/sessions"
2022-05-26 15:18:53 +08:00
"github.com/gin-gonic/gin"
2023-12-22 10:21:15 +08:00
"github.com/gorilla/websocket"
2022-07-17 12:22:32 +08:00
"github.com/siyuan-note/logging"
2022-05-26 15:18:53 +08:00
"github.com/siyuan-note/siyuan/kernel/util"
2022-07-16 10:48:33 +08:00
"github.com/steambap/captcha"
2022-05-26 15:18:53 +08:00
)
2024-11-15 11:19:52 +08:00
var (
BasicAuthHeaderKey = "WWW-Authenticate"
BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\""
)
2022-05-26 15:18:53 +08:00
func LogoutAuth ( c * gin . Context ) {
ret := gulu . Ret . NewResult ( )
defer c . JSON ( http . StatusOK , ret )
if "" == Conf . AccessAuthCode {
ret . Code = - 1
ret . Msg = Conf . Language ( 86 )
ret . Data = map [ string ] interface { } { "closeTimeout" : 5000 }
return
}
2023-01-12 16:35:32 +08:00
session := util . GetSession ( c )
util . RemoveWorkspaceSession ( session )
2024-09-04 04:40:50 +03:00
if err := session . Save ( c ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "saves session failed: " + err . Error ( ) )
2022-05-26 15:18:53 +08:00
ret . Code = - 1
ret . Msg = "save session failed"
}
}
func LoginAuth ( c * gin . Context ) {
ret := gulu . Ret . NewResult ( )
defer c . JSON ( http . StatusOK , ret )
2022-07-16 10:48:33 +08:00
2022-05-26 15:18:53 +08:00
arg , ok := util . JsonArg ( c , ret )
if ! ok {
return
}
2022-07-16 10:48:33 +08:00
var inputCaptcha string
session := util . GetSession ( c )
2023-01-10 22:25:02 +08:00
workspaceSession := util . GetWorkspaceSession ( session )
2022-07-18 23:03:03 +08:00
if util . NeedCaptcha ( ) {
2022-07-16 12:20:09 +08:00
captchaArg := arg [ "captcha" ]
if nil == captchaArg {
ret . Code = 1
2022-07-16 16:30:11 +08:00
ret . Msg = Conf . Language ( 21 )
2024-03-11 22:45:01 +08:00
logging . LogWarnf ( "invalid captcha" )
2022-07-16 12:20:09 +08:00
return
}
inputCaptcha = captchaArg . ( string )
2022-07-18 23:03:03 +08:00
if "" == inputCaptcha {
ret . Code = 1
ret . Msg = Conf . Language ( 21 )
2024-03-11 22:45:01 +08:00
logging . LogWarnf ( "invalid captcha" )
2022-07-18 23:03:03 +08:00
return
}
2022-07-16 12:20:09 +08:00
2023-01-10 22:25:02 +08:00
if strings . ToLower ( workspaceSession . Captcha ) != strings . ToLower ( inputCaptcha ) {
2022-07-16 12:20:09 +08:00
ret . Code = 1
2022-07-16 16:30:11 +08:00
ret . Msg = Conf . Language ( 22 )
2024-03-11 22:45:01 +08:00
logging . LogWarnf ( "invalid captcha" )
2025-06-05 17:12:33 +08:00
workspaceSession . Captcha = gulu . Rand . String ( 7 ) // https://github.com/siyuan-note/siyuan/issues/13147
if err := session . Save ( c ) ; err != nil {
logging . LogErrorf ( "save session failed: " + err . Error ( ) )
c . Status ( http . StatusInternalServerError )
return
}
2022-07-16 12:20:09 +08:00
return
}
2022-07-16 10:48:33 +08:00
}
2022-05-26 15:18:53 +08:00
authCode := arg [ "authCode" ] . ( string )
2025-01-09 11:33:23 +08:00
authCode = strings . TrimSpace ( authCode )
authCode = util . RemoveInvalid ( authCode )
2022-05-26 15:18:53 +08:00
if Conf . AccessAuthCode != authCode {
ret . Code = - 1
ret . Msg = Conf . Language ( 83 )
2024-03-25 10:23:29 +08:00
logging . LogWarnf ( "invalid auth code [ip=%s]" , util . GetRemoteAddr ( c . Request ) )
2022-07-16 10:48:33 +08:00
2022-07-18 23:03:03 +08:00
util . WrongAuthCount ++
2023-01-10 22:25:02 +08:00
workspaceSession . Captcha = gulu . Rand . String ( 7 )
2022-07-18 23:03:03 +08:00
if util . NeedCaptcha ( ) {
2022-07-16 10:48:33 +08:00
ret . Code = 1 // 需要渲染验证码
}
2024-09-04 04:40:50 +03:00
if err := session . Save ( c ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "save session failed: " + err . Error ( ) )
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusInternalServerError )
2022-07-16 10:48:33 +08:00
return
}
2022-05-26 15:18:53 +08:00
return
}
2023-01-10 22:25:02 +08:00
workspaceSession . AccessAuthCode = authCode
2022-07-18 23:03:03 +08:00
util . WrongAuthCount = 0
2023-01-10 22:25:02 +08:00
workspaceSession . Captcha = gulu . Rand . String ( 7 )
2025-06-04 15:54:31 +08:00
maxAge := 0 // Default session expiration (browser session)
2025-06-04 03:35:31 -04:00
if rememberMe , ok := arg [ "rememberMe" ] . ( bool ) ; ok && rememberMe {
2025-06-04 15:54:31 +08:00
// Add a 'Remember me' checkbox when logging in to save a session https://github.com/siyuan-note/siyuan/pull/14964
maxAge = 60 * 60 * 24 * 30 // 30 days
2025-06-04 03:35:31 -04:00
}
2025-06-04 15:54:31 +08:00
ginSessions . Default ( c ) . Options ( ginSessions . Options {
Path : "/" ,
Secure : util . SSL ,
MaxAge : maxAge ,
HttpOnly : true ,
} )
2025-06-04 03:35:31 -04:00
2025-06-04 15:54:31 +08:00
logging . LogInfof ( "auth success [ip=%s, maxAge=%d]" , util . GetRemoteAddr ( c . Request ) , maxAge )
2024-09-04 04:40:50 +03:00
if err := session . Save ( c ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "save session failed: " + err . Error ( ) )
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusInternalServerError )
2022-07-16 10:48:33 +08:00
return
}
}
func GetCaptcha ( c * gin . Context ) {
2022-07-16 12:20:09 +08:00
img , err := captcha . New ( 100 , 26 , func ( options * captcha . Options ) {
2022-07-16 16:41:40 +08:00
options . CharPreset = "ABCDEFGHKLMNPQRSTUVWXYZ23456789"
2022-07-16 12:20:09 +08:00
options . Noise = 0.5
options . CurveNumber = 0
2023-06-30 09:33:04 +08:00
options . BackgroundColor = color . White
2022-07-16 12:20:09 +08:00
} )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "generates captcha failed: " + err . Error ( ) )
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusInternalServerError )
2022-07-16 10:48:33 +08:00
return
}
session := util . GetSession ( c )
2023-01-10 22:25:02 +08:00
workspaceSession := util . GetWorkspaceSession ( session )
workspaceSession . Captcha = img . Text
2024-09-04 04:40:50 +03:00
if err = session . Save ( c ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "save session failed: " + err . Error ( ) )
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusInternalServerError )
2022-07-16 10:48:33 +08:00
return
}
2024-09-04 04:40:50 +03:00
if err = img . WriteImage ( c . Writer ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "writes captcha image failed: " + err . Error ( ) )
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusInternalServerError )
2022-05-26 15:18:53 +08:00
return
}
2023-11-12 08:52:34 +08:00
c . Status ( http . StatusOK )
2022-05-26 15:18:53 +08:00
}
func CheckReadonly ( c * gin . Context ) {
if util . ReadOnly {
result := util . NewResult ( )
result . Code = - 1
result . Msg = Conf . Language ( 34 )
result . Data = map [ string ] interface { } { "closeTimeout" : 5000 }
2023-11-12 08:52:34 +08:00
c . JSON ( http . StatusOK , result )
2022-05-26 15:18:53 +08:00
c . Abort ( )
return
}
}
func CheckAuth ( c * gin . Context ) {
2024-06-12 21:03:51 +08:00
// 已通过 JWT 认证
if role := GetGinContextRole ( c ) ; IsValidRole ( role , [ ] Role {
RoleAdministrator ,
RoleEditor ,
RoleReader ,
} ) {
c . Next ( )
return
}
2025-04-12 16:22:01 +08:00
// 通过 API token (header: Authorization)
if authHeader := c . GetHeader ( "Authorization" ) ; "" != authHeader {
var token string
if strings . HasPrefix ( authHeader , "Token " ) {
token = strings . TrimPrefix ( authHeader , "Token " )
} else if strings . HasPrefix ( authHeader , "token " ) {
token = strings . TrimPrefix ( authHeader , "token " )
} else if strings . HasPrefix ( authHeader , "Bearer " ) {
token = strings . TrimPrefix ( authHeader , "Bearer " )
} else if strings . HasPrefix ( authHeader , "bearer " ) {
token = strings . TrimPrefix ( authHeader , "bearer " )
}
if "" != token {
if Conf . Api . Token == token {
c . Set ( RoleContextKey , RoleAdministrator )
c . Next ( )
return
}
c . JSON ( http . StatusUnauthorized , map [ string ] interface { } { "code" : - 1 , "msg" : "Auth failed [header: Authorization]" } )
c . Abort ( )
return
}
}
// 通过 API token (query-params: token)
if token := c . Query ( "token" ) ; "" != token {
if Conf . Api . Token == token {
c . Set ( RoleContextKey , RoleAdministrator )
c . Next ( )
return
}
c . JSON ( http . StatusUnauthorized , map [ string ] interface { } { "code" : - 1 , "msg" : "Auth failed [query: token]" } )
c . Abort ( )
return
}
2022-07-17 12:22:32 +08:00
//logging.LogInfof("check auth for [%s]", c.Request.RequestURI)
2023-11-12 08:52:34 +08:00
localhost := util . IsLocalHost ( c . Request . RemoteAddr )
2022-05-26 15:18:53 +08:00
2023-11-12 08:52:34 +08:00
// 未设置访问授权码
2022-10-08 11:05:15 +08:00
if "" == Conf . AccessAuthCode {
2023-11-22 16:55:44 +08:00
// Skip the empty access authorization code check https://github.com/siyuan-note/siyuan/issues/9709
2023-11-22 17:00:46 +08:00
if util . SiyuanAccessAuthCodeBypass {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2023-11-22 16:55:44 +08:00
c . Next ( )
return
}
2023-11-12 08:52:34 +08:00
// Authenticate requests with the Origin header other than 127.0.0.1 https://github.com/siyuan-note/siyuan/issues/9180
2023-11-21 21:45:44 +08:00
clientIP := c . ClientIP ( )
2023-11-12 08:52:34 +08:00
host := c . GetHeader ( "Host" )
origin := c . GetHeader ( "Origin" )
forwardedHost := c . GetHeader ( "X-Forwarded-Host" )
if ! localhost ||
2023-11-21 21:45:44 +08:00
( "" != clientIP && ! util . IsLocalHostname ( clientIP ) ) ||
2023-11-12 08:52:34 +08:00
( "" != host && ! util . IsLocalHost ( host ) ) ||
( "" != origin && ! util . IsLocalOrigin ( origin ) && ! strings . HasPrefix ( origin , "chrome-extension://" ) ) ||
( "" != forwardedHost && ! util . IsLocalHost ( forwardedHost ) ) {
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 访问时请设置 [访问授权码]" } )
c . Abort ( )
return
2023-10-10 16:52:40 +08:00
}
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2022-10-08 11:05:15 +08:00
c . Next ( )
return
}
2025-07-25 11:21:23 +08:00
// 放过静态资源请求
if strings . HasPrefix ( c . Request . RequestURI , "/appearance/" ) || strings . HasPrefix ( c . Request . RequestURI , "/stage/" ) {
2022-05-26 15:18:53 +08:00
c . Next ( )
return
}
2022-06-11 10:41:14 +08:00
// 放过来自本机的某些请求
2023-11-12 08:52:34 +08:00
if localhost {
2024-09-11 22:30:47 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/assets/" ) || strings . HasPrefix ( c . Request . RequestURI , "/export/" ) {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2022-06-11 10:41:14 +08:00
c . Next ( )
return
}
if strings . HasPrefix ( c . Request . RequestURI , "/api/system/exit" ) {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2022-06-11 10:41:14 +08:00
c . Next ( )
return
}
2024-11-29 10:56:32 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/api/system/getNetwork" ) || strings . HasPrefix ( c . Request . RequestURI , "/api/system/getWorkspaceInfo" ) {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2023-11-24 21:47:51 +08:00
c . Next ( )
return
}
2024-01-20 12:09:47 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/api/sync/performSync" ) {
2024-11-22 20:15:47 +08:00
if util . ContainerIOS == util . Container || util . ContainerAndroid == util . Container || util . ContainerHarmony == util . Container {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2024-01-20 12:09:47 +08:00
c . Next ( )
return
}
}
2022-05-26 15:18:53 +08:00
}
// 通过 Cookie
session := util . GetSession ( c )
2023-01-10 22:25:02 +08:00
workspaceSession := util . GetWorkspaceSession ( session )
if workspaceSession . AccessAuthCode == Conf . AccessAuthCode {
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2022-05-26 15:18:53 +08:00
c . Next ( )
return
}
2024-09-08 10:00:09 +08:00
// 通过 BasicAuth (header: Authorization)
if username , password , ok := c . Request . BasicAuth ( ) ; ok {
// 使用访问授权码作为密码
if util . WorkspaceName == username && Conf . AccessAuthCode == password {
c . Set ( RoleContextKey , RoleAdministrator )
c . Next ( )
return
}
}
// WebDAV BasicAuth Authenticate
2024-12-01 23:20:47 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/webdav" ) ||
strings . HasPrefix ( c . Request . RequestURI , "/caldav" ) ||
strings . HasPrefix ( c . Request . RequestURI , "/carddav" ) {
2024-11-15 11:19:52 +08:00
c . Header ( BasicAuthHeaderKey , BasicAuthHeaderValue )
2024-09-08 10:00:09 +08:00
c . AbortWithStatus ( http . StatusUnauthorized )
return
}
// 跳过访问授权页
if "/check-auth" == c . Request . URL . Path {
2022-05-26 15:18:53 +08:00
c . Next ( )
return
}
2023-01-10 22:25:02 +08:00
if workspaceSession . AccessAuthCode != Conf . AccessAuthCode {
2022-05-26 15:18:53 +08:00
userAgentHeader := c . GetHeader ( "User-Agent" )
if strings . HasPrefix ( userAgentHeader , "SiYuan/" ) || strings . HasPrefix ( userAgentHeader , "Mozilla/" ) {
2023-11-21 21:45:44 +08:00
if "GET" != c . Request . Method || c . IsWebsocket ( ) {
2023-11-12 08:52:34 +08:00
c . JSON ( http . StatusUnauthorized , map [ string ] interface { } { "code" : - 1 , "msg" : Conf . Language ( 156 ) } )
2022-07-06 22:13:49 +08:00
c . Abort ( )
return
}
2023-04-18 19:07:58 +08:00
location := url . URL { }
queryParams := url . Values { }
queryParams . Set ( "to" , c . Request . URL . String ( ) )
location . RawQuery = queryParams . Encode ( )
location . Path = "/check-auth"
2023-11-12 08:52:34 +08:00
c . Redirect ( http . StatusFound , location . String ( ) )
2022-05-26 15:18:53 +08:00
c . Abort ( )
return
}
2024-03-28 10:35:02 +08:00
c . JSON ( http . StatusUnauthorized , map [ string ] interface { } { "code" : - 1 , "msg" : "Auth failed [session]" } )
2022-05-26 15:18:53 +08:00
c . Abort ( )
return
}
2024-06-12 21:03:51 +08:00
c . Set ( RoleContextKey , RoleAdministrator )
2022-05-26 15:18:53 +08:00
c . Next ( )
}
2023-04-05 15:22:44 +08:00
2024-06-12 21:03:51 +08:00
func CheckAdminRole ( c * gin . Context ) {
2024-07-07 22:39:53 +08:00
if IsAdminRoleContext ( c ) {
2024-06-12 21:03:51 +08:00
c . Next ( )
} else {
c . AbortWithStatus ( http . StatusForbidden )
}
}
func CheckEditRole ( c * gin . Context ) {
if IsValidRole ( GetGinContextRole ( c ) , [ ] Role {
RoleAdministrator ,
RoleEditor ,
} ) {
c . Next ( )
} else {
c . AbortWithStatus ( http . StatusForbidden )
}
}
func CheckReadRole ( c * gin . Context ) {
if IsValidRole ( GetGinContextRole ( c ) , [ ] Role {
RoleAdministrator ,
RoleEditor ,
RoleReader ,
} ) {
c . Next ( )
} else {
c . AbortWithStatus ( http . StatusForbidden )
}
}
2023-04-05 15:55:07 +08:00
var timingAPIs = map [ string ] int {
"/api/search/fullTextSearchBlock" : 200 , // Monitor the search performance and suggest solutions https://github.com/siyuan-note/siyuan/issues/7873
2023-04-05 15:22:44 +08:00
}
func Timing ( c * gin . Context ) {
2023-04-05 15:55:07 +08:00
p := c . Request . URL . Path
tip , ok := timingAPIs [ p ]
if ! ok {
c . Next ( )
return
}
timing := 15 * 1000
if timingEnv := os . Getenv ( "SIYUAN_PERFORMANCE_TIMING" ) ; "" != timingEnv {
val , err := strconv . Atoi ( timingEnv )
2024-09-04 04:40:50 +03:00
if err == nil {
2023-04-05 15:55:07 +08:00
timing = val
}
}
2023-04-05 15:22:44 +08:00
now := time . Now ( ) . UnixMilli ( )
c . Next ( )
2023-04-05 15:55:07 +08:00
elapsed := int ( time . Now ( ) . UnixMilli ( ) - now )
if timing < elapsed {
logging . LogWarnf ( "[%s] elapsed [%dms]" , c . Request . RequestURI , elapsed )
util . PushMsg ( Conf . Language ( tip ) , 7000 )
}
2023-04-05 15:22:44 +08:00
}
func Recover ( c * gin . Context ) {
2024-06-28 21:11:52 +08:00
defer logging . Recover ( )
2023-04-05 15:22:44 +08:00
c . Next ( )
}
2023-12-22 10:00:40 +08:00
var (
requestingLock = sync . Mutex { }
requesting = map [ string ] * sync . Mutex { }
)
func ControlConcurrency ( c * gin . Context ) {
2023-12-22 10:21:15 +08:00
if websocket . IsWebSocketUpgrade ( c . Request ) {
c . Next ( )
return
}
2024-01-11 22:46:54 +08:00
reqPath := c . Request . URL . Path
// Improve the concurrency of the kernel data reading interfaces https://github.com/siyuan-note/siyuan/issues/10149
2025-01-01 21:51:45 +08:00
if strings . HasPrefix ( reqPath , "/stage/" ) ||
strings . HasPrefix ( reqPath , "/assets/" ) ||
strings . HasPrefix ( reqPath , "/emojis/" ) ||
strings . HasPrefix ( reqPath , "/plugins/" ) ||
strings . HasPrefix ( reqPath , "/public/" ) ||
strings . HasPrefix ( reqPath , "/snippets/" ) ||
strings . HasPrefix ( reqPath , "/templates/" ) ||
strings . HasPrefix ( reqPath , "/widgets/" ) ||
strings . HasPrefix ( reqPath , "/appearance/" ) ||
strings . HasPrefix ( reqPath , "/export/" ) ||
strings . HasPrefix ( reqPath , "/history/" ) ||
strings . HasPrefix ( reqPath , "/api/query/" ) ||
strings . HasPrefix ( reqPath , "/api/search/" ) ||
strings . HasPrefix ( reqPath , "/api/network/" ) ||
strings . HasPrefix ( reqPath , "/api/broadcast/" ) ||
strings . HasPrefix ( reqPath , "/es/" ) {
2024-01-11 22:46:54 +08:00
c . Next ( )
return
}
parts := strings . Split ( reqPath , "/" )
function := parts [ len ( parts ) - 1 ]
2025-01-01 21:51:45 +08:00
if strings . HasPrefix ( function , "get" ) ||
strings . HasPrefix ( function , "list" ) ||
strings . HasPrefix ( function , "search" ) ||
strings . HasPrefix ( function , "render" ) ||
strings . HasPrefix ( function , "ls" ) {
2024-01-11 22:46:54 +08:00
c . Next ( )
return
}
2023-12-22 10:00:40 +08:00
requestingLock . Lock ( )
2024-01-11 22:46:54 +08:00
mutex := requesting [ reqPath ]
2023-12-22 10:00:40 +08:00
if nil == mutex {
mutex = & sync . Mutex { }
2024-01-11 22:46:54 +08:00
requesting [ reqPath ] = mutex
2023-12-22 10:00:40 +08:00
}
requestingLock . Unlock ( )
mutex . Lock ( )
2023-12-26 19:57:31 +08:00
defer mutex . Unlock ( )
2023-12-22 10:00:40 +08:00
c . Next ( )
}