Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlessio Caiazza <acaiazza@gitlab.com>2018-03-21 20:29:55 +0300
committerNick Thomas <nick@gitlab.com>2018-03-21 20:29:55 +0300
commit51f0df18e3d8dd5f1e0faeea3b2a41e6ff73f551 (patch)
tree8c222620310e6bb5a967f099314e40428e920c73 /internal
parentfe1561978ed164220e471129c9b2fa6b89d07992 (diff)
Add /etc/resolv.conf and /etc/ssl/certs to pages chroot
Diffstat (limited to 'internal')
-rw-r--r--internal/jail/jail.go137
-rw-r--r--internal/jail/jail_test.go236
-rw-r--r--internal/jail/mount_linux.go32
-rw-r--r--internal/jail/mount_not_supported.go23
4 files changed, 428 insertions, 0 deletions
diff --git a/internal/jail/jail.go b/internal/jail/jail.go
new file mode 100644
index 00000000..745ccb2a
--- /dev/null
+++ b/internal/jail/jail.go
@@ -0,0 +1,137 @@
+package jail
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "time"
+)
+
+type pathAndMode struct {
+ path string
+ mode os.FileMode
+}
+
+// Jail is a Chroot jail builder
+type Jail struct {
+ directories []pathAndMode
+ files map[string]pathAndMode
+ bindMounts map[string]string
+}
+
+// New returns a Jail for path
+func New(path string, perm os.FileMode) *Jail {
+ return &Jail{
+ directories: []pathAndMode{pathAndMode{path: path, mode: perm}},
+ files: make(map[string]pathAndMode),
+ bindMounts: make(map[string]string),
+ }
+}
+
+// TimestampedJail return a Jail with Path composed by prefix and current timestamp
+func TimestampedJail(prefix string, perm os.FileMode) *Jail {
+ jailPath := path.Join(os.TempDir(), fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()))
+
+ return New(jailPath, perm)
+}
+
+// Path returns the path of the jail
+func (j *Jail) Path() string {
+ return j.directories[0].path
+}
+
+// Build creates the jail, making directories and copying files
+func (j *Jail) Build() error {
+ for _, dir := range j.directories {
+ if err := os.Mkdir(dir.path, dir.mode); err != nil {
+ return fmt.Errorf("Can't create directory %q. %s", dir.path, err)
+ }
+ }
+
+ for dest, src := range j.files {
+ if err := copyFile(dest, src.path, src.mode); err != nil {
+ return fmt.Errorf("Can't copy %q -> %q. %s", src.path, dest, err)
+ }
+ }
+
+ return j.mount()
+}
+
+// Dispose erases everything inside the jail
+func (j *Jail) Dispose() error {
+ err := j.unmount()
+ if err != nil {
+ return err
+ }
+
+ err = os.RemoveAll(j.Path())
+ if err != nil {
+ return fmt.Errorf("Can't delete jail %q. %s", j.Path(), err)
+ }
+
+ return nil
+}
+
+// MkDir enqueue a mkdir operation at jail building time
+func (j *Jail) MkDir(path string, perm os.FileMode) {
+ j.directories = append(j.directories, pathAndMode{path: j.ExternalPath(path), mode: perm})
+}
+
+// CopyTo enqueues a file copy operation at jail building time
+func (j *Jail) CopyTo(dest, src string) error {
+ fi, err := os.Stat(src)
+ if err != nil {
+ return fmt.Errorf("Can't stat %q. %s", src, err)
+ }
+
+ if fi.IsDir() {
+ return fmt.Errorf("Can't copy directories. %s", src)
+ }
+
+ jailedDest := j.ExternalPath(dest)
+ j.files[jailedDest] = pathAndMode{
+ path: src,
+ mode: fi.Mode(),
+ }
+
+ return nil
+}
+
+// Copy enqueues a file copy operation at jail building time
+func (j *Jail) Copy(path string) error {
+ return j.CopyTo(path, path)
+}
+
+// Bind enqueues a bind mount operation at jail building time
+func (j *Jail) Bind(dest, src string) {
+ jailedDest := j.ExternalPath(dest)
+ j.bindMounts[jailedDest] = src
+}
+
+// LazyUnbind detaches all binded mountpoints
+func (j *Jail) LazyUnbind() error {
+ return j.unmount()
+}
+
+// ExternalPath converts a jail internal path to the equivalent jail external path
+func (j *Jail) ExternalPath(internal string) string {
+ return path.Join(j.Path(), internal)
+}
+
+func copyFile(dest, src string, perm os.FileMode) error {
+ srcFile, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer srcFile.Close()
+
+ destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
+ if err != nil {
+ return err
+ }
+ defer destFile.Close()
+
+ _, err = io.Copy(destFile, srcFile)
+ return err
+}
diff --git a/internal/jail/jail_test.go b/internal/jail/jail_test.go
new file mode 100644
index 00000000..2095000e
--- /dev/null
+++ b/internal/jail/jail_test.go
@@ -0,0 +1,236 @@
+package jail_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/jail"
+)
+
+func tmpJailPath() string {
+ return path.Join(os.TempDir(), fmt.Sprintf("my-jail-%d", time.Now().Unix()))
+}
+
+func TestTimestampedJails(t *testing.T) {
+ assert := assert.New(t)
+
+ prefix := "jail"
+ var mode os.FileMode = 0755
+
+ j1 := jail.TimestampedJail(prefix, mode)
+ j2 := jail.TimestampedJail(prefix, mode)
+
+ assert.NotEqual(j1.Path, j2.Path())
+}
+
+func TestJailPath(t *testing.T) {
+ assert := assert.New(t)
+
+ jailPath := tmpJailPath()
+ cage := jail.New(jailPath, 0755)
+
+ assert.Equal(jailPath, cage.Path())
+}
+
+func TestJailBuild(t *testing.T) {
+ assert := assert.New(t)
+
+ jailPath := tmpJailPath()
+ cage := jail.New(jailPath, 0755)
+
+ _, err := os.Stat(cage.Path())
+ assert.Error(err, "Jail path should not exist before Jail.Build()")
+
+ err = cage.Build()
+ assert.NoError(err)
+ defer cage.Dispose()
+
+ _, err = os.Stat(cage.Path())
+ assert.NoError(err, "Jail path should exist after Jail.Build()")
+}
+
+func TestJailDispose(t *testing.T) {
+ assert := assert.New(t)
+
+ jailPath := tmpJailPath()
+ cage := jail.New(jailPath, 0755)
+
+ err := cage.Build()
+ assert.NoError(err)
+
+ err = cage.Dispose()
+ assert.NoError(err)
+
+ _, err = os.Stat(cage.Path())
+ assert.Error(err, "Jail path should not exist after Jail.Dispose()")
+}
+
+func TestJailDisposeDoNotFailOnMissingPath(t *testing.T) {
+ assert := assert.New(t)
+
+ jailPath := tmpJailPath()
+ cage := jail.New(jailPath, 0755)
+
+ _, err := os.Stat(cage.Path())
+ assert.Error(err, "Jail path should not exist")
+
+ err = cage.Dispose()
+ assert.NoError(err)
+}
+
+func TestJailWithFiles(t *testing.T) {
+ tests := []struct {
+ name string
+ directories []string
+ files []string
+ error bool
+ }{
+ {
+ name: "Happy path",
+ directories: []string{"/tmp", "/tmp/foo", "/bar"},
+ },
+ {
+ name: "Missing direcories in path",
+ directories: []string{"/tmp/foo/bar"},
+ error: true,
+ },
+ {
+ name: "copy /etc/resolv.conf",
+ directories: []string{"/etc"},
+ files: []string{"/etc/resolv.conf"},
+ },
+ {
+ name: "copy /etc/resolv.conf without creating /etc",
+ files: []string{"/etc/resolv.conf"},
+ error: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert := assert.New(t)
+
+ cage := jail.TimestampedJail("jail-mkdir", 0755)
+ for _, dir := range test.directories {
+ cage.MkDir(dir, 0755)
+ }
+ for _, file := range test.files {
+ if err := cage.Copy(file); err != nil {
+ t.Errorf("Can't prepare copy of %s inside the jail. %s", file, err)
+ }
+ }
+
+ err := cage.Build()
+ defer cage.Dispose()
+
+ if test.error {
+ assert.Error(err)
+ } else {
+ assert.NoError(err)
+
+ for _, dir := range test.directories {
+ _, err := os.Stat(path.Join(cage.Path(), dir))
+ assert.NoError(err, "jailed dir should exist")
+ }
+
+ for _, file := range test.files {
+ _, err := os.Stat(path.Join(cage.Path(), file))
+ assert.NoError(err, "Jailed file should exist")
+ }
+ }
+ })
+ }
+}
+
+func TestJailCopyTo(t *testing.T) {
+ assert := assert.New(t)
+
+ content := "hello"
+
+ cage := jail.TimestampedJail("check-file-copy", 0755)
+
+ tmpFile, err := ioutil.TempFile("", "dummy-file")
+ if err != nil {
+ t.Error("Can't create temporary file")
+ }
+ defer os.Remove(tmpFile.Name())
+ tmpFile.WriteString(content)
+
+ filePath := tmpFile.Name()
+ jailedFilePath := cage.ExternalPath(path.Base(filePath))
+
+ err = cage.CopyTo(path.Base(filePath), filePath)
+ assert.NoError(err)
+
+ err = cage.Build()
+ defer cage.Dispose()
+ assert.NoError(err)
+
+ jailedFI, err := os.Stat(jailedFilePath)
+ assert.NoError(err)
+
+ fi, err := os.Stat(filePath)
+ assert.NoError(err)
+
+ assert.Equal(fi.Mode(), jailedFI.Mode(), "jailed file should preserve file mode")
+ assert.Equal(fi.Size(), jailedFI.Size(), "jailed file should have same size of original file")
+
+ jailedContent, err := ioutil.ReadFile(jailedFilePath)
+ assert.NoError(err)
+ assert.Equal(content, string(jailedContent), "jailed file should preserve file content")
+}
+
+func TestJailLazyUnbind(t *testing.T) {
+ if os.Geteuid() != 0 || runtime.GOOS != "linux" {
+ t.Skip("chroot binding requires linux and root permissions")
+ }
+
+ assert := assert.New(t)
+
+ toBind, err := ioutil.TempDir("", "to-bind")
+ require.NoError(t, err)
+ defer os.RemoveAll(toBind)
+
+ tmpFilePath := path.Join(toBind, "a-file")
+ tmpFile, err := os.OpenFile(tmpFilePath, os.O_CREATE, 0644)
+ require.NoError(t, err)
+ tmpFile.Close()
+
+ jailPath := tmpJailPath()
+ cage := jail.New(jailPath, 0755)
+
+ cage.MkDir("/my-bind", 0755)
+ cage.Bind("/my-bind", toBind)
+
+ err = cage.Build()
+ assert.NoError(err, "jail build failed")
+
+ bindedTmpFilePath := cage.ExternalPath("/my-bind/a-file")
+ f, err := os.Open(bindedTmpFilePath)
+ assert.NoError(err, "temporary file not binded")
+ require.NotNil(t, f)
+
+ err = cage.LazyUnbind()
+ assert.NoError(err, "lazy unbind failed")
+
+ f.Close()
+ _, err = os.Stat(bindedTmpFilePath)
+ assert.Error(err, "lazy unbind should remove mount-point after file close")
+
+ err = cage.Dispose()
+ assert.NoError(err, "dispose failed")
+
+ _, err = os.Stat(cage.Path())
+ assert.Error(err, "Jail path should not exist after Jail.Dispose()")
+
+ _, err = os.Stat(tmpFilePath)
+ assert.NoError(err, "disposing a jail should not delete files under binded directories")
+}
diff --git a/internal/jail/mount_linux.go b/internal/jail/mount_linux.go
new file mode 100644
index 00000000..f285f8b4
--- /dev/null
+++ b/internal/jail/mount_linux.go
@@ -0,0 +1,32 @@
+package jail
+
+import (
+ "fmt"
+
+ "golang.org/x/sys/unix"
+)
+
+func (j *Jail) mount() error {
+ for dest, src := range j.bindMounts {
+ var opts uintptr = unix.MS_BIND | unix.MS_REC
+ if err := unix.Mount(src, dest, "none", opts, ""); err != nil {
+ return fmt.Errorf("Failed to bind mount %s on %s. %s", src, dest, err)
+ }
+ }
+
+ return nil
+}
+
+func (j *Jail) unmount() error {
+ for dest := range j.bindMounts {
+ if err := unix.Unmount(dest, unix.MNT_DETACH); err != nil {
+ // A second invocation on unmount with MNT_DETACH flag will return EINVAL
+ // there's no need to abort with an error if bind mountpoint is already unmounted
+ if err != unix.EINVAL {
+ return fmt.Errorf("Failed to unmount %s. %s", dest, err)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/jail/mount_not_supported.go b/internal/jail/mount_not_supported.go
new file mode 100644
index 00000000..b8e359af
--- /dev/null
+++ b/internal/jail/mount_not_supported.go
@@ -0,0 +1,23 @@
+// +build !linux
+
+package jail
+
+import (
+ "fmt"
+ "runtime"
+)
+
+func (j *Jail) notSupported() error {
+ if len(j.bindMounts) > 0 {
+ return fmt.Errorf("Bind mount not supported on %s", runtime.GOOS)
+ }
+
+ return nil
+}
+func (j *Jail) mount() error {
+ return j.notSupported()
+}
+
+func (j *Jail) unmount() error {
+ return j.notSupported()
+}