Something went wrong. Try again.
A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017// SiYuan - Refactor your thinking// 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 ( "bytes" "errors" "fmt" "html/template" "mime" "net" "net/http" "net/http/pprof" "net/url" "os" "path" "path/filepath" "strings" "time"
"github.com/88250/gulu" "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/mssola/useragent" "github.com/olahol/melody" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/api" "github.com/siyuan-note/siyuan/kernel/cmd" "github.com/siyuan-note/siyuan/kernel/model" "github.com/siyuan-note/siyuan/kernel/server/proxy" "github.com/siyuan-note/siyuan/kernel/util" "github.com/soheilhy/cmux" "golang.org/x/net/webdav")
const ( MethodMkCol = "MKCOL" MethodCopy = "COPY" MethodMove = "MOVE" MethodLock = "LOCK" MethodUnlock = "UNLOCK" MethodPropFind = "PROPFIND" MethodPropPatch = "PROPPATCH" MethodReport = "REPORT")
var ( sessionStore cookie.Store
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,
MethodMkCol, MethodCopy, MethodMove, MethodLock, MethodUnlock, MethodPropFind, MethodPropPatch, } CalDavMethods = []string{ http.MethodOptions, http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete,
MethodMkCol, MethodCopy, MethodMove, // MethodLock, // MethodUnlock, MethodPropFind, MethodPropPatch,
MethodReport, } CardDavMethods = []string{ http.MethodOptions, http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete,
MethodMkCol, MethodCopy, MethodMove, // MethodLock, // MethodUnlock, MethodPropFind, MethodPropPatch,
MethodReport, })
func Serve(fastMode bool, cookieKey string) { gin.SetMode(gin.ReleaseMode) ginServer := gin.New() ginServer.UseH2C = true ginServer.MaxMultipartMemory = 1024 * 1024 * 32 // 插入较大的资源文件时内存占用较大 https://github.com/siyuan-note/siyuan/issues/5023 ginServer.Use( model.ControlConcurrency, // 请求串行化 Concurrency control when requesting the kernel API https://github.com/siyuan-note/siyuan/issues/9939 model.Timing, model.Recover, corsMiddleware(), // 后端服务支持 CORS 预检请求验证 https://github.com/siyuan-note/siyuan/pull/5593 jwtMiddleware, // 解析 JWT https://github.com/siyuan-note/siyuan/issues/11364 gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm", ".flac"})), )
sessionStore = cookie.NewStore([]byte(cookieKey)) sessionStore.Options(sessions.Options{ Path: "/", Secure: util.SSL, //MaxAge: 60 * 60 * 24 * 7, // 默认是 Session HttpOnly: true, }) ginServer.Use(sessions.Sessions("siyuan", sessionStore))
serveDebug(ginServer) serveAssets(ginServer) serveAppearance(ginServer) serveWebSocket(ginServer) serveWebDAV(ginServer) serveCalDAV(ginServer) serveCardDAV(ginServer) serveExport(ginServer) serveWidgets(ginServer) servePlugins(ginServer) serveEmojis(ginServer) serveTemplates(ginServer) servePublic(ginServer) serveSnippets(ginServer) serveRepoDiff(ginServer) serveCheckAuth(ginServer) serveFixedStaticFiles(ginServer) api.ServeAPI(ginServer)
var host string if model.Conf.System.NetworkServe || util.ContainerDocker == util.Container { host = "0.0.0.0" } else { host = "127.0.0.1" }
ln, err := net.Listen("tcp", host+":"+util.ServerPort) if err != nil { if !fastMode { logging.LogErrorf("boot kernel failed: %s", err) os.Exit(logging.ExitCodeUnavailablePort) }
// fast 模式下启动失败则直接返回 return }
_, port, err := net.SplitHostPort(ln.Addr().String()) if err != nil { if !fastMode { logging.LogErrorf("boot kernel failed: %s", err) os.Exit(logging.ExitCodeUnavailablePort) } } util.ServerPort = port
model.Conf.ServerAddrs = util.GetServerAddrs() model.Conf.Save()
util.ServerURL, err = url.Parse("http://127.0.0.1:" + port) if err != nil { logging.LogErrorf("parse server url failed: %s", err) }
pid := fmt.Sprintf("%d", os.Getpid()) if !fastMode { rewritePortJSON(pid, port) }
// Prepare TLS if enabled var certPath, keyPath string useTLS := model.Conf.System.NetworkServeTLS && model.Conf.System.NetworkServe if useTLS { // Ensure TLS certificates exist (proxy will use them directly) var tlsErr error certPath, keyPath, tlsErr = util.GetOrCreateTLSCert() if tlsErr != nil { logging.LogErrorf("failed to get TLS certificates: %s", tlsErr) if !fastMode { os.Exit(logging.ExitCodeUnavailablePort) } return } logging.LogInfof("kernel [pid=%s] http server [%s] is booting (TLS will be enabled on fixed port proxy)", pid, host+":"+port) } else { logging.LogInfof("kernel [pid=%s] http server [%s] is booting", pid, host+":"+port) } util.HttpServing = true
go util.HookUILoaded()
go func() { time.Sleep(1 * time.Second) go proxy.InitFixedPortService(host, useTLS, certPath, keyPath) go proxy.InitPublishService() // 反代服务器启动失败不影响核心服务器启动 }()
util.HttpServer = &http.Server{ Handler: ginServer, }
if useTLS && (util.FixedPort == util.ServerPort || util.IsPortOpen(util.FixedPort)) { if err = util.ServeMultiplexed(ln, ginServer, certPath, keyPath, util.HttpServer); err != nil { if errors.Is(err, http.ErrServerClosed) || err == cmux.ErrListenerClosed { return }
if !fastMode { logging.LogErrorf("boot kernel failed: %s", err) os.Exit(logging.ExitCodeUnavailablePort) } } return }
if err = util.HttpServer.Serve(ln); err != nil { if errors.Is(err, http.ErrServerClosed) { return }
if !fastMode { logging.LogErrorf("boot kernel failed: %s", err) os.Exit(logging.ExitCodeUnavailablePort) } }}
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) if err != nil { logging.LogWarnf("read port.json failed: %s", err) } else { if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); err != nil { logging.LogWarnf("unmarshal port.json failed: %s", err) } } }
pidPorts[pid] = port if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); err != nil { logging.LogWarnf("marshal port.json failed: %s", err) } else { if err = os.WriteFile(portJSON, data, 0644); err != nil { logging.LogWarnf("write port.json failed: %s", err) } }}
func serveExport(ginServer *gin.Engine) { // Potential data export disclosure security vulnerability https://github.com/siyuan-note/siyuan/issues/12213 exportGroup := ginServer.Group("/export/", model.CheckAuth) exportBaseDir := filepath.Join(util.TempDir, "export")
// 应下载而不是查看导出的文件 exportGroup.GET("/*filepath", func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/export/temp/") { c.File(filepath.Join(util.TempDir, c.Request.URL.Path)) return }
filePath := strings.TrimPrefix(c.Request.URL.Path, "/export/")
decodedPath, err := url.PathUnescape(filePath) if err != nil { decodedPath = filePath }
fullPath := filepath.Join(exportBaseDir, decodedPath)
fileInfo, err := os.Stat(fullPath) if os.IsNotExist(err) { c.Status(http.StatusNotFound) return } if err != nil { c.Status(http.StatusInternalServerError) return }
if fileInfo.IsDir() { c.Status(http.StatusNotFound) return }
fileName := filepath.Base(decodedPath) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
c.File(fullPath) })}
func serveWidgets(ginServer *gin.Engine) { widgets := ginServer.Group("/widgets/", model.CheckAuth) widgets.Static("", filepath.Join(util.DataDir, "widgets"))}
func servePlugins(ginServer *gin.Engine) { plugins := ginServer.Group("/plugins/", model.CheckAuth) plugins.Static("", filepath.Join(util.DataDir, "plugins"))}
func serveEmojis(ginServer *gin.Engine) { emojis := ginServer.Group("/emojis/", model.CheckAuth) emojis.Static("", filepath.Join(util.DataDir, "emojis"))}
func serveTemplates(ginServer *gin.Engine) { templates := ginServer.Group("/templates/", model.CheckAuth) templates.Static("", filepath.Join(util.DataDir, "templates"))}
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"))}
func serveSnippets(ginServer *gin.Engine) { ginServer.Handle("GET", "/snippets/*filepath", model.CheckAuth, func(c *gin.Context) { filePath := strings.TrimPrefix(c.Request.URL.Path, "/snippets/") if !model.IsAdminRoleContext(c) { if "conf.json" == filePath { c.Status(http.StatusUnauthorized) return } }
ext := filepath.Ext(filePath) name := strings.TrimSuffix(filePath, ext) confSnippets, err := model.LoadSnippets() if err != nil { 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 } }
// 没有在配置文件中命中时在文件系统上查找 filePath = filepath.Join(util.SnippetsPath, filePath) c.File(filePath) })}
func serveAppearance(ginServer *gin.Engine) { siyuan := ginServer.Group("", model.CheckAuth)
siyuan.Handle("GET", "/", func(c *gin.Context) { userAgentHeader := c.GetHeader("User-Agent") logging.LogInfof("serving [/] for user-agent [%s]", userAgentHeader)
// Carry query parameters when redirecting location := url.URL{} queryParams := c.Request.URL.Query() queryParams.Set("r", gulu.Rand.String(7)) location.RawQuery = queryParams.Encode()
if strings.Contains(userAgentHeader, "Electron") { location.Path = "/stage/build/app/" } 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 location.Path = "/stage/build/desktop/" } else { if idx := strings.Index(userAgentHeader, "Mozilla/"); 0 < idx { userAgentHeader = userAgentHeader[idx:] } ua := useragent.New(userAgentHeader) if ua.Mobile() { location.Path = "/stage/build/mobile/" } else { location.Path = "/stage/build/desktop/" } }
c.Redirect(302, location.String()) })
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/")) 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) if err != nil { logging.LogErrorf("read en_US.json [%s] failed: %s", enUSFilePath, err) util.ReportFileSysFatalError(err) return } enUSMap := map[string]interface{}{} if err = gulu.JSON.UnmarshalJSON(enUSData, &enUSMap); err != nil { logging.LogErrorf("unmarshal en_US.json [%s] failed: %s", enUSFilePath, err) util.ReportFileSysFatalError(err) return }
for { data, err := os.ReadFile(filePath) if err != nil { c.JSON(200, enUSMap) return }
langMap := map[string]interface{}{} if err = gulu.JSON.UnmarshalJSON(data, &langMap); err != nil { logging.LogErrorf("unmarshal json [%s] failed: %s", filePath, err) c.JSON(200, enUSMap) return }
for enUSDataKey, enUSDataValue := range enUSMap { if _, ok := langMap[enUSDataKey]; !ok { langMap[enUSDataKey] = enUSDataValue } } c.JSON(200, langMap) return } } }
c.File(filePath) })
siyuan.Static("/stage", filepath.Join(util.WorkingDir, "stage"))}
func serveCheckAuth(ginServer *gin.Engine) { ginServer.GET("/check-auth", serveAuthPage)}
func serveAuthPage(c *gin.Context) { data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html")) if err != nil { logging.LogErrorf("load auth page failed: %s", err) c.Status(500) return }
tpl, err := template.New("auth").Parse(string(data)) if err != nil { logging.LogErrorf("parse auth page failed: %s", err) c.Status(500) return }
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" } } model := map[string]interface{}{ "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)), "l8": model.Conf.Language(95), "l9": model.Conf.Language(83), "l10": model.Conf.Language(257), "l11": model.Conf.Language(282), "appearanceMode": model.Conf.Appearance.Mode, "appearanceModeOS": model.Conf.Appearance.ModeOS, "workspace": util.WorkspaceName, "workspacePath": util.WorkspaceDir, "keymapGeneralToggleWin": keymapHideWindow, "trayMenuLangs": util.TrayMenuLangs[util.Lang], "workspaceDir": util.WorkspaceDir, } buf := &bytes.Buffer{} if err = tpl.Execute(buf, model); err != nil { logging.LogErrorf("execute auth page failed: %s", err) c.Status(500) return } data = buf.Bytes() c.Data(http.StatusOK, "text/html; charset=utf-8", data)}
func serveAssets(ginServer *gin.Engine) { ginServer.POST("/upload", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, model.Upload)
ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) { requestPath := context.Param("path") if "/" == requestPath || "" == requestPath { // 禁止访问根目录 Disable HTTP access to the /assets/ path https://github.com/siyuan-note/siyuan/issues/15257 context.Status(http.StatusForbidden) return }
relativePath := path.Join("assets", requestPath) p, err := model.GetAssetAbsPath(relativePath) if err != nil { 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) }
if err != nil { context.Status(http.StatusNotFound) return } }
if serveThumbnail(context, p, requestPath) || serveSVG(context, p) { return }
// 返回原始文件 http.ServeFile(context.Writer, context.Request, p) return })
ginServer.GET("/history/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) { p := filepath.Join(util.HistoryDir, context.Param("path")) http.ServeFile(context.Writer, context.Request, p) return })}
func serveSVG(context *gin.Context, assetAbsPath string) bool { if strings.HasSuffix(assetAbsPath, ".svg") { data, err := os.ReadFile(assetAbsPath) if err != nil { logging.LogErrorf("read svg file failed: %s", err) return false }
if !model.Conf.Editor.AllowSVGScript { data = []byte(util.SanitizeSVG(string(data))) }
context.Data(200, "image/svg+xml", data) return true } return false}
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}
func serveRepoDiff(ginServer *gin.Engine) { ginServer.GET("/repo/diff/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) { requestPath := context.Param("path") p := filepath.Join(util.TempDir, "repo", "diff", requestPath) http.ServeFile(context.Writer, context.Request, p) return })}
func serveDebug(ginServer *gin.Engine) { if "prod" == util.Mode { // The production environment will no longer register `/debug/pprof/` https://github.com/siyuan-note/siyuan/issues/10152 return }
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 = melody.New() util.WebSocketServer.Config.MaxMessageSize = 1024 * 1024 * 8
ginServer.GET("/ws", func(c *gin.Context) { if err := util.WebSocketServer.HandleRequest(c.Writer, c.Request); err != nil { logging.LogErrorf("handle command failed: %s", err) } })
util.WebSocketServer.HandlePong(func(session *melody.Session) { //logging.LogInfof("pong") })
util.WebSocketServer.HandleConnect(func(s *melody.Session) { //logging.LogInfof("ws check auth for [%s]", s.Request.RequestURI) authOk := true
if "" != model.Conf.AccessAuthCode { session, err := sessionStore.Get(s.Request, "siyuan") if err != nil { authOk = false logging.LogErrorf("get cookie failed: %s", err) } else { val := session.Values["data"] if nil == val { authOk = false } else { sess := &util.SessionData{} err = gulu.JSON.UnmarshalJSON([]byte(val.(string)), sess) if err != nil { authOk = false logging.LogErrorf("unmarshal cookie failed: %s", err) } else { workspaceSess := util.GetWorkspaceSession(sess) authOk = workspaceSess.AccessAuthCode == model.Conf.AccessAuthCode } } } }
// 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, }) } }
if !authOk { // 用于授权页保持连接,避免非常驻内存内核自动退出 https://github.com/siyuan-note/insider/issues/1099 authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan") && strings.Contains(s.Request.RequestURI, "&id=auth&type=auth") }
if !authOk { s.CloseWithMsg([]byte(" unauthenticated")) logging.LogWarnf("closed an unauthenticated session [%s]", util.GetRemoteAddr(s.Request)) return }
// 标记发布服务的连接 if token := model.ParseXAuthToken(s.Request); token != nil { if model.IsPublishServiceToken(token) { s.Set("isPublish", true) } }
util.AddPushChan(s) //sessionId, _ := s.Get("id") //logging.LogInfof("ws [%s] connected", sessionId) })
util.WebSocketServer.HandleDisconnect(func(s *melody.Session) { util.RemovePushChan(s) //sessionId, _ := s.Get("id") //logging.LogInfof("ws [%s] disconnected", sessionId) })
util.WebSocketServer.HandleError(func(s *melody.Session, err error) { //sessionId, _ := s.Get("id") //logging.LogWarnf("ws [%s] failed: %s", sessionId, err) })
util.WebSocketServer.HandleClose(func(s *melody.Session, i int, str string) error { //sessionId, _ := s.Get("id") //logging.LogDebugf("ws [%s] closed: %v, %v", sessionId, i, str) return nil })
util.WebSocketServer.HandleMessage(func(s *melody.Session, msg []byte) { start := time.Now() logging.LogTracef("request [%s]", shortReqMsg(msg)) request := map[string]interface{}{} if err := gulu.JSON.UnmarshalJSON(msg, &request); err != nil { 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 } 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 } }
end := time.Now() logging.LogTracef("parse cmd [%s] consumed [%d]ms", command.Name(), end.Sub(start).Milliseconds())
cmd.Exec(command) })}
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) // 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, 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, MethodCopy, MethodMove, MethodLock, MethodUnlock, MethodPropPatch: c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) return } } handler.ServeHTTP(c.Writer, c.Request) // logging.LogDebugf("CalDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) })}
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) { // logging.LogDebugf("CardDAV [/.well-known/carddav]") handler.ServeHTTP(c.Writer, c.Request) })
ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole) ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { case http.MethodPost, http.MethodPut, http.MethodDelete, MethodMkCol, MethodCopy, MethodMove, MethodLock, MethodUnlock, MethodPropPatch: c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) return } } // TODO: Can't handle Thunderbird's PROPFIND request with prop <current-user-privilege-set/> handler.ServeHTTP(c.Writer, c.Request) // logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) })}
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}
func corsMiddleware() gin.HandlerFunc { allowMethods := strings.Join(HttpMethods, ", ") allowWebDavMethods := strings.Join(WebDavMethods, ", ") allowCalDavMethods := strings.Join(CalDavMethods, ", ") allowCardDavMethods := strings.Join(CardDavMethods, ", ")
return func(c *gin.Context) { 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") c.Header("Access-Control-Allow-Private-Network", "true")
if strings.HasPrefix(c.Request.RequestURI, "/webdav") { c.Header("Access-Control-Allow-Methods", allowWebDavMethods) c.Next() return }
if strings.HasPrefix(c.Request.RequestURI, "/caldav") { c.Header("Access-Control-Allow-Methods", allowCalDavMethods) c.Next() return }
if strings.HasPrefix(c.Request.RequestURI, "/carddav") { c.Header("Access-Control-Allow-Methods", allowCardDavMethods) c.Next() return }
c.Header("Access-Control-Allow-Methods", allowMethods)
switch c.Request.Method { case http.MethodOptions: c.Header("Access-Control-Max-Age", "600") c.AbortWithStatus(204) return }
c.Next() }}
// jwtMiddleware is a middleware to check jwt token// REF: https://github.com/siyuan-note/siyuan/issues/11364func 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"))}