forked from Plemya-x/ALR
		
	Initial commit
This commit is contained in:
		
							
								
								
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| /* | ||||
|  * LURE - Linux User REpository | ||||
|  * Copyright (C) 2023 Elara Musayelyan | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU 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 General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| ) | ||||
|  | ||||
| func InsufficientArgsError(cmd string, exp, got int) error { | ||||
| 	argsWord := "arguments" | ||||
| 	if exp == 1 { | ||||
| 		argsWord = "argument" | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got) | ||||
| } | ||||
|  | ||||
| type ExecFunc func(hc interp.HandlerContext, name string, args []string) error | ||||
|  | ||||
| type ExecFuncs map[string]ExecFunc | ||||
|  | ||||
| // ExecHandler returns a new ExecHandlerFunc that falls back to fallback | ||||
| // if the command cannot be found in the map. If fallback is nil, the default | ||||
| // handler is used. | ||||
| func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc { | ||||
| 	return func(ctx context.Context, args []string) error { | ||||
| 		name := args[0] | ||||
|  | ||||
| 		if fn, ok := ef[name]; ok { | ||||
| 			hctx := interp.HandlerCtx(ctx) | ||||
| 			if len(args) > 1 { | ||||
| 				return fn(hctx, args[0], args[1:]) | ||||
| 			} else { | ||||
| 				return fn(hctx, args[0], nil) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if fallback == nil { | ||||
| 			fallback = interp.DefaultExecHandler(2 * time.Second) | ||||
| 		} | ||||
| 		return fallback(ctx, args) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| /* | ||||
|  * LURE - Linux User REpository | ||||
|  * Copyright (C) 2023 Elara Musayelyan | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU 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 General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package handlers_test | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"lure.sh/lure/internal/shutils/handlers" | ||||
| 	"lure.sh/lure/internal/shutils/decoder" | ||||
| 	"lure.sh/lure/pkg/distro" | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| 	"mvdan.cc/sh/v3/syntax" | ||||
| ) | ||||
|  | ||||
| const testScript = ` | ||||
| 	name='test' | ||||
| 	version='0.0.1' | ||||
| 	release=1 | ||||
| 	epoch=2 | ||||
| 	desc="Test package" | ||||
| 	homepage='https://lure.sh' | ||||
| 	maintainer='Elara Musayelyan <elara@elara.ws>' | ||||
| 	architectures=('arm64' 'amd64') | ||||
| 	license=('GPL-3.0-or-later') | ||||
| 	provides=('test') | ||||
| 	conflicts=('test') | ||||
| 	replaces=('test-old') | ||||
| 	replaces_test_os=('test-legacy') | ||||
|  | ||||
| 	deps=('sudo') | ||||
|  | ||||
| 	build_deps=('golang') | ||||
| 	build_deps_arch=('go') | ||||
|  | ||||
| 	test() { | ||||
| 		test-cmd "Hello, World" | ||||
| 		test-fb | ||||
| 	} | ||||
|  | ||||
| 	package() { | ||||
| 		install-binary test | ||||
| 	} | ||||
| ` | ||||
|  | ||||
| var osRelease = &distro.OSRelease{ | ||||
| 	ID:   "test_os", | ||||
| 	Like: []string{"arch"}, | ||||
| } | ||||
|  | ||||
| func TestExecFuncs(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	runner, err := interp.New() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	err = runner.Run(ctx, fl) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	dec := decoder.New(osRelease, runner) | ||||
| 	fn, ok := dec.GetFunc("test") | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected test() function to exist") | ||||
| 	} | ||||
|  | ||||
| 	eh := shutils.ExecFuncs{ | ||||
| 		"test-cmd": func(hc interp.HandlerContext, name string, args []string) error { | ||||
| 			if name != "test-cmd" { | ||||
| 				t.Errorf("Expected name to be 'test-cmd', got '%s'", name) | ||||
| 			} | ||||
|  | ||||
| 			if len(args) < 1 { | ||||
| 				t.Fatalf("Expected at least one argument, got %d", len(args)) | ||||
| 			} | ||||
|  | ||||
| 			if args[0] != "Hello, World" { | ||||
| 				t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0]) | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	fbInvoked := false | ||||
| 	fbHandler := func(context.Context, []string) error { | ||||
| 		fbInvoked = true | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler))) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if !fbInvoked { | ||||
| 		t.Errorf("Expected fallback handler to be invoked") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"lure.sh/fakeroot" | ||||
| 	"mvdan.cc/sh/v3/expand" | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| ) | ||||
|  | ||||
| // FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go | ||||
| // and modified to run commands in a fakeroot environent. | ||||
| func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc { | ||||
| 	return func(ctx context.Context, args []string) error { | ||||
| 		hc := interp.HandlerCtx(ctx) | ||||
| 		path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintln(hc.Stderr, err) | ||||
| 			return interp.NewExitStatus(127) | ||||
| 		} | ||||
| 		cmd := &exec.Cmd{ | ||||
| 			Path:   path, | ||||
| 			Args:   args, | ||||
| 			Env:    execEnv(hc.Env), | ||||
| 			Dir:    hc.Dir, | ||||
| 			Stdin:  hc.Stdin, | ||||
| 			Stdout: hc.Stdout, | ||||
| 			Stderr: hc.Stderr, | ||||
| 		} | ||||
|  | ||||
| 		err = fakeroot.Apply(cmd) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		err = cmd.Start() | ||||
| 		if err == nil { | ||||
| 			if done := ctx.Done(); done != nil { | ||||
| 				go func() { | ||||
| 					<-done | ||||
|  | ||||
| 					if killTimeout <= 0 || runtime.GOOS == "windows" { | ||||
| 						_ = cmd.Process.Signal(os.Kill) | ||||
| 						return | ||||
| 					} | ||||
|  | ||||
| 					// TODO: don't temporarily leak this goroutine | ||||
| 					// if the program stops itself with the | ||||
| 					// interrupt. | ||||
| 					go func() { | ||||
| 						time.Sleep(killTimeout) | ||||
| 						_ = cmd.Process.Signal(os.Kill) | ||||
| 					}() | ||||
| 					_ = cmd.Process.Signal(os.Interrupt) | ||||
| 				}() | ||||
| 			} | ||||
|  | ||||
| 			err = cmd.Wait() | ||||
| 		} | ||||
|  | ||||
| 		switch x := err.(type) { | ||||
| 		case *exec.ExitError: | ||||
| 			// started, but errored - default to 1 if OS | ||||
| 			// doesn't have exit statuses | ||||
| 			if status, ok := x.Sys().(syscall.WaitStatus); ok { | ||||
| 				if status.Signaled() { | ||||
| 					if ctx.Err() != nil { | ||||
| 						return ctx.Err() | ||||
| 					} | ||||
| 					return interp.NewExitStatus(uint8(128 + status.Signal())) | ||||
| 				} | ||||
| 				return interp.NewExitStatus(uint8(status.ExitStatus())) | ||||
| 			} | ||||
| 			return interp.NewExitStatus(1) | ||||
| 		case *exec.Error: | ||||
| 			// did not start | ||||
| 			fmt.Fprintf(hc.Stderr, "%v\n", err) | ||||
| 			return interp.NewExitStatus(127) | ||||
| 		default: | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // execEnv was extracted from github.com/mvdan/sh/interp/vars.go | ||||
| func execEnv(env expand.Environ) []string { | ||||
| 	list := make([]string, 0, 64) | ||||
| 	env.Each(func(name string, vr expand.Variable) bool { | ||||
| 		if !vr.IsSet() { | ||||
| 			// If a variable is set globally but unset in the | ||||
| 			// runner, we need to ensure it's not part of the final | ||||
| 			// list. Seems like zeroing the element is enough. | ||||
| 			// This is a linear search, but this scenario should be | ||||
| 			// rare, and the number of variables shouldn't be large. | ||||
| 			for i, kv := range list { | ||||
| 				if strings.HasPrefix(kv, name+"=") { | ||||
| 					list[i] = "" | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if vr.Exported && vr.Kind == expand.String { | ||||
| 			list = append(list, name+"="+vr.String()) | ||||
| 		} | ||||
| 		return true | ||||
| 	}) | ||||
| 	return list | ||||
| } | ||||
							
								
								
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| /* | ||||
|  * LURE - Linux User REpository | ||||
|  * Copyright (C) 2023 Elara Musayelyan | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU 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 General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| func NopReadDir(context.Context, string) ([]os.FileInfo, error) { | ||||
| 	return nil, os.ErrNotExist | ||||
| } | ||||
|  | ||||
| func NopStat(context.Context, string, bool) (os.FileInfo, error) { | ||||
| 	return nil, os.ErrNotExist | ||||
| } | ||||
|  | ||||
| func NopExec(context.Context, []string) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) { | ||||
| 	return NopRWC{}, nil | ||||
| } | ||||
|  | ||||
| type NopRWC struct{} | ||||
|  | ||||
| func (NopRWC) Read([]byte) (int, error) { | ||||
| 	return 0, io.EOF | ||||
| } | ||||
|  | ||||
| func (NopRWC) Write(b []byte) (int, error) { | ||||
| 	return len(b), nil | ||||
| } | ||||
|  | ||||
| func (NopRWC) Close() error { | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| /* | ||||
|  * LURE - Linux User REpository | ||||
|  * Copyright (C) 2023 Elara Musayelyan | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU 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 General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package handlers_test | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"lure.sh/lure/internal/shutils/handlers" | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| 	"mvdan.cc/sh/v3/syntax" | ||||
| ) | ||||
|  | ||||
| func TestNopExec(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	runner, err := interp.New( | ||||
| 		interp.ExecHandler(handlers.NopExec), | ||||
| 		interp.StdIO(os.Stdin, buf, buf), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	err = runner.Run(ctx, fl) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if buf.String() != "" { | ||||
| 		t.Fatalf("Expected empty string, got %#v", buf.String()) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| /* | ||||
|  * LURE - Linux User REpository | ||||
|  * Copyright (C) 2023 Elara Musayelyan | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU 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 General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"io/fs" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"golang.org/x/exp/slices" | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| ) | ||||
|  | ||||
| func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc { | ||||
| 	return func(ctx context.Context, s string) ([]fs.FileInfo, error) { | ||||
| 		path := filepath.Clean(s) | ||||
| 		for _, allowedPrefix := range allowedPrefixes { | ||||
| 			if strings.HasPrefix(path, allowedPrefix) { | ||||
| 				return interp.DefaultReadDirHandler()(ctx, s) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil, fs.ErrNotExist | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc { | ||||
| 	return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) { | ||||
| 		path := filepath.Clean(s) | ||||
| 		for _, allowedPrefix := range allowedPrefixes { | ||||
| 			if strings.HasPrefix(path, allowedPrefix) { | ||||
| 				return interp.DefaultStatHandler()(ctx, s, b) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil, fs.ErrNotExist | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc { | ||||
| 	return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) { | ||||
| 		path := filepath.Clean(s) | ||||
| 		for _, allowedPrefix := range allowedPrefixes { | ||||
| 			if strings.HasPrefix(path, allowedPrefix) { | ||||
| 				return interp.DefaultOpenHandler()(ctx, s, i, fm) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return NopRWC{}, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc { | ||||
| 	return func(ctx context.Context, args []string) error { | ||||
| 		if slices.Contains(allowedCmds, args[0]) { | ||||
| 			return interp.DefaultExecHandler(2*time.Second)(ctx, args) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user