main
Raw Download raw file
  1//go:build !js
  2// +build !js
  3
  4/*
  5   Copyright 2022 The Flux authors.
  6
  7   Licensed under the Apache License, Version 2.0 (the "License");
  8   you may not use this file except in compliance with the License.
  9   You may obtain a copy of the License at
 10
 11       http://www.apache.org/licenses/LICENSE-2.0
 12
 13   Unless required by applicable law or agreed to in writing, software
 14   distributed under the License is distributed on an "AS IS" BASIS,
 15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 16   See the License for the specific language governing permissions and
 17   limitations under the License.
 18*/
 19
 20package osfs
 21
 22import (
 23	"fmt"
 24	"os"
 25	"path/filepath"
 26	"strings"
 27
 28	securejoin "github.com/cyphar/filepath-securejoin"
 29	"github.com/go-git/go-billy/v5"
 30)
 31
 32// BoundOS is a fs implementation based on the OS filesystem which is bound to
 33// a base dir.
 34// Prefer this fs implementation over ChrootOS.
 35//
 36// Behaviours of note:
 37//  1. Read and write operations can only be directed to files which descends
 38//     from the base dir.
 39//  2. Symlinks don't have their targets modified, and therefore can point
 40//     to locations outside the base dir or to non-existent paths.
 41//  3. Readlink and Lstat ensures that the link file is located within the base
 42//     dir, evaluating any symlinks that file or base dir may contain.
 43type BoundOS struct {
 44	baseDir         string
 45	deduplicatePath bool
 46}
 47
 48func newBoundOS(d string, deduplicatePath bool) billy.Filesystem {
 49	return &BoundOS{baseDir: d, deduplicatePath: deduplicatePath}
 50}
 51
 52func (fs *BoundOS) Create(filename string) (billy.File, error) {
 53	return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
 54}
 55
 56func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
 57	fn, err := fs.abs(filename)
 58	if err != nil {
 59		return nil, err
 60	}
 61	return openFile(fn, flag, perm, fs.createDir)
 62}
 63
 64func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
 65	dir, err := fs.abs(path)
 66	if err != nil {
 67		return nil, err
 68	}
 69
 70	return readDir(dir)
 71}
 72
 73func (fs *BoundOS) Rename(from, to string) error {
 74	f, err := fs.abs(from)
 75	if err != nil {
 76		return err
 77	}
 78	t, err := fs.abs(to)
 79	if err != nil {
 80		return err
 81	}
 82
 83	// MkdirAll for target name.
 84	if err := fs.createDir(t); err != nil {
 85		return err
 86	}
 87
 88	return os.Rename(f, t)
 89}
 90
 91func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
 92	dir, err := fs.abs(path)
 93	if err != nil {
 94		return err
 95	}
 96	return os.MkdirAll(dir, perm)
 97}
 98
 99func (fs *BoundOS) Open(filename string) (billy.File, error) {
100	return fs.OpenFile(filename, os.O_RDONLY, 0)
101}
102
103func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
104	filename, err := fs.abs(filename)
105	if err != nil {
106		return nil, err
107	}
108	return os.Stat(filename)
109}
110
111func (fs *BoundOS) Remove(filename string) error {
112	fn, err := fs.abs(filename)
113	if err != nil {
114		return err
115	}
116	return os.Remove(fn)
117}
118
119// TempFile creates a temporary file. If dir is empty, the file
120// will be created within the OS Temporary dir. If dir is provided
121// it must descend from the current base dir.
122func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
123	if dir != "" {
124		var err error
125		dir, err = fs.abs(dir)
126		if err != nil {
127			return nil, err
128		}
129	}
130
131	return tempFile(dir, prefix)
132}
133
134func (fs *BoundOS) Join(elem ...string) string {
135	return filepath.Join(elem...)
136}
137
138func (fs *BoundOS) RemoveAll(path string) error {
139	dir, err := fs.abs(path)
140	if err != nil {
141		return err
142	}
143	return os.RemoveAll(dir)
144}
145
146func (fs *BoundOS) Symlink(target, link string) error {
147	ln, err := fs.abs(link)
148	if err != nil {
149		return err
150	}
151	// MkdirAll for containing dir.
152	if err := fs.createDir(ln); err != nil {
153		return err
154	}
155	return os.Symlink(target, ln)
156}
157
158func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
159	filename = filepath.Clean(filename)
160	if !filepath.IsAbs(filename) {
161		filename = filepath.Join(fs.baseDir, filename)
162	}
163	if ok, err := fs.insideBaseDirEval(filename); !ok {
164		return nil, err
165	}
166	return os.Lstat(filename)
167}
168
169func (fs *BoundOS) Readlink(link string) (string, error) {
170	if !filepath.IsAbs(link) {
171		link = filepath.Clean(filepath.Join(fs.baseDir, link))
172	}
173	if ok, err := fs.insideBaseDirEval(link); !ok {
174		return "", err
175	}
176	return os.Readlink(link)
177}
178
179// Chroot returns a new OS filesystem, with the base dir set to the
180// result of joining the provided path with the underlying base dir.
181func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
182	joined, err := securejoin.SecureJoin(fs.baseDir, path)
183	if err != nil {
184		return nil, err
185	}
186	return New(joined), nil
187}
188
189// Root returns the current base dir of the billy.Filesystem.
190// This is required in order for this implementation to be a drop-in
191// replacement for other upstream implementations (e.g. memory and osfs).
192func (fs *BoundOS) Root() string {
193	return fs.baseDir
194}
195
196func (fs *BoundOS) createDir(fullpath string) error {
197	dir := filepath.Dir(fullpath)
198	if dir != "." {
199		if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
200			return err
201		}
202	}
203
204	return nil
205}
206
207// abs transforms filename to an absolute path, taking into account the base dir.
208// Relative paths won't be allowed to ascend the base dir, so `../file` will become
209// `/working-dir/file`.
210//
211// Note that if filename is a symlink, the returned address will be the target of the
212// symlink.
213func (fs *BoundOS) abs(filename string) (string, error) {
214	if filename == fs.baseDir {
215		filename = string(filepath.Separator)
216	}
217
218	path, err := securejoin.SecureJoin(fs.baseDir, filename)
219	if err != nil {
220		return "", nil
221	}
222
223	if fs.deduplicatePath {
224		vol := filepath.VolumeName(fs.baseDir)
225		dup := filepath.Join(fs.baseDir, fs.baseDir[len(vol):])
226		if strings.HasPrefix(path, dup+string(filepath.Separator)) {
227			return fs.abs(path[len(dup):])
228		}
229	}
230	return path, nil
231}
232
233// insideBaseDir checks whether filename is located within
234// the fs.baseDir.
235func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
236	if filename == fs.baseDir {
237		return true, nil
238	}
239	if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) {
240		return false, fmt.Errorf("path outside base dir")
241	}
242	return true, nil
243}
244
245// insideBaseDirEval checks whether filename is contained within
246// a dir that is within the fs.baseDir, by first evaluating any symlinks
247// that either filename or fs.baseDir may contain.
248func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
249	// "/" contains all others.
250	if fs.baseDir == "/" {
251		return true, nil
252	}
253	dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
254	if dir == "" || os.IsNotExist(err) {
255		dir = filepath.Dir(filename)
256	}
257	wd, err := filepath.EvalSymlinks(fs.baseDir)
258	if wd == "" || os.IsNotExist(err) {
259		wd = fs.baseDir
260	}
261	if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
262		return false, fmt.Errorf("%q: path outside base dir %q: %w", filename, fs.baseDir, os.ErrNotExist)
263	}
264	return true, nil
265}