Introduce path Clean/Join helper functions (#23495)
Since #23493 has conflicts with latest commits, this PR is my proposal for fixing #23371 Details are in the comments And refactor the `modules/options` module, to make it always use "filepath" to access local files. Benefits: * No need to do `util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/")` any more (not only one before) * The function behaviors are clearly defined
This commit is contained in:
parent
253a00aaac
commit
ce9dee5a1e
|
@ -34,7 +34,7 @@ func init() {
|
||||||
|
|
||||||
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
||||||
func (l *LFSLock) BeforeInsert() {
|
func (l *LFSLock) BeforeInsert() {
|
||||||
l.Path = util.CleanPath(l.Path)
|
l.Path = util.PathJoinRel(l.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLFSLock creates a new lock.
|
// CreateLFSLock creates a new lock.
|
||||||
|
@ -49,7 +49,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.Path = util.CleanPath(lock.Path)
|
lock.Path = util.PathJoinRel(lock.Path)
|
||||||
lock.RepoID = repo.ID
|
lock.RepoID = repo.ID
|
||||||
|
|
||||||
l, err := GetLFSLock(dbCtx, repo, lock.Path)
|
l, err := GetLFSLock(dbCtx, repo, lock.Path)
|
||||||
|
@ -69,7 +69,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
|
||||||
|
|
||||||
// GetLFSLock returns release by given path.
|
// GetLFSLock returns release by given path.
|
||||||
func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (*LFSLock, error) {
|
func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (*LFSLock, error) {
|
||||||
path = util.CleanPath(path)
|
path = util.PathJoinRel(path)
|
||||||
rel := &LFSLock{RepoID: repo.ID}
|
rel := &LFSLock{RepoID: repo.ID}
|
||||||
has, err := db.GetEngine(ctx).Where("lower(path) = ?", strings.ToLower(path)).Get(rel)
|
has, err := db.GetEngine(ctx).Where("lower(path) = ?", strings.ToLower(path)).Get(rel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,36 +7,38 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var directories = make(directorySet)
|
||||||
|
|
||||||
// Locale reads the content of a specific locale from static/bindata or custom path.
|
// Locale reads the content of a specific locale from static/bindata or custom path.
|
||||||
func Locale(name string) ([]byte, error) {
|
func Locale(name string) ([]byte, error) {
|
||||||
return fileFromDir(path.Join("locale", util.CleanPath(name)))
|
return fileFromOptionsDir("locale", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readme reads the content of a specific readme from static/bindata or custom path.
|
// Readme reads the content of a specific readme from static/bindata or custom path.
|
||||||
func Readme(name string) ([]byte, error) {
|
func Readme(name string) ([]byte, error) {
|
||||||
return fileFromDir(path.Join("readme", util.CleanPath(name)))
|
return fileFromOptionsDir("readme", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
|
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
|
||||||
func Gitignore(name string) ([]byte, error) {
|
func Gitignore(name string) ([]byte, error) {
|
||||||
return fileFromDir(path.Join("gitignore", util.CleanPath(name)))
|
return fileFromOptionsDir("gitignore", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// License reads the content of a specific license from static/bindata or custom path.
|
// License reads the content of a specific license from static/bindata or custom path.
|
||||||
func License(name string) ([]byte, error) {
|
func License(name string) ([]byte, error) {
|
||||||
return fileFromDir(path.Join("license", util.CleanPath(name)))
|
return fileFromOptionsDir("license", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels reads the content of a specific labels from static/bindata or custom path.
|
// Labels reads the content of a specific labels from static/bindata or custom path.
|
||||||
func Labels(name string) ([]byte, error) {
|
func Labels(name string) ([]byte, error) {
|
||||||
return fileFromDir(path.Join("label", util.CleanPath(name)))
|
return fileFromOptionsDir("label", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalkLocales reads the content of a specific locale
|
// WalkLocales reads the content of a specific locale
|
||||||
|
@ -79,17 +81,54 @@ func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func statDirIfExist(dir string) ([]string, error) {
|
// mustLocalPathAbs coverts a path to absolute path
|
||||||
isDir, err := util.IsDir(dir)
|
// FIXME: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
|
||||||
|
func mustLocalPathAbs(s string) string {
|
||||||
|
abs, err := filepath.Abs(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to check if static directory %s is a directory. %w", dir, err)
|
// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
|
||||||
|
log.Fatal("Unable to get absolute path for %q: %v", s, err)
|
||||||
}
|
}
|
||||||
if !isDir {
|
return abs
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
files, err := util.StatDir(dir, true)
|
|
||||||
|
func joinLocalPaths(baseDirs []string, subDir string, elems ...string) (paths []string) {
|
||||||
|
abs := make([]string, len(elems)+2)
|
||||||
|
abs[1] = subDir
|
||||||
|
copy(abs[2:], elems)
|
||||||
|
for _, baseDir := range baseDirs {
|
||||||
|
abs[0] = mustLocalPathAbs(baseDir)
|
||||||
|
paths = append(paths, util.FilePathJoinAbs(abs...))
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func listLocalDirIfExist(baseDirs []string, subDir string, elems ...string) (files []string, err error) {
|
||||||
|
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
||||||
|
isDir, err := util.IsDir(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to read directory %q. %w", dir, err)
|
return nil, fmt.Errorf("unable to check if path %q is a directory. %w", localPath, err)
|
||||||
|
} else if !isDir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dirFiles, err := util.StatDir(localPath, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read directory %q. %w", localPath, err)
|
||||||
|
}
|
||||||
|
files = append(files, dirFiles...)
|
||||||
}
|
}
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readLocalFile(baseDirs []string, subDir string, elems ...string) ([]byte, error) {
|
||||||
|
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
||||||
|
data, err := os.ReadFile(localPath)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
log.Error("Unable to read file %q. Error: %v", localPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
|
@ -6,62 +6,26 @@
|
||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var directories = make(directorySet)
|
|
||||||
|
|
||||||
// Dir returns all files from static or custom directory.
|
// Dir returns all files from static or custom directory.
|
||||||
func Dir(name string) ([]string, error) {
|
func Dir(name string) ([]string, error) {
|
||||||
if directories.Filled(name) {
|
if directories.Filled(name) {
|
||||||
return directories.Get(name), nil
|
return directories.Get(name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
result, err := listLocalDirIfExist([]string{setting.CustomPath, setting.StaticRootPath}, "options", name)
|
||||||
|
|
||||||
for _, dir := range []string{
|
|
||||||
path.Join(setting.CustomPath, "options", name), // custom dir
|
|
||||||
path.Join(setting.StaticRootPath, "options", name), // static dir
|
|
||||||
} {
|
|
||||||
files, err := statDirIfExist(dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result = append(result, files...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return directories.AddAndGet(name, result), nil
|
return directories.AddAndGet(name, result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileFromDir is a helper to read files from static or custom path.
|
// fileFromOptionsDir is a helper to read files from custom or static path.
|
||||||
func fileFromDir(name string) ([]byte, error) {
|
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
||||||
customPath := path.Join(setting.CustomPath, "options", name)
|
return readLocalFile([]string{setting.CustomPath, setting.StaticRootPath}, "options", elems...)
|
||||||
|
|
||||||
isFile, err := util.IsFile(customPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
return os.ReadFile(customPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
staticPath := path.Join(setting.StaticRootPath, "options", name)
|
|
||||||
|
|
||||||
isFile, err = util.IsFile(staticPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", staticPath, err)
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
return os.ReadFile(staticPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte{}, fmt.Errorf("Asset file does not exist: %s", name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDynamic will return false when using embedded data (-tags bindata)
|
// IsDynamic will return false when using embedded data (-tags bindata)
|
||||||
|
|
|
@ -8,34 +8,21 @@ package options
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var directories = make(directorySet)
|
// Dir returns all files from custom directory or bindata.
|
||||||
|
|
||||||
// Dir returns all files from bindata or custom directory.
|
|
||||||
func Dir(name string) ([]string, error) {
|
func Dir(name string) ([]string, error) {
|
||||||
if directories.Filled(name) {
|
if directories.Filled(name) {
|
||||||
return directories.Get(name), nil
|
return directories.Get(name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
result, err := listLocalDirIfExist([]string{setting.CustomPath}, "options", name)
|
||||||
|
|
||||||
for _, dir := range []string{
|
|
||||||
path.Join(setting.CustomPath, "options", name), // custom dir
|
|
||||||
// no static dir
|
|
||||||
} {
|
|
||||||
files, err := statDirIfExist(dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result = append(result, files...)
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := AssetDir(name)
|
files, err := AssetDir(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -64,24 +51,18 @@ func AssetDir(dirName string) ([]string, error) {
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileFromDir is a helper to read files from bindata or custom path.
|
// fileFromOptionsDir is a helper to read files from custom path or bindata.
|
||||||
func fileFromDir(name string) ([]byte, error) {
|
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
||||||
customPath := path.Join(setting.CustomPath, "options", name)
|
// only try custom dir, no static dir
|
||||||
|
if data, err := readLocalFile([]string{setting.CustomPath}, "options", elems...); err == nil {
|
||||||
isFile, err := util.IsFile(customPath)
|
return data, nil
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
return os.ReadFile(customPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := Assets.Open(name)
|
f, err := Assets.Open(util.PathJoinRelX(elems...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
return io.ReadAll(f)
|
return io.ReadAll(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,29 +45,19 @@ func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file := req.URL.Path
|
var corsSent bool
|
||||||
file = file[len(opts.Prefix):]
|
|
||||||
if len(file) == 0 {
|
|
||||||
resp.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(file, "\\") {
|
|
||||||
resp.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file = "/" + file
|
|
||||||
|
|
||||||
var written bool
|
|
||||||
if opts.CorsHandler != nil {
|
if opts.CorsHandler != nil {
|
||||||
written = true
|
|
||||||
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
written = false
|
corsSent = true
|
||||||
})).ServeHTTP(resp, req)
|
})).ServeHTTP(resp, req)
|
||||||
}
|
}
|
||||||
if written {
|
// If CORS is not sent, the response must have been written by other handlers
|
||||||
|
if !corsSent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file := req.URL.Path[len(opts.Prefix):]
|
||||||
|
|
||||||
// custom files
|
// custom files
|
||||||
if opts.handle(resp, req, http.Dir(custPath), file) {
|
if opts.handle(resp, req, http.Dir(custPath), file) {
|
||||||
return
|
return
|
||||||
|
@ -102,8 +92,8 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
|
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
|
||||||
// use clean to keep the file is a valid path with no . or ..
|
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
|
||||||
f, err := fs.Open(util.CleanPath(file))
|
f, err := fs.Open(util.PathJoinRelX(file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -5,11 +5,11 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -41,13 +41,19 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||||
}
|
}
|
||||||
config := configInterface.(LocalStorageConfig)
|
config := configInterface.(LocalStorageConfig)
|
||||||
|
|
||||||
|
if !filepath.IsAbs(config.Path) {
|
||||||
|
return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
|
||||||
|
}
|
||||||
log.Info("Creating new Local Storage at %s", config.Path)
|
log.Info("Creating new Local Storage at %s", config.Path)
|
||||||
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.TemporaryPath == "" {
|
if config.TemporaryPath == "" {
|
||||||
config.TemporaryPath = config.Path + "/tmp"
|
config.TemporaryPath = filepath.Join(config.Path, "tmp")
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(config.TemporaryPath) {
|
||||||
|
return nil, fmt.Errorf("LocalStorageConfig.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LocalStorage{
|
return &LocalStorage{
|
||||||
|
@ -58,7 +64,7 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalStorage) buildLocalPath(p string) string {
|
func (l *LocalStorage) buildLocalPath(p string) string {
|
||||||
return filepath.Join(l.dir, util.CleanPath(strings.ReplaceAll(p, "\\", "/")))
|
return util.FilePathJoinAbs(l.dir, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a file
|
// Open a file
|
||||||
|
@ -128,10 +134,7 @@ func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
|
||||||
|
|
||||||
// IterateObjects iterates across the objects in the local storage
|
// IterateObjects iterates across the objects in the local storage
|
||||||
func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
|
func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
|
||||||
dir := l.dir
|
dir := l.buildLocalPath(prefix)
|
||||||
if prefix != "" {
|
|
||||||
dir = filepath.Join(l.dir, util.CleanPath(prefix))
|
|
||||||
}
|
|
||||||
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -20,29 +20,29 @@ func TestBuildLocalPath(t *testing.T) {
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"a",
|
"/a",
|
||||||
"0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a",
|
"/a",
|
||||||
"../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a",
|
"/a",
|
||||||
"0\\a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"0\\a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"b",
|
"/b",
|
||||||
"a/../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"a/../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"/b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"b",
|
"/b",
|
||||||
"a\\..\\0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"a\\..\\0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"/b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MinioStorage) buildMinioPath(p string) string {
|
func (m *MinioStorage) buildMinioPath(p string) string {
|
||||||
return strings.TrimPrefix(path.Join(m.basePath, util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/")
|
return util.PathJoinRelX(m.basePath, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open open a file
|
// Open open a file
|
||||||
|
|
|
@ -5,6 +5,7 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -14,21 +15,92 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CleanPath ensure to clean the path
|
// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
|
||||||
func CleanPath(p string) string {
|
// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
|
||||||
if strings.HasPrefix(p, "/") {
|
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||||
return path.Clean(p)
|
//
|
||||||
|
// empty => ``
|
||||||
|
// `` => ``
|
||||||
|
// `..` => `.`
|
||||||
|
// `dir` => `dir`
|
||||||
|
// `/dir/` => `dir`
|
||||||
|
// `foo\..\bar` => `foo\..\bar`
|
||||||
|
// {`foo`, ``, `bar`} => `foo/bar`
|
||||||
|
// {`foo`, `..`, `bar`} => `foo/bar`
|
||||||
|
func PathJoinRel(elem ...string) string {
|
||||||
|
elems := make([]string, len(elem))
|
||||||
|
for i, e := range elem {
|
||||||
|
if e == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elems[i] = path.Clean("/" + e)
|
||||||
|
}
|
||||||
|
p := path.Join(elems...)
|
||||||
|
if p == "" {
|
||||||
|
return ""
|
||||||
|
} else if p == "/" {
|
||||||
|
return "."
|
||||||
|
} else {
|
||||||
|
return p[1:]
|
||||||
}
|
}
|
||||||
return path.Clean("/" + p)[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureAbsolutePath ensure that a path is absolute, making it
|
// PathJoinRelX joins the path elements into a single path like PathJoinRel,
|
||||||
// relative to absoluteBase if necessary
|
// and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
|
||||||
func EnsureAbsolutePath(path, absoluteBase string) string {
|
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||||
if filepath.IsAbs(path) {
|
// It returns similar results as PathJoinRel except:
|
||||||
return path
|
//
|
||||||
|
// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
|
||||||
|
//
|
||||||
|
// All backslashes are handled as slashes, the result only contains slashes.
|
||||||
|
func PathJoinRelX(elem ...string) string {
|
||||||
|
elems := make([]string, len(elem))
|
||||||
|
for i, e := range elem {
|
||||||
|
if e == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return filepath.Join(absoluteBase, path)
|
elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
|
||||||
|
}
|
||||||
|
return PathJoinRel(elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSeparator = string(os.PathSeparator)
|
||||||
|
|
||||||
|
// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
|
||||||
|
// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
|
||||||
|
// The first element must be an absolute path, caller should prepare the base path.
|
||||||
|
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||||
|
// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
|
||||||
|
//
|
||||||
|
// {`/foo`, ``, `bar`} => `/foo/bar`
|
||||||
|
// {`/foo`, `..`, `bar`} => `/foo/bar`
|
||||||
|
func FilePathJoinAbs(elem ...string) string {
|
||||||
|
elems := make([]string, len(elem))
|
||||||
|
|
||||||
|
// POISX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
|
||||||
|
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
|
||||||
|
if isOSWindows() {
|
||||||
|
elems[0] = filepath.Clean(elem[0])
|
||||||
|
} else {
|
||||||
|
elems[0] = filepath.Clean(strings.ReplaceAll(elem[0], "\\", pathSeparator))
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(elems[0]) {
|
||||||
|
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
|
||||||
|
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(elem); i++ {
|
||||||
|
if elem[i] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isOSWindows() {
|
||||||
|
elems[i] = filepath.Clean(pathSeparator + elem[i])
|
||||||
|
} else {
|
||||||
|
elems[i] = filepath.Clean(pathSeparator + strings.ReplaceAll(elem[i], "\\", pathSeparator))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the elems[0] must be an absolute path, just join them together
|
||||||
|
return filepath.Join(elems...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDir returns true if given path is a directory,
|
// IsDir returns true if given path is a directory,
|
||||||
|
|
|
@ -138,13 +138,75 @@ func TestMisc_IsReadmeFileName(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanPath(t *testing.T) {
|
func TestCleanPath(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := []struct {
|
||||||
"../../test": "test",
|
elems []string
|
||||||
"/test": "/test",
|
expected string
|
||||||
"/../test": "/test",
|
}{
|
||||||
|
{[]string{}, ``},
|
||||||
|
{[]string{``}, ``},
|
||||||
|
{[]string{`..`}, `.`},
|
||||||
|
{[]string{`a`}, `a`},
|
||||||
|
{[]string{`/a/`}, `a`},
|
||||||
|
{[]string{`../a/`, `../b`, `c/..`, `d`}, `a/b/d`},
|
||||||
|
{[]string{`a\..\b`}, `a\..\b`},
|
||||||
|
{[]string{`a`, ``, `b`}, `a/b`},
|
||||||
|
{[]string{`a`, `..`, `b`}, `a/b`},
|
||||||
|
{[]string{`lfs`, `repo/..`, `user/../path`}, `lfs/path`},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.expected, PathJoinRel(c.elems...), "case: %v", c.elems)
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range cases {
|
cases = []struct {
|
||||||
assert.Equal(t, v, CleanPath(k))
|
elems []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{}, ``},
|
||||||
|
{[]string{``}, ``},
|
||||||
|
{[]string{`..`}, `.`},
|
||||||
|
{[]string{`a`}, `a`},
|
||||||
|
{[]string{`/a/`}, `a`},
|
||||||
|
{[]string{`../a/`, `../b`, `c/..`, `d`}, `a/b/d`},
|
||||||
|
{[]string{`a\..\b`}, `b`},
|
||||||
|
{[]string{`a`, ``, `b`}, `a/b`},
|
||||||
|
{[]string{`a`, `..`, `b`}, `a/b`},
|
||||||
|
{[]string{`lfs`, `repo/..`, `user/../path`}, `lfs/path`},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.expected, PathJoinRelX(c.elems...), "case: %v", c.elems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for POSIX only, but the result is similar on Windows, because the first element must be an absolute path
|
||||||
|
if isOSWindows() {
|
||||||
|
cases = []struct {
|
||||||
|
elems []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{`C:\..`}, `C:\`},
|
||||||
|
{[]string{`C:\a`}, `C:\a`},
|
||||||
|
{[]string{`C:\a/`}, `C:\a`},
|
||||||
|
{[]string{`C:\..\a\`, `../b`, `c\..`, `d`}, `C:\a\b\d`},
|
||||||
|
{[]string{`C:\a/..\b`}, `C:\b`},
|
||||||
|
{[]string{`C:\a`, ``, `b`}, `C:\a\b`},
|
||||||
|
{[]string{`C:\a`, `..`, `b`}, `C:\a\b`},
|
||||||
|
{[]string{`C:\lfs`, `repo/..`, `user/../path`}, `C:\lfs\path`},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cases = []struct {
|
||||||
|
elems []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{`/..`}, `/`},
|
||||||
|
{[]string{`/a`}, `/a`},
|
||||||
|
{[]string{`/a/`}, `/a`},
|
||||||
|
{[]string{`/../a/`, `../b`, `c/..`, `d`}, `/a/b/d`},
|
||||||
|
{[]string{`/a\..\b`}, `/b`},
|
||||||
|
{[]string{`/a`, ``, `b`}, `/a/b`},
|
||||||
|
{[]string{`/a`, `..`, `b`}, `/a/b`},
|
||||||
|
{[]string{`/lfs`, `repo/..`, `user/../path`}, `/lfs/path`},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems...), "case: %v", c.elems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
|
||||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||||
rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/"))
|
rPath = util.PathJoinRelX(rPath)
|
||||||
|
|
||||||
u, err := objStore.URL(rPath, path.Base(rPath))
|
u, err := objStore.URL(rPath, path.Base(rPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -81,8 +81,8 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
|
||||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||||
rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/"))
|
rPath = util.PathJoinRelX(rPath)
|
||||||
if rPath == "" {
|
if rPath == "" || rPath == "." {
|
||||||
http.Error(w, "file not found", http.StatusNotFound)
|
http.Error(w, "file not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -726,7 +726,7 @@ func UploadFilePost(ctx *context.Context) {
|
||||||
|
|
||||||
func cleanUploadFileName(name string) string {
|
func cleanUploadFileName(name string) string {
|
||||||
// Rebase the filename
|
// Rebase the filename
|
||||||
name = strings.Trim(util.CleanPath(name), "/")
|
name = util.PathJoinRel(name)
|
||||||
// Git disallows any filenames to have a .git directory in them.
|
// Git disallows any filenames to have a .git directory in them.
|
||||||
for _, part := range strings.Split(name, "/") {
|
for _, part := range strings.Split(name, "/") {
|
||||||
if strings.ToLower(part) == ".git" {
|
if strings.ToLower(part) == ".git" {
|
||||||
|
|
|
@ -207,7 +207,7 @@ func LFSLockFile(ctx *context.Context) {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lockPath = util.CleanPath(lockPath)
|
lockPath = util.PathJoinRel(lockPath)
|
||||||
if len(lockPath) == 0 {
|
if len(lockPath) == 0 {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
|
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
|
||||||
|
|
|
@ -865,8 +865,8 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
|
||||||
_, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk)
|
_, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY: The TreePath must be cleaned!
|
// SECURITY: The TreePath must be cleaned! use relative path
|
||||||
comment.TreePath = util.CleanPath(comment.TreePath)
|
comment.TreePath = util.PathJoinRel(comment.TreePath)
|
||||||
|
|
||||||
var patch string
|
var patch string
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe()
|
||||||
|
|
|
@ -8,8 +8,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
packages_module "code.gitea.io/gitea/modules/packages"
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
|
@ -33,7 +31,7 @@ type BlobUploader struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFilePath(id string) string {
|
func buildFilePath(id string) string {
|
||||||
return filepath.Join(setting.Packages.ChunkedUploadPath, util.CleanPath(strings.ReplaceAll(id, "\\", "/")))
|
return util.FilePathJoinAbs(setting.Packages.ChunkedUploadPath, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBlobUploader creates a new blob uploader for the given id
|
// NewBlobUploader creates a new blob uploader for the given id
|
||||||
|
|
|
@ -129,7 +129,7 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m
|
||||||
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
|
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
|
||||||
func CleanUploadFileName(name string) string {
|
func CleanUploadFileName(name string) string {
|
||||||
// Rebase the filename
|
// Rebase the filename
|
||||||
name = strings.Trim(util.CleanPath(name), "/")
|
name = util.PathJoinRel(name)
|
||||||
// Git disallows any filenames to have a .git directory in them.
|
// Git disallows any filenames to have a .git directory in them.
|
||||||
for _, part := range strings.Split(name, "/") {
|
for _, part := range strings.Split(name, "/") {
|
||||||
if strings.ToLower(part) == ".git" {
|
if strings.ToLower(part) == ".git" {
|
||||||
|
|
Loading…
Reference in a new issue