mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-16 14:40:12 +01:00
✨ Support read-only publish service
* 🎨 kernel supports read-only publishing services * 🐛 Fix authentication vulnerabilities * 🎨 Protect secret information * 🎨 Adjust the permission control * 🎨 Adjust the permission control * 🎨 Fixed the vulnerability that `getFile` gets file `conf.json` * 🎨 Add API `/api/setting/setPublish` * 🎨 Add API `/api/setting/getPublish` * 🐛 Fixed the issue that PWA-related files could not pass BasicAuth * 🎨 Add a settings panel for publishing features * 📝 Add guide for `Publish Service` * 📝 Update Japanese user guide * 🎨 Merge fixed static file services
This commit is contained in:
parent
536879cb84
commit
ba2193403d
47 changed files with 3690 additions and 375 deletions
|
|
@ -18,7 +18,6 @@ package server
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -65,7 +64,7 @@ func killRunningKernel() {
|
|||
}
|
||||
|
||||
func killByPort(port string) {
|
||||
if !isPortOpen(port) {
|
||||
if !util.IsPortOpen(port) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -87,19 +86,6 @@ func killByPort(port string) {
|
|||
logging.LogInfof("killed process [name=%s, pid=%s]", name, pid)
|
||||
}
|
||||
|
||||
func isPortOpen(port string) bool {
|
||||
timeout := time.Second
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), timeout)
|
||||
if nil != err {
|
||||
return false
|
||||
}
|
||||
if nil != conn {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func kill(pid string) {
|
||||
var killCmd *exec.Cmd
|
||||
if gulu.OS.IsWindows() {
|
||||
|
|
|
|||
41
kernel/server/proxy/fixedport.go
Normal file
41
kernel/server/proxy/fixedport.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// 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 proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
func InitFixedPortService(host string) {
|
||||
if util.FixedPort != util.ServerPort {
|
||||
if util.IsPortOpen(util.FixedPort) {
|
||||
return
|
||||
}
|
||||
|
||||
// 启动一个固定 6806 端口的反向代理服务器,这样浏览器扩展才能直接使用 127.0.0.1:6806,不用配置端口
|
||||
proxy := httputil.NewSingleHostReverseProxy(util.ServerURL)
|
||||
logging.LogInfof("fixed port service [%s:%s] is running", host, util.FixedPort)
|
||||
if proxyErr := http.ListenAndServe(host+":"+util.FixedPort, proxy); nil != proxyErr {
|
||||
logging.LogWarnf("boot fixed port service [%s] failed: %s", util.ServerURL, proxyErr)
|
||||
}
|
||||
logging.LogInfof("fixed port service [%s:%s] is stopped", host, util.FixedPort)
|
||||
}
|
||||
}
|
||||
161
kernel/server/proxy/publish.go
Normal file
161
kernel/server/proxy/publish.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// 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 proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/model"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
type PublishServiceTransport struct{}
|
||||
|
||||
var (
|
||||
Host = "0.0.0.0"
|
||||
Port = "0"
|
||||
|
||||
listener net.Listener
|
||||
transport = PublishServiceTransport{}
|
||||
proxy = &httputil.ReverseProxy{
|
||||
Rewrite: rewrite,
|
||||
Transport: transport,
|
||||
}
|
||||
)
|
||||
|
||||
func InitPublishService() (uint16, error) {
|
||||
model.InitAccounts()
|
||||
|
||||
if listener != nil {
|
||||
if !model.Conf.Publish.Enable {
|
||||
// 关闭发布服务
|
||||
closePublishListener()
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if port, err := util.ParsePort(Port); err != nil {
|
||||
return 0, err
|
||||
} else if port != model.Conf.Publish.Port {
|
||||
// 关闭原端口的发布服务
|
||||
if err = closePublishListener(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 重新启动新端口的发布服务
|
||||
initPublishService()
|
||||
}
|
||||
} else {
|
||||
if !model.Conf.Publish.Enable {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 启动新端口的发布服务
|
||||
initPublishService()
|
||||
}
|
||||
return util.ParsePort(Port)
|
||||
}
|
||||
|
||||
func initPublishService() (err error) {
|
||||
if err = initPublishListener(); err == nil {
|
||||
go startPublishReverseProxyService()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func initPublishListener() (err error) {
|
||||
// Start new listener
|
||||
listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", Host, model.Conf.Publish.Port))
|
||||
if err != nil {
|
||||
logging.LogErrorf("start listener failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, Port, err = net.SplitHostPort(listener.Addr().String())
|
||||
if nil != err {
|
||||
logging.LogErrorf("split host and port failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func closePublishListener() (err error) {
|
||||
listener_ := listener
|
||||
listener = nil
|
||||
if err = listener_.Close(); err != nil {
|
||||
logging.LogErrorf("close listener %s failed: %s", listener_.Addr().String(), err)
|
||||
listener = listener_
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func startPublishReverseProxyService() {
|
||||
logging.LogInfof("publish service [%s:%s] is running", Host, Port)
|
||||
// 服务进行时一直阻塞
|
||||
if err := http.Serve(listener, proxy); nil != err {
|
||||
if listener != nil {
|
||||
logging.LogErrorf("boot publish service failed: %s", err)
|
||||
}
|
||||
}
|
||||
logging.LogInfof("publish service [%s:%s] is stopped", Host, Port)
|
||||
}
|
||||
|
||||
func rewrite(r *httputil.ProxyRequest) {
|
||||
r.SetURL(util.ServerURL)
|
||||
r.SetXForwarded()
|
||||
// r.Out.Host = r.In.Host // if desired
|
||||
}
|
||||
|
||||
func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http.Response, err error) {
|
||||
if model.Conf.Publish.Auth.Enable {
|
||||
// Basic Auth
|
||||
username, password, ok := request.BasicAuth()
|
||||
account := model.GetBasicAuthAccount(username)
|
||||
|
||||
if !ok ||
|
||||
account == nil ||
|
||||
account.Username == "" || // 匿名用户
|
||||
account.Password != password {
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Status: http.StatusText(http.StatusUnauthorized),
|
||||
Proto: request.Proto,
|
||||
ProtoMajor: request.ProtoMajor,
|
||||
ProtoMinor: request.ProtoMinor,
|
||||
Request: request,
|
||||
Header: http.Header{
|
||||
"WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")},
|
||||
},
|
||||
Close: false,
|
||||
ContentLength: -1,
|
||||
}, nil
|
||||
} else {
|
||||
// set JWT
|
||||
request.Header.Set(model.XAuthTokenKey, account.Token)
|
||||
}
|
||||
} else {
|
||||
request.Header.Set(model.XAuthTokenKey, model.GetBasicAuthAccount("").Token)
|
||||
}
|
||||
|
||||
response, err = http.DefaultTransport.RoundTrip(request)
|
||||
return
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -42,10 +42,13 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
var cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
|
||||
var (
|
||||
cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
|
||||
)
|
||||
|
||||
func Serve(fastMode bool) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
|
@ -57,6 +60,7 @@ func Serve(fastMode bool) {
|
|||
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"})),
|
||||
)
|
||||
|
||||
|
|
@ -78,7 +82,10 @@ func Serve(fastMode bool) {
|
|||
serveEmojis(ginServer)
|
||||
serveTemplates(ginServer)
|
||||
servePublic(ginServer)
|
||||
serveSnippets(ginServer)
|
||||
serveRepoDiff(ginServer)
|
||||
serveCheckAuth(ginServer)
|
||||
serveFixedStaticFiles(ginServer)
|
||||
api.ServeAPI(ginServer)
|
||||
|
||||
var host string
|
||||
|
|
@ -108,34 +115,27 @@ func Serve(fastMode bool) {
|
|||
}
|
||||
util.ServerPort = port
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if util.FixedPort != port {
|
||||
if isPortOpen(util.FixedPort) {
|
||||
return
|
||||
}
|
||||
|
||||
// 启动一个 6806 端口的反向代理服务器,这样浏览器扩展才能直接使用 127.0.0.1:6806,不用配置端口
|
||||
serverURL, _ := url.Parse("http://127.0.0.1:" + port)
|
||||
proxy := httputil.NewSingleHostReverseProxy(serverURL)
|
||||
logging.LogInfof("reverse proxy server [%s] is booting", host+":"+util.FixedPort)
|
||||
if proxyErr := http.ListenAndServe(host+":"+util.FixedPort, proxy); nil != proxyErr {
|
||||
logging.LogWarnf("boot reverse proxy server [%s] failed: %s", serverURL, proxyErr)
|
||||
}
|
||||
// 反代服务器启动失败不影响核心服务器启动
|
||||
}
|
||||
go proxy.InitFixedPortService(host)
|
||||
go proxy.InitPublishService()
|
||||
// 反代服务器启动失败不影响核心服务器启动
|
||||
}()
|
||||
|
||||
go util.HookUILoaded()
|
||||
|
||||
if err = http.Serve(ln, ginServer.Handler()); nil != err {
|
||||
if !fastMode {
|
||||
logging.LogErrorf("boot kernel failed: %s", err)
|
||||
|
|
@ -196,11 +196,33 @@ func servePublic(ginServer *gin.Engine) {
|
|||
ginServer.Static("/public/", filepath.Join(util.DataDir, "public"))
|
||||
}
|
||||
|
||||
func serveAppearance(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"))
|
||||
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()
|
||||
if nil != err {
|
||||
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) {
|
||||
|
|
@ -295,12 +317,13 @@ func serveAppearance(ginServer *gin.Engine) {
|
|||
})
|
||||
|
||||
siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
|
||||
ginServer.StaticFile("service-worker.js", filepath.Join(util.WorkingDir, "stage", "service-worker.js"))
|
||||
|
||||
siyuan.GET("/check-auth", serveCheckAuth)
|
||||
}
|
||||
|
||||
func serveCheckAuth(c *gin.Context) {
|
||||
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 nil != err {
|
||||
logging.LogErrorf("load auth page failed: %s", err)
|
||||
|
|
@ -363,20 +386,20 @@ func serveCheckAuth(c *gin.Context) {
|
|||
}
|
||||
|
||||
func serveAssets(ginServer *gin.Engine) {
|
||||
ginServer.POST("/upload", model.CheckAuth, model.Upload)
|
||||
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")
|
||||
relativePath := path.Join("assets", requestPath)
|
||||
p, err := model.GetAssetAbsPath(relativePath)
|
||||
if nil != err {
|
||||
context.Status(404)
|
||||
context.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.ServeFile(context.Writer, context.Request, p)
|
||||
return
|
||||
})
|
||||
ginServer.GET("/history/*path", model.CheckAuth, func(context *gin.Context) {
|
||||
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
|
||||
|
|
@ -384,7 +407,7 @@ func serveAssets(ginServer *gin.Engine) {
|
|||
}
|
||||
|
||||
func serveRepoDiff(ginServer *gin.Engine) {
|
||||
ginServer.GET("/repo/diff/*path", model.CheckAuth, func(context *gin.Context) {
|
||||
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)
|
||||
|
|
@ -451,6 +474,17 @@ func serveWebSocket(ginServer *gin.Engine) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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&id=auth")
|
||||
|
|
@ -516,12 +550,24 @@ func serveWebSocket(ginServer *gin.Engine) {
|
|||
s.Write(result.Bytes())
|
||||
return
|
||||
}
|
||||
if util.ReadOnly && !command.IsRead() {
|
||||
result := util.NewResult()
|
||||
result.Code = -1
|
||||
result.Msg = model.Conf.Language(34)
|
||||
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()
|
||||
|
|
@ -564,3 +610,30 @@ func corsMiddleware() gin.HandlerFunc {
|
|||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue