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 server
import (
2022-12-08 23:19:03 +08:00
"bytes"
2022-10-24 22:07:44 +08:00
"fmt"
2022-12-08 23:19:03 +08:00
"html/template"
2024-06-12 21:03:51 +08:00
"mime"
2022-10-24 21:52:50 +08:00
"net"
2022-05-26 15:18:53 +08:00
"net/http"
"net/http/pprof"
2022-10-25 15:31:48 +08:00
"net/url"
2022-05-26 15:18:53 +08:00
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/88250/gulu"
2024-12-01 23:20:47 +08:00
"github.com/emersion/go-webdav/caldav"
2024-11-15 11:19:52 +08:00
"github.com/emersion/go-webdav/carddav"
2022-05-26 15:18:53 +08:00
"github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"
2025-06-05 17:12:33 +08:00
"github.com/gin-contrib/sessions/cookie"
2022-05-26 15:18:53 +08:00
"github.com/gin-gonic/gin"
2023-06-01 22:10:14 +08:00
"github.com/mssola/useragent"
2022-09-16 10:47:25 +08:00
"github.com/olahol/melody"
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/api"
"github.com/siyuan-note/siyuan/kernel/cmd"
"github.com/siyuan-note/siyuan/kernel/model"
2024-06-12 21:03:51 +08:00
"github.com/siyuan-note/siyuan/kernel/server/proxy"
2022-05-26 15:18:53 +08:00
"github.com/siyuan-note/siyuan/kernel/util"
2024-09-08 10:00:09 +08:00
"golang.org/x/net/webdav"
2022-05-26 15:18:53 +08:00
)
2024-11-15 11:19:52 +08:00
const (
2024-12-01 23:20:47 +08:00
MethodMkCol = "MKCOL"
2024-11-15 11:19:52 +08:00
MethodCopy = "COPY"
MethodMove = "MOVE"
MethodLock = "LOCK"
MethodUnlock = "UNLOCK"
MethodPropFind = "PROPFIND"
MethodPropPatch = "PROPPATCH"
MethodReport = "REPORT"
)
2024-06-12 21:03:51 +08:00
var (
2025-06-05 17:12:33 +08:00
sessionStore = cookie . NewStore ( [ ] byte ( "ATN51UlxVq1Gcvdf" ) )
2024-11-15 20:32:54 +08:00
2024-11-15 11:19:52 +08:00
HttpMethods = [ ] string {
http . MethodGet ,
http . MethodHead ,
http . MethodPost ,
http . MethodPut ,
http . MethodPatch ,
http . MethodDelete ,
http . MethodConnect ,
http . MethodOptions ,
http . MethodTrace ,
}
WebDavMethods = [ ] string {
http . MethodOptions ,
http . MethodHead ,
http . MethodGet ,
http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
2024-12-01 23:20:47 +08:00
MethodMkCol ,
2024-11-15 11:19:52 +08:00
MethodCopy ,
MethodMove ,
MethodLock ,
MethodUnlock ,
MethodPropFind ,
MethodPropPatch ,
}
2024-12-01 23:20:47 +08:00
CalDavMethods = [ ] string {
http . MethodOptions ,
http . MethodHead ,
http . MethodGet ,
http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
MethodMkCol ,
MethodCopy ,
MethodMove ,
// MethodLock,
// MethodUnlock,
MethodPropFind ,
MethodPropPatch ,
MethodReport ,
}
2024-11-15 11:19:52 +08:00
CardDavMethods = [ ] string {
http . MethodOptions ,
http . MethodHead ,
http . MethodGet ,
http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
2024-12-01 23:20:47 +08:00
MethodMkCol ,
MethodCopy ,
MethodMove ,
2024-11-15 11:19:52 +08:00
// MethodLock,
// MethodUnlock,
MethodPropFind ,
MethodPropPatch ,
MethodReport ,
2024-09-08 10:00:09 +08:00
}
2024-06-12 21:03:51 +08:00
)
2022-05-26 15:18:53 +08:00
func Serve ( fastMode bool ) {
gin . SetMode ( gin . ReleaseMode )
ginServer := gin . New ( )
2024-04-23 17:06:39 +08:00
ginServer . UseH2C = true
2022-05-27 12:56:45 +08:00
ginServer . MaxMultipartMemory = 1024 * 1024 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023
2023-04-05 15:22:44 +08:00
ginServer . Use (
2023-12-22 10:00:40 +08:00
model . ControlConcurrency , // 请求串行化 Concurrency control when requesting the kernel API https://github.com/siyuan-note/siyuan/issues/9939
2023-04-05 15:22:44 +08:00
model . Timing ,
model . Recover ,
corsMiddleware ( ) , // 后端服务支持 CORS 预检请求验证 https://github.com/siyuan-note/siyuan/pull/5593
2024-06-12 21:03:51 +08:00
jwtMiddleware , // 解析 JWT https://github.com/siyuan-note/siyuan/issues/11364
2024-12-07 11:13:25 +08:00
gzip . Gzip ( gzip . DefaultCompression , gzip . WithExcludedExtensions ( [ ] string { ".pdf" , ".mp3" , ".wav" , ".ogg" , ".mov" , ".weba" , ".mkv" , ".mp4" , ".webm" , ".flac" } ) ) ,
2023-04-05 15:22:44 +08:00
)
2022-05-26 15:18:53 +08:00
2024-11-15 20:32:54 +08:00
sessionStore . Options ( sessions . Options {
2022-05-26 15:18:53 +08:00
Path : "/" ,
Secure : util . SSL ,
//MaxAge: 60 * 60 * 24 * 7, // 默认是 Session
HttpOnly : true ,
} )
2024-11-15 20:32:54 +08:00
ginServer . Use ( sessions . Sessions ( "siyuan" , sessionStore ) )
2022-05-26 15:18:53 +08:00
2023-01-26 11:56:06 +08:00
serveDebug ( ginServer )
2022-05-26 15:18:53 +08:00
serveAssets ( ginServer )
serveAppearance ( ginServer )
serveWebSocket ( ginServer )
2024-09-08 10:00:09 +08:00
serveWebDAV ( ginServer )
2024-12-01 23:20:47 +08:00
serveCalDAV ( ginServer )
2024-11-15 11:19:52 +08:00
serveCardDAV ( ginServer )
2022-05-26 15:18:53 +08:00
serveExport ( ginServer )
serveWidgets ( ginServer )
2023-05-05 16:20:23 +08:00
servePlugins ( ginServer )
2022-05-26 15:18:53 +08:00
serveEmojis ( ginServer )
2022-09-02 10:36:56 +08:00
serveTemplates ( ginServer )
2023-06-22 16:30:04 +08:00
servePublic ( ginServer )
2024-06-12 21:03:51 +08:00
serveSnippets ( ginServer )
2023-06-03 17:42:01 +08:00
serveRepoDiff ( ginServer )
2024-06-12 21:03:51 +08:00
serveCheckAuth ( ginServer )
serveFixedStaticFiles ( ginServer )
2022-05-26 15:18:53 +08:00
api . ServeAPI ( ginServer )
2022-10-24 21:52:50 +08:00
var host string
2022-08-31 12:25:55 +08:00
if model . Conf . System . NetworkServe || util . ContainerDocker == util . Container {
2022-10-24 21:52:50 +08:00
host = "0.0.0.0"
2022-05-26 15:18:53 +08:00
} else {
2022-10-24 21:52:50 +08:00
host = "127.0.0.1"
2022-05-26 15:18:53 +08:00
}
2022-10-24 21:52:50 +08:00
2022-10-25 15:31:48 +08:00
ln , err := net . Listen ( "tcp" , host + ":" + util . ServerPort )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-10-24 21:52:50 +08:00
if ! fastMode {
logging . LogErrorf ( "boot kernel failed: %s" , err )
2023-03-18 18:10:06 +08:00
os . Exit ( logging . ExitCodeUnavailablePort )
2022-10-24 21:52:50 +08:00
}
2022-10-28 20:27:39 +08:00
// fast 模式下启动失败则直接返回
return
2022-10-24 21:52:50 +08:00
}
_ , port , err := net . SplitHostPort ( ln . Addr ( ) . String ( ) )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-10-24 21:52:50 +08:00
if ! fastMode {
logging . LogErrorf ( "boot kernel failed: %s" , err )
2023-03-18 18:10:06 +08:00
os . Exit ( logging . ExitCodeUnavailablePort )
2022-10-24 21:52:50 +08:00
}
}
2022-10-26 08:54:58 +08:00
util . ServerPort = port
2022-10-24 21:52:50 +08:00
2024-06-12 21:03:51 +08:00
util . ServerURL , err = url . Parse ( "http://127.0.0.1:" + port )
if err != nil {
logging . LogErrorf ( "parse server url failed: %s" , err )
}
2022-10-24 22:07:44 +08:00
pid := fmt . Sprintf ( "%d" , os . Getpid ( ) )
if ! fastMode {
rewritePortJSON ( pid , port )
}
2023-01-06 11:48:03 +08:00
logging . LogInfof ( "kernel [pid=%s] http server [%s] is booting" , pid , host + ":" + port )
2022-05-26 15:18:53 +08:00
util . HttpServing = true
2022-10-24 21:52:50 +08:00
2024-06-12 21:03:51 +08:00
go util . HookUILoaded ( )
2022-10-25 15:31:48 +08:00
go func ( ) {
2023-01-06 11:19:19 +08:00
time . Sleep ( 1 * time . Second )
2024-06-12 21:03:51 +08:00
go proxy . InitFixedPortService ( host )
go proxy . InitPublishService ( )
// 反代服务器启动失败不影响核心服务器启动
2022-10-25 15:31:48 +08:00
} ( )
2024-09-04 04:40:50 +03:00
if err = http . Serve ( ln , ginServer . Handler ( ) ) ; err != nil {
2022-05-26 15:18:53 +08:00
if ! fastMode {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "boot kernel failed: %s" , err )
2023-03-18 18:10:06 +08:00
os . Exit ( logging . ExitCodeUnavailablePort )
2022-05-26 15:18:53 +08:00
}
}
}
2022-10-24 22:07:44 +08:00
func rewritePortJSON ( pid , port string ) {
portJSON := filepath . Join ( util . HomeDir , ".config" , "siyuan" , "port.json" )
pidPorts := map [ string ] string { }
var data [ ] byte
var err error
if gulu . File . IsExist ( portJSON ) {
data , err = os . ReadFile ( portJSON )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-10-24 22:07:44 +08:00
logging . LogWarnf ( "read port.json failed: %s" , err )
} else {
2024-09-04 04:40:50 +03:00
if err = gulu . JSON . UnmarshalJSON ( data , & pidPorts ) ; err != nil {
2022-10-24 22:07:44 +08:00
logging . LogWarnf ( "unmarshal port.json failed: %s" , err )
}
}
}
pidPorts [ pid ] = port
2024-09-04 04:40:50 +03:00
if data , err = gulu . JSON . MarshalIndentJSON ( pidPorts , "" , " " ) ; err != nil {
2022-10-24 22:07:44 +08:00
logging . LogWarnf ( "marshal port.json failed: %s" , err )
} else {
2024-09-04 04:40:50 +03:00
if err = os . WriteFile ( portJSON , data , 0644 ) ; err != nil {
2022-10-24 22:07:44 +08:00
logging . LogWarnf ( "write port.json failed: %s" , err )
}
}
}
2022-05-26 15:18:53 +08:00
func serveExport ( ginServer * gin . Engine ) {
2024-08-08 10:58:43 +08:00
// Potential data export disclosure security vulnerability https://github.com/siyuan-note/siyuan/issues/12213
2024-08-08 10:56:31 +08:00
exportGroup := ginServer . Group ( "/export/" , model . CheckAuth )
exportGroup . Static ( "/" , filepath . Join ( util . TempDir , "export" ) )
2022-05-26 15:18:53 +08:00
}
func serveWidgets ( ginServer * gin . Engine ) {
ginServer . Static ( "/widgets/" , filepath . Join ( util . DataDir , "widgets" ) )
}
2023-05-05 16:20:23 +08:00
func servePlugins ( ginServer * gin . Engine ) {
ginServer . Static ( "/plugins/" , filepath . Join ( util . DataDir , "plugins" ) )
}
2022-05-26 15:18:53 +08:00
func serveEmojis ( ginServer * gin . Engine ) {
ginServer . Static ( "/emojis/" , filepath . Join ( util . DataDir , "emojis" ) )
}
2022-09-01 19:37:33 +08:00
func serveTemplates ( ginServer * gin . Engine ) {
ginServer . Static ( "/templates/" , filepath . Join ( util . DataDir , "templates" ) )
}
2023-06-22 16:30:04 +08:00
func servePublic ( ginServer * gin . Engine ) {
// Support directly access `data/public/*` contents via URL link https://github.com/siyuan-note/siyuan/issues/8593
ginServer . Static ( "/public/" , filepath . Join ( util . DataDir , "public" ) )
}
2024-06-12 21:03:51 +08:00
func serveSnippets ( ginServer * gin . Engine ) {
ginServer . Handle ( "GET" , "/snippets/*filepath" , func ( c * gin . Context ) {
filePath := strings . TrimPrefix ( c . Request . URL . Path , "/snippets/" )
ext := filepath . Ext ( filePath )
name := strings . TrimSuffix ( filePath , ext )
confSnippets , err := model . LoadSnippets ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-06-12 21:03:51 +08:00
logging . LogErrorf ( "load snippets failed: %s" , err )
c . Status ( http . StatusNotFound )
return
}
for _ , s := range confSnippets {
if s . Name == name && ( "" != ext && s . Type == ext [ 1 : ] ) {
c . Header ( "Content-Type" , mime . TypeByExtension ( ext ) )
c . String ( http . StatusOK , s . Content )
return
}
}
2023-04-18 19:07:58 +08:00
2024-06-12 21:03:51 +08:00
// 没有在配置文件中命中时在文件系统上查找
filePath = filepath . Join ( util . SnippetsPath , filePath )
c . File ( filePath )
} )
}
func serveAppearance ( ginServer * gin . Engine ) {
2022-05-26 15:18:53 +08:00
siyuan := ginServer . Group ( "" , model . CheckAuth )
siyuan . Handle ( "GET" , "/" , func ( c * gin . Context ) {
2023-06-02 09:39:08 +08:00
userAgentHeader := c . GetHeader ( "User-Agent" )
2023-11-20 17:50:39 +08:00
logging . LogInfof ( "serving [/] for user-agent [%s]" , userAgentHeader )
2022-05-26 15:18:53 +08:00
2023-11-20 17:50:39 +08:00
// Carry query parameters when redirecting
2023-04-18 19:07:58 +08:00
location := url . URL { }
queryParams := c . Request . URL . Query ( )
queryParams . Set ( "r" , gulu . Rand . String ( 7 ) )
location . RawQuery = queryParams . Encode ( )
2023-06-02 09:39:08 +08:00
if strings . Contains ( userAgentHeader , "Electron" ) {
2023-04-18 19:07:58 +08:00
location . Path = "/stage/build/app/"
2023-06-08 22:48:32 +08:00
} else if strings . Contains ( userAgentHeader , "Pad" ) ||
( strings . ContainsAny ( userAgentHeader , "Android" ) && ! strings . Contains ( userAgentHeader , "Mobile" ) ) {
// 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
2023-06-02 08:58:30 +08:00
location . Path = "/stage/build/desktop/"
2023-04-18 19:07:58 +08:00
} else {
2023-06-02 09:39:08 +08:00
if idx := strings . Index ( userAgentHeader , "Mozilla/" ) ; 0 < idx {
userAgentHeader = userAgentHeader [ idx : ]
}
ua := useragent . New ( userAgentHeader )
2023-06-01 22:10:14 +08:00
if ua . Mobile ( ) {
2023-06-02 08:58:30 +08:00
location . Path = "/stage/build/mobile/"
2023-06-01 22:10:14 +08:00
} else {
location . Path = "/stage/build/desktop/"
}
2022-05-26 15:18:53 +08:00
}
2023-04-18 19:07:58 +08:00
c . Redirect ( 302 , location . String ( ) )
2022-05-26 15:18:53 +08:00
} )
appearancePath := util . AppearancePath
if "dev" == util . Mode {
appearancePath = filepath . Join ( util . WorkingDir , "appearance" )
}
siyuan . GET ( "/appearance/*filepath" , func ( c * gin . Context ) {
filePath := filepath . Join ( appearancePath , strings . TrimPrefix ( c . Request . URL . Path , "/appearance/" ) )
2022-06-30 10:16:33 +08:00
if strings . HasSuffix ( c . Request . URL . Path , "/theme.js" ) {
if ! gulu . File . IsExist ( filePath ) {
// 主题 js 不存在时生成空内容返回
c . Data ( 200 , "application/x-javascript" , nil )
return
}
} else if strings . Contains ( c . Request . URL . Path , "/langs/" ) && strings . HasSuffix ( c . Request . URL . Path , ".json" ) {
lang := path . Base ( c . Request . URL . Path )
lang = strings . TrimSuffix ( lang , ".json" )
if "zh_CN" != lang && "en_US" != lang {
// 多语言配置缺失项使用对应英文配置项补齐 https://github.com/siyuan-note/siyuan/issues/5322
enUSFilePath := filepath . Join ( appearancePath , "langs" , "en_US.json" )
enUSData , err := os . ReadFile ( enUSFilePath )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-03-17 21:43:38 +08:00
logging . LogErrorf ( "read en_US.json [%s] failed: %s" , enUSFilePath , err )
util . ReportFileSysFatalError ( err )
2022-06-30 10:16:33 +08:00
return
}
enUSMap := map [ string ] interface { } { }
2024-09-04 04:40:50 +03:00
if err = gulu . JSON . UnmarshalJSON ( enUSData , & enUSMap ) ; err != nil {
2023-03-17 21:43:38 +08:00
logging . LogErrorf ( "unmarshal en_US.json [%s] failed: %s" , enUSFilePath , err )
util . ReportFileSysFatalError ( err )
2022-06-30 10:16:33 +08:00
return
}
for {
data , err := os . ReadFile ( filePath )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-06-30 10:16:33 +08:00
c . JSON ( 200 , enUSMap )
return
}
langMap := map [ string ] interface { } { }
2024-09-04 04:40:50 +03:00
if err = gulu . JSON . UnmarshalJSON ( data , & langMap ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "unmarshal json [%s] failed: %s" , filePath , err )
2022-06-30 10:16:33 +08:00
c . JSON ( 200 , enUSMap )
return
}
for enUSDataKey , enUSDataValue := range enUSMap {
if _ , ok := langMap [ enUSDataKey ] ; ! ok {
langMap [ enUSDataKey ] = enUSDataValue
}
}
c . JSON ( 200 , langMap )
return
}
}
2022-05-26 15:18:53 +08:00
}
2022-06-30 10:16:33 +08:00
2022-05-26 15:18:53 +08:00
c . File ( filePath )
} )
siyuan . Static ( "/stage/" , filepath . Join ( util . WorkingDir , "stage" ) )
2024-06-12 21:03:51 +08:00
}
2022-05-26 15:18:53 +08:00
2024-06-12 21:03:51 +08:00
func serveCheckAuth ( ginServer * gin . Engine ) {
ginServer . GET ( "/check-auth" , serveAuthPage )
2022-05-26 15:18:53 +08:00
}
2024-06-12 21:03:51 +08:00
func serveAuthPage ( c * gin . Context ) {
2022-05-26 15:18:53 +08:00
data , err := os . ReadFile ( filepath . Join ( util . WorkingDir , "stage/auth.html" ) )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "load auth page failed: %s" , err )
2022-05-26 15:18:53 +08:00
c . Status ( 500 )
return
}
2022-12-08 23:19:03 +08:00
tpl , err := template . New ( "auth" ) . Parse ( string ( data ) )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-12-08 23:19:03 +08:00
logging . LogErrorf ( "parse auth page failed: %s" , err )
c . Status ( 500 )
return
}
2023-11-09 09:55:11 +08:00
keymapHideWindow := "⌥M"
if nil != ( * model . Conf . Keymap ) [ "general" ] {
switch ( * model . Conf . Keymap ) [ "general" ] . ( type ) {
case map [ string ] interface { } :
keymapGeneral := ( * model . Conf . Keymap ) [ "general" ] . ( map [ string ] interface { } )
if nil != keymapGeneral [ "toggleWin" ] {
switch keymapGeneral [ "toggleWin" ] . ( type ) {
case map [ string ] interface { } :
toggleWin := keymapGeneral [ "toggleWin" ] . ( map [ string ] interface { } )
if nil != toggleWin [ "custom" ] {
keymapHideWindow = toggleWin [ "custom" ] . ( string )
}
}
}
}
if "" == keymapHideWindow {
keymapHideWindow = "⌥M"
}
}
2022-12-08 23:19:03 +08:00
model := map [ string ] interface { } {
2023-11-09 09:55:53 +08:00
"l0" : model . Conf . Language ( 173 ) ,
"l1" : model . Conf . Language ( 174 ) ,
"l2" : template . HTML ( model . Conf . Language ( 172 ) ) ,
"l3" : model . Conf . Language ( 175 ) ,
"l4" : model . Conf . Language ( 176 ) ,
"l5" : model . Conf . Language ( 177 ) ,
"l6" : model . Conf . Language ( 178 ) ,
"l7" : template . HTML ( model . Conf . Language ( 184 ) ) ,
2023-11-18 11:14:36 +08:00
"l8" : model . Conf . Language ( 95 ) ,
2025-01-09 11:46:03 +08:00
"l9" : model . Conf . Language ( 83 ) ,
2025-06-04 03:35:31 -04:00
"l10" : model . Conf . Language ( 257 ) ,
2023-11-09 09:55:53 +08:00
"appearanceMode" : model . Conf . Appearance . Mode ,
"appearanceModeOS" : model . Conf . Appearance . ModeOS ,
2024-09-08 10:00:09 +08:00
"workspace" : util . WorkspaceName ,
2023-11-09 09:55:53 +08:00
"workspacePath" : util . WorkspaceDir ,
"keymapGeneralToggleWin" : keymapHideWindow ,
2023-11-09 10:52:46 +08:00
"trayMenuLangs" : util . TrayMenuLangs [ util . Lang ] ,
2023-11-09 11:16:44 +08:00
"workspaceDir" : util . WorkspaceDir ,
2022-12-08 23:19:03 +08:00
}
buf := & bytes . Buffer { }
2024-09-04 04:40:50 +03:00
if err = tpl . Execute ( buf , model ) ; err != nil {
2022-12-08 23:19:03 +08:00
logging . LogErrorf ( "execute auth page failed: %s" , err )
c . Status ( 500 )
return
}
data = buf . Bytes ( )
2022-05-26 15:18:53 +08:00
c . Data ( http . StatusOK , "text/html; charset=utf-8" , data )
}
func serveAssets ( ginServer * gin . Engine ) {
2024-06-12 21:03:51 +08:00
ginServer . POST ( "/upload" , model . CheckAuth , model . CheckAdminRole , model . CheckReadonly , model . Upload )
2022-05-26 15:18:53 +08:00
ginServer . GET ( "/assets/*path" , model . CheckAuth , func ( context * gin . Context ) {
requestPath := context . Param ( "path" )
2025-07-09 20:12:54 +08:00
if "/" == requestPath || "" == requestPath {
// 禁止访问根目录 Disable HTTP access to the /assets/ path https://github.com/siyuan-note/siyuan/issues/15257
context . Status ( http . StatusForbidden )
return
}
2022-05-26 15:18:53 +08:00
relativePath := path . Join ( "assets" , requestPath )
p , err := model . GetAssetAbsPath ( relativePath )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-07-01 21:59:57 +08:00
if strings . Contains ( strings . TrimPrefix ( requestPath , "/" ) , "/" ) {
// 再使用编码过的路径解析一次 https://github.com/siyuan-note/siyuan/issues/11823
dest := url . PathEscape ( strings . TrimPrefix ( requestPath , "/" ) )
dest = strings . ReplaceAll ( dest , ":" , "%3A" )
relativePath = path . Join ( "assets" , dest )
p , err = model . GetAssetAbsPath ( relativePath )
}
2024-09-04 04:40:50 +03:00
if err != nil {
2024-07-01 21:59:57 +08:00
context . Status ( http . StatusNotFound )
return
}
2022-05-26 15:18:53 +08:00
}
2025-07-09 16:18:53 +08:00
if serveThumbnail ( context , p , requestPath ) {
// 如果请求缩略图服务成功则返回
return
}
// 返回原始文件
2022-05-26 15:18:53 +08:00
http . ServeFile ( context . Writer , context . Request , p )
return
} )
2025-07-09 16:18:53 +08:00
2024-06-12 21:03:51 +08:00
ginServer . GET ( "/history/*path" , model . CheckAuth , model . CheckAdminRole , func ( context * gin . Context ) {
2022-08-30 20:12:26 +08:00
p := filepath . Join ( util . HistoryDir , context . Param ( "path" ) )
2022-05-26 15:18:53 +08:00
http . ServeFile ( context . Writer , context . Request , p )
return
} )
}
2025-07-09 16:18:53 +08:00
func serveThumbnail ( context * gin . Context , assetAbsPath , requestPath string ) bool {
if style := context . Query ( "style" ) ; style == "thumb" && model . NeedGenerateAssetsThumbnail ( assetAbsPath ) { // 请求缩略图
thumbnailPath := filepath . Join ( util . TempDir , "thumbnails" , "assets" , requestPath )
if ! gulu . File . IsExist ( thumbnailPath ) {
// 如果缩略图不存在,则生成缩略图
err := model . GenerateAssetsThumbnail ( assetAbsPath , thumbnailPath )
if err != nil {
logging . LogErrorf ( "generate thumbnail failed: %s" , err )
return false
}
}
http . ServeFile ( context . Writer , context . Request , thumbnailPath )
return true
}
return false
}
2023-06-03 17:42:01 +08:00
func serveRepoDiff ( ginServer * gin . Engine ) {
2024-06-12 21:03:51 +08:00
ginServer . GET ( "/repo/diff/*path" , model . CheckAuth , model . CheckAdminRole , func ( context * gin . Context ) {
2023-06-03 17:42:01 +08:00
requestPath := context . Param ( "path" )
p := filepath . Join ( util . TempDir , "repo" , "diff" , requestPath )
http . ServeFile ( context . Writer , context . Request , p )
return
} )
}
2022-05-26 15:18:53 +08:00
func serveDebug ( ginServer * gin . Engine ) {
2024-01-11 20:04:31 +08:00
if "prod" == util . Mode {
// The production environment will no longer register `/debug/pprof/` https://github.com/siyuan-note/siyuan/issues/10152
return
}
2022-05-26 15:18:53 +08:00
ginServer . GET ( "/debug/pprof/" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/allocs" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/block" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/goroutine" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/heap" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/mutex" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/threadcreate" , gin . WrapF ( pprof . Index ) )
ginServer . GET ( "/debug/pprof/cmdline" , gin . WrapF ( pprof . Cmdline ) )
ginServer . GET ( "/debug/pprof/profile" , gin . WrapF ( pprof . Profile ) )
ginServer . GET ( "/debug/pprof/symbol" , gin . WrapF ( pprof . Symbol ) )
ginServer . GET ( "/debug/pprof/trace" , gin . WrapF ( pprof . Trace ) )
}
func serveWebSocket ( ginServer * gin . Engine ) {
util . WebSocketServer . Config . MaxMessageSize = 1024 * 1024 * 8
ginServer . GET ( "/ws" , func ( c * gin . Context ) {
2024-09-04 04:40:50 +03:00
if err := util . WebSocketServer . HandleRequest ( c . Writer , c . Request ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "handle command failed: %s" , err )
2022-05-26 15:18:53 +08:00
}
} )
util . WebSocketServer . HandlePong ( func ( session * melody . Session ) {
2023-06-03 12:21:59 +08:00
//logging.LogInfof("pong")
2022-05-26 15:18:53 +08:00
} )
util . WebSocketServer . HandleConnect ( func ( s * melody . Session ) {
2023-06-03 12:21:59 +08:00
//logging.LogInfof("ws check auth for [%s]", s.Request.RequestURI)
2022-05-26 15:18:53 +08:00
authOk := true
if "" != model . Conf . AccessAuthCode {
2024-11-15 20:32:54 +08:00
session , err := sessionStore . Get ( s . Request , "siyuan" )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
authOk = false
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "get cookie failed: %s" , err )
2022-05-26 15:18:53 +08:00
} else {
val := session . Values [ "data" ]
if nil == val {
authOk = false
} else {
2023-01-10 22:25:02 +08:00
sess := & util . SessionData { }
err = gulu . JSON . UnmarshalJSON ( [ ] byte ( val . ( string ) ) , sess )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
authOk = false
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "unmarshal cookie failed: %s" , err )
2022-05-26 15:18:53 +08:00
} else {
2023-01-10 22:25:02 +08:00
workspaceSess := util . GetWorkspaceSession ( sess )
authOk = workspaceSess . AccessAuthCode == model . Conf . AccessAuthCode
2022-05-26 15:18:53 +08:00
}
}
}
}
2024-06-12 21:03:51 +08:00
// REF: https://github.com/siyuan-note/siyuan/issues/11364
if ! authOk {
if token := model . ParseXAuthToken ( s . Request ) ; token != nil {
authOk = token . Valid && model . IsValidRole ( model . GetClaimRole ( model . GetTokenClaims ( token ) ) , [ ] model . Role {
model . RoleAdministrator ,
model . RoleEditor ,
model . RoleReader ,
} )
}
}
2022-10-26 11:08:32 +08:00
if ! authOk {
// 用于授权页保持连接,避免非常驻内存内核自动退出 https://github.com/siyuan-note/insider/issues/1099
authOk = strings . Contains ( s . Request . RequestURI , "/ws?app=siyuan&id=auth" )
}
2022-05-26 15:18:53 +08:00
if ! authOk {
s . CloseWithMsg ( [ ] byte ( " unauthenticated" ) )
2024-03-25 10:23:29 +08:00
logging . LogWarnf ( "closed an unauthenticated session [%s]" , util . GetRemoteAddr ( s . Request ) )
2022-05-26 15:18:53 +08:00
return
}
util . AddPushChan ( s )
2023-06-03 12:21:59 +08:00
//sessionId, _ := s.Get("id")
//logging.LogInfof("ws [%s] connected", sessionId)
2022-05-26 15:18:53 +08:00
} )
util . WebSocketServer . HandleDisconnect ( func ( s * melody . Session ) {
util . RemovePushChan ( s )
2023-06-03 12:21:59 +08:00
//sessionId, _ := s.Get("id")
//logging.LogInfof("ws [%s] disconnected", sessionId)
2022-05-26 15:18:53 +08:00
} )
util . WebSocketServer . HandleError ( func ( s * melody . Session , err error ) {
2023-06-03 12:21:59 +08:00
//sessionId, _ := s.Get("id")
//logging.LogWarnf("ws [%s] failed: %s", sessionId, err)
2022-05-26 15:18:53 +08:00
} )
util . WebSocketServer . HandleClose ( func ( s * melody . Session , i int , str string ) error {
2023-06-03 12:21:59 +08:00
//sessionId, _ := s.Get("id")
//logging.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str)
2022-05-26 15:18:53 +08:00
return nil
} )
util . WebSocketServer . HandleMessage ( func ( s * melody . Session , msg [ ] byte ) {
start := time . Now ( )
2022-07-17 12:22:32 +08:00
logging . LogTracef ( "request [%s]" , shortReqMsg ( msg ) )
2022-05-26 15:18:53 +08:00
request := map [ string ] interface { } { }
2024-09-04 04:40:50 +03:00
if err := gulu . JSON . UnmarshalJSON ( msg , & request ) ; err != nil {
2022-05-26 15:18:53 +08:00
result := util . NewResult ( )
result . Code = - 1
result . Msg = "Bad Request"
responseData , _ := gulu . JSON . MarshalJSON ( result )
s . Write ( responseData )
return
}
if _ , ok := s . Get ( "app" ) ; ! ok {
result := util . NewResult ( )
result . Code = - 1
result . Msg = "Bad Request"
s . Write ( result . Bytes ( ) )
return
}
cmdStr := request [ "cmd" ] . ( string )
cmdId := request [ "reqId" ] . ( float64 )
param := request [ "param" ] . ( map [ string ] interface { } )
command := cmd . NewCommand ( cmdStr , cmdId , param , s )
if nil == command {
result := util . NewResult ( )
result . Code = - 1
result . Msg = "can not find command [" + cmdStr + "]"
s . Write ( result . Bytes ( ) )
return
}
2024-06-12 21:03:51 +08:00
if ! command . IsRead ( ) {
readonly := util . ReadOnly
if ! readonly {
if token := model . ParseXAuthToken ( s . Request ) ; token != nil {
readonly = token . Valid && model . IsValidRole ( model . GetClaimRole ( model . GetTokenClaims ( token ) ) , [ ] model . Role {
model . RoleReader ,
model . RoleVisitor ,
} )
}
}
if readonly {
result := util . NewResult ( )
result . Code = - 1
result . Msg = model . Conf . Language ( 34 )
s . Write ( result . Bytes ( ) )
return
}
2022-05-26 15:18:53 +08:00
}
end := time . Now ( )
2022-07-17 12:22:32 +08:00
logging . LogTracef ( "parse cmd [%s] consumed [%d]ms" , command . Name ( ) , end . Sub ( start ) . Milliseconds ( ) )
2022-05-26 15:18:53 +08:00
cmd . Exec ( command )
} )
}
2024-09-08 10:00:09 +08:00
func serveWebDAV ( ginServer * gin . Engine ) {
// REF: https://github.com/fungaren/gin-webdav
handler := webdav . Handler {
Prefix : "/webdav/" ,
FileSystem : webdav . Dir ( util . WorkspaceDir ) ,
LockSystem : webdav . NewMemLS ( ) ,
Logger : func ( r * http . Request , err error ) {
if nil != err {
logging . LogErrorf ( "WebDAV [%s %s]: %s" , r . Method , r . URL . String ( ) , err . Error ( ) )
}
// logging.LogDebugf("WebDAV [%s %s]", r.Method, r.URL.String())
} ,
}
ginGroup := ginServer . Group ( "/webdav" , model . CheckAuth , model . CheckAdminRole )
2024-11-15 11:19:52 +08:00
// ginGroup.Any NOT support extension methods (PROPFIND etc.)
ginGroup . Match ( WebDavMethods , "/*path" , func ( c * gin . Context ) {
if util . ReadOnly {
switch c . Request . Method {
case http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
2024-12-01 23:20:47 +08:00
MethodMkCol ,
MethodCopy ,
MethodMove ,
MethodLock ,
MethodUnlock ,
MethodPropPatch :
c . AbortWithError ( http . StatusForbidden , fmt . Errorf ( model . Conf . Language ( 34 ) ) )
return
}
}
handler . ServeHTTP ( c . Writer , c . Request )
} )
}
func serveCalDAV ( ginServer * gin . Engine ) {
// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
handler := caldav . Handler {
Backend : & model . CalDavBackend { } ,
Prefix : model . CalDavPrincipalsPath ,
}
ginServer . Match ( CalDavMethods , "/.well-known/caldav" , func ( c * gin . Context ) {
// logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
handler . ServeHTTP ( c . Writer , c . Request )
} )
ginGroup := ginServer . Group ( model . CalDavPrefixPath , model . CheckAuth , model . CheckAdminRole )
ginGroup . Match ( CalDavMethods , "/*path" , func ( c * gin . Context ) {
// logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
if util . ReadOnly {
switch c . Request . Method {
case http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
MethodMkCol ,
2024-11-15 11:19:52 +08:00
MethodCopy ,
MethodMove ,
MethodLock ,
MethodUnlock ,
MethodPropPatch :
c . AbortWithError ( http . StatusForbidden , fmt . Errorf ( model . Conf . Language ( 34 ) ) )
return
}
}
handler . ServeHTTP ( c . Writer , c . Request )
2024-12-01 23:20:47 +08:00
// logging.LogDebugf("CalDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
2024-11-15 11:19:52 +08:00
} )
}
func serveCardDAV ( ginServer * gin . Engine ) {
// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
handler := carddav . Handler {
Backend : & model . CardDavBackend { } ,
Prefix : model . CardDavPrincipalsPath ,
}
ginServer . Match ( CardDavMethods , "/.well-known/carddav" , func ( c * gin . Context ) {
2024-12-01 23:20:47 +08:00
// logging.LogDebugf("CardDAV [/.well-known/carddav]")
2024-11-15 11:19:52 +08:00
handler . ServeHTTP ( c . Writer , c . Request )
} )
ginGroup := ginServer . Group ( model . CardDavPrefixPath , model . CheckAuth , model . CheckAdminRole )
ginGroup . Match ( CardDavMethods , "/*path" , func ( c * gin . Context ) {
2024-09-08 10:00:09 +08:00
if util . ReadOnly {
switch c . Request . Method {
2024-11-15 11:19:52 +08:00
case http . MethodPost ,
http . MethodPut ,
http . MethodDelete ,
2024-12-01 23:20:47 +08:00
MethodMkCol ,
2024-11-15 11:19:52 +08:00
MethodCopy ,
MethodMove ,
MethodLock ,
MethodUnlock ,
MethodPropPatch :
2024-09-08 10:00:09 +08:00
c . AbortWithError ( http . StatusForbidden , fmt . Errorf ( model . Conf . Language ( 34 ) ) )
return
}
}
2024-12-01 23:20:47 +08:00
// TODO: Can't handle Thunderbird's PROPFIND request with prop <current-user-privilege-set/>
2024-09-08 10:00:09 +08:00
handler . ServeHTTP ( c . Writer , c . Request )
2024-11-15 11:19:52 +08:00
// logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
2024-09-08 10:00:09 +08:00
} )
}
2022-05-26 15:18:53 +08:00
func shortReqMsg ( msg [ ] byte ) [ ] byte {
s := gulu . Str . FromBytes ( msg )
max := 128
if len ( s ) > max {
count := 0
for i := range s {
count ++
if count > max {
return gulu . Str . ToBytes ( s [ : i ] + "..." )
}
}
}
return msg
}
2022-08-06 22:56:49 +08:00
func corsMiddleware ( ) gin . HandlerFunc {
2024-11-15 11:19:52 +08:00
allowMethods := strings . Join ( HttpMethods , ", " )
allowWebDavMethods := strings . Join ( WebDavMethods , ", " )
2024-12-01 23:20:47 +08:00
allowCalDavMethods := strings . Join ( CalDavMethods , ", " )
2024-11-15 11:19:52 +08:00
allowCardDavMethods := strings . Join ( CardDavMethods , ", " )
2022-08-06 22:56:49 +08:00
2024-11-15 11:19:52 +08:00
return func ( c * gin . Context ) {
2022-08-06 22:56:49 +08:00
c . Header ( "Access-Control-Allow-Origin" , "*" )
c . Header ( "Access-Control-Allow-Credentials" , "true" )
c . Header ( "Access-Control-Allow-Headers" , "origin, Content-Length, Content-Type, Authorization" )
2022-12-29 22:48:06 +08:00
c . Header ( "Access-Control-Allow-Private-Network" , "true" )
2022-08-06 22:56:49 +08:00
2024-12-01 23:20:47 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/webdav" ) {
2024-11-15 11:19:52 +08:00
c . Header ( "Access-Control-Allow-Methods" , allowWebDavMethods )
c . Next ( )
return
}
2024-12-01 23:20:47 +08:00
if strings . HasPrefix ( c . Request . RequestURI , "/caldav" ) {
c . Header ( "Access-Control-Allow-Methods" , allowCalDavMethods )
c . Next ( )
return
}
if strings . HasPrefix ( c . Request . RequestURI , "/carddav" ) {
2024-11-15 11:19:52 +08:00
c . Header ( "Access-Control-Allow-Methods" , allowCardDavMethods )
c . Next ( )
return
}
c . Header ( "Access-Control-Allow-Methods" , allowMethods )
switch c . Request . Method {
case http . MethodOptions :
2023-09-25 09:28:58 +08:00
c . Header ( "Access-Control-Max-Age" , "600" )
2022-08-06 22:56:49 +08:00
c . AbortWithStatus ( 204 )
return
}
c . Next ( )
}
}
2024-06-12 21:03:51 +08:00
// jwtMiddleware is a middleware to check jwt token
// REF: https://github.com/siyuan-note/siyuan/issues/11364
func jwtMiddleware ( c * gin . Context ) {
if token := model . ParseXAuthToken ( c . Request ) ; token != nil {
// c.Request.Header.Del(model.XAuthTokenKey)
if token . Valid {
claims := model . GetTokenClaims ( token )
c . Set ( model . ClaimsContextKey , claims )
c . Set ( model . RoleContextKey , model . GetClaimRole ( claims ) )
c . Next ( )
return
}
}
c . Set ( model . RoleContextKey , model . RoleVisitor )
c . Next ( )
return
}
func serveFixedStaticFiles ( ginServer * gin . Engine ) {
ginServer . StaticFile ( "favicon.ico" , filepath . Join ( util . WorkingDir , "stage" , "icon.png" ) )
ginServer . StaticFile ( "manifest.json" , filepath . Join ( util . WorkingDir , "stage" , "manifest.webmanifest" ) )
ginServer . StaticFile ( "manifest.webmanifest" , filepath . Join ( util . WorkingDir , "stage" , "manifest.webmanifest" ) )
ginServer . StaticFile ( "service-worker.js" , filepath . Join ( util . WorkingDir , "stage" , "service-worker.js" ) )
}