From e22f2e6bdb030ac56549e04b23cc62c80c88cc30 Mon Sep 17 00:00:00 2001 From: Alexey Berezhok Date: Fri, 23 Aug 2024 23:10:01 +0300 Subject: [PATCH] Added admin utility --- Makefile | 15 +- go.mod | 7 + go.sum | 6 + hestiacp-php-selector.spec | 18 ++ installer.sh | 38 ++- src/converter/hestiacp-php-set-ver.go | 318 ++++++++++++++++++++++++++ src/main/phpselector.go | 53 ++--- 7 files changed, 418 insertions(+), 37 deletions(-) create mode 100644 go.sum create mode 100644 src/converter/hestiacp-php-set-ver.go diff --git a/Makefile b/Makefile index 7e1c5da..4d79416 100644 --- a/Makefile +++ b/Makefile @@ -15,18 +15,19 @@ help: install: cd _build && \ install -d $(BIN_INSTALL_DIR) && \ - install -m 755 hestiacp-php-selector $(BIN_INSTALL_DIR) + install -m 755 hestiacp-php-selector $(BIN_INSTALL_DIR) && \ + install -m 755 hestiacp-php-admin $(BIN_INSTALL_DIR) targz: mkdir $(PKG_NAME)-$(VERSION) && \ git rev-parse --abbrev-ref HEAD > $(PKG_NAME)-$(VERSION)/version - cp -r LICENSE src Makefile *.spec go.mod.prepare.sh go.* $(PKG_NAME)-$(VERSION)/ && \ + cp -r LICENSE src Makefile *.spec go.mod.prepare.sh go.* installer.sh $(PKG_NAME)-$(VERSION)/ && \ tar zcvf $(PKG_NAME)-$(VERSION).tar.gz $(PKG_NAME)-$(VERSION) && \ rm -rf $(PKG_NAME)-$(VERSION) targzvendor: mkdir $(PKG_NAME)-$(VERSION) && \ - cp -r LICENSE src Makefile *.spec go.mod.prepare.sh go.* $(PKG_NAME)-$(VERSION)/ && \ + cp -r LICENSE src Makefile *.spec go.mod.prepare.sh go.* installer.sh $(PKG_NAME)-$(VERSION)/ && \ pushd $(PKG_NAME)-$(VERSION) && \ $(GOENV) go mod vendor && \ popd && \ @@ -44,7 +45,9 @@ build: cd _src && \ { [ -e vendor ] || $(GOENV) go mod tidy; } && \ { [ -e vendor ] || $(GOENV) go build -buildvcs=false $(GOLDFLAGS) -o ../_build/hestiacp-php-selector src/main/*.go; } && \ - { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go build -mod=vendor -buildvcs=false $(GOLDFLAGS) -o ../_build/hestiacp-php-selector src/main/*.go; } + { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go build -mod=vendor -buildvcs=false $(GOLDFLAGS) -o ../_build/hestiacp-php-selector src/main/*.go; } && \ + { [ -e vendor ] || $(GOENV) go build -buildvcs=false $(GOLDFLAGS) -o ../_build/hestiacp-php-admin src/converter/*.go; } && \ + { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go build -mod=vendor -buildvcs=false $(GOLDFLAGS) -o ../_build/hestiacp-php-admin src/converter/*.go; } check: { [ ! -e _src ] || rm -rf _src; } && \ @@ -54,7 +57,9 @@ check: cd _src && \ { [ -e vendor ] || $(GOENV) go mod tidy; } && \ { [ -e vendor ] || $(GOENV) go test src/main/*.go; } && \ - { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go test -mod=vendor -buildvcs=false src/main/*.go; } + { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go test -mod=vendor -buildvcs=false src/main/*.go; } && \ + { [ -e vendor ] || $(GOENV) go test src/converter/*.go; } && \ + { [ ! -e vendor ] || GO111MODULE=on GOPROXY=off go test -mod=vendor -buildvcs=false src/converter/*.go; } all: help diff --git a/go.mod b/go.mod index cee635c..adef91f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module dev.brepo.ru/brepo/hestiacp-php-selector go 1.22.6 + +require ( + github.com/akamensky/argparse v1.4.0 + github.com/gofrs/flock v0.12.1 +) + +require golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b07af4e --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= +github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/hestiacp-php-selector.spec b/hestiacp-php-selector.spec index 5197a2b..463c36b 100644 --- a/hestiacp-php-selector.spec +++ b/hestiacp-php-selector.spec @@ -25,6 +25,10 @@ make build %install [ "$RPM_BUILD_ROOT" != "/" ] && rm -rf "$RPM_BUILD_ROOT" make DESTDIR=$RPM_BUILD_ROOT install +%{__mkdir} -p "$RPM_BUILD_ROOT"%{_sysconfdir}/hestia_php_selector/system/ +touch "$RPM_BUILD_ROOT"%{_sysconfdir}/hestia_php_selector/system/lock +%{__mkdir} -p "$RPM_BUILD_ROOT"/usr/local/hestia_php_selector/ +%{__install} -D -m 755 installer.sh "$RPM_BUILD_ROOT"/usr/local/hestia_php_selector/hestiacp_php_selector_installer %check make check @@ -32,10 +36,24 @@ make check %clean [ "$RPM_BUILD_ROOT" != "/" ] && rm -rf "$RPM_BUILD_ROOT" +%posttrans +if [ $1 -eq 1 ]; then + /usr/local/hestia_php_selector/hestiacp_php_selector_installer install +fi + +%postun +if [ $1 -eq 0 ]; then + /usr/local/hestia_php_selector/hestiacp_php_selector_installer delete +fi + + %files %defattr(-,root,root) %doc LICENSE %attr(0755, root, root) %{_bindir}/hestiacp-php-selector +%attr(0755, root, root) %{_bindir}/hestiacp-php-admin +%{_sysconfdir}/hestia_php_selector/* +/usr/local/hestia_php_selector/* %changelog * Wed Aug 21 2024 Alexey Berezhok 0.1.0-1 diff --git a/installer.sh b/installer.sh index bc2f211..6e42d5c 100644 --- a/installer.sh +++ b/installer.sh @@ -1,8 +1,34 @@ #!/usr/bin/env bash -echo "Try to find php-selector for hestiacp" -update-alternatives --display php | grep hestiacp-php-selector -if [ $? -ne 0 ]; then - echo "Register php-selector" - update-alternatives --install /usr/bin/php php /usr/bin/hestiacp-php-selector 1 -fi \ No newline at end of file +uid=$(id -u) +if [ "$uid" != "0" ]; then + echo "Command must be executed as privileged user" + exit 0 +fi +case "$1" in +install) + echo "Try to find php-selector for hestiacp" + update-alternatives --display php | grep hestiacp-php-selector + if [ $? -ne 0 ]; then + echo "Register php-selector" + update-alternatives --install /usr/bin/php php /usr/bin/hestiacp-php-selector 1 + if [ ! -e /etc/hestia_php_selector/system/php.path ]; then + mkdir -p /etc/hestia_php_selector/system/ + current_path_to_php=$(readlink -f /usr/bin/php) + echo "$current_path_to_php" > /etc/hestia_php_selector/system/php.path + chmod 644 /etc/hestia_php_selector/system/php.path + fi + /usr/bin/hestiacp-php-admin add + fi +;; +delete) + php_sys=$(cat /etc/hestia_php_selector/system/php.path) + if [ -n "$php_sys" ]; then + update-alternatives --set php "$php_sys" + fi + /usr/bin/hestiacp-php-admin remove-all +;; +*) + echo "Unknown command" +;; +esac \ No newline at end of file diff --git a/src/converter/hestiacp-php-set-ver.go b/src/converter/hestiacp-php-set-ver.go new file mode 100644 index 0000000..b12fdc9 --- /dev/null +++ b/src/converter/hestiacp-php-set-ver.go @@ -0,0 +1,318 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/user" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/akamensky/argparse" + flk "github.com/gofrs/flock" +) + +const ( + PATH_TO_CONFIG_NAME = "/php.path" + PATH_TO_USER_CONFIG_ROOT = "/etc/hestia_php_selector/%s" + PATH_TO_USER_CONFIG = "/usr/local/hestia/data/users/*/user.conf" + PATH_TO_USER_CONFIG_FMT = "/usr/local/hestia/data/users/%s/user.conf" + PATH_TO_USER_CONFIG_FULL = PATH_TO_USER_CONFIG_ROOT + PATH_TO_CONFIG_NAME + PATH_TO_LOCAL_PHP_ROOT = "php_sel" + PATH_TO_LOCAL_PHP = PATH_TO_LOCAL_PHP_ROOT + PATH_TO_CONFIG_NAME + LOCK_PATH = "lock" +) + +func escapeUserName(username string) string { + username0 := strings.TrimSpace(username) + uname := strings.Split(username0, "/") + uname2 := uname[len(uname)-1] + if uname2 == "." || uname2 == ".." { + return "" + } + return uname2 +} + +func isExecOther(mode os.FileMode) bool { + return mode&0001 != 0 +} + +func getPHPVerFromConf(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("user config reading error %s", err) + } + defer f.Close() + scanner := bufio.NewScanner(f) + + for scanner.Scan() { + line := scanner.Text() + formatedLine := strings.SplitN(strings.TrimSpace(line), "=", 2) + if len(formatedLine) < 2 { + return "", fmt.Errorf("incorrect string formatting in config %s", line) + } + if strings.TrimSpace(formatedLine[0]) == "PHPCLI" { + result := strings.Trim(formatedLine[1], "' ") + return result, nil + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("user config reading error %s", err) + } + + return "", nil +} + +func getUserPHPVerHestia(username string) (string, string, error) { + pathToConf := fmt.Sprintf(PATH_TO_USER_CONFIG_FMT, username) + phpVer, err := getPHPVerFromConf(pathToConf) + if err != nil { + return "", "", err + } + if phpVer == "" { + return "", "", nil + } + phpPath := "/usr/bin/php" + phpVer + if finfo, err := os.Stat(phpPath); err != nil { + return "", "", err + } else { + fmode := finfo.Mode() + if !isExecOther(fmode) { + return "", "", fmt.Errorf("not executable by others %s", phpPath) + } + if strings.Contains(finfo.Name(), "hestiacp-php-selector") { + return "", "", fmt.Errorf("infinite symbolic link") + } + } + return phpPath, phpVer, nil +} + +func setUserPHPVer(username string, php_ver string) error { + reIsVer := regexp.MustCompile(`^(\d\.?\d+)`) + matches := reIsVer.FindStringSubmatch(php_ver) + if len(matches) > 1 { + phpver := strings.TrimSpace(matches[1]) + phpPath := "/usr/bin/php" + phpver + if finfo, err := os.Stat(phpPath); err != nil { + return err + } else { + fmode := finfo.Mode() + if !isExecOther(fmode) { + return fmt.Errorf("not executable by others %s", phpPath) + } + if strings.Contains(finfo.Name(), "hestiacp-php-selector") { + return fmt.Errorf("infinite symbolic link") + } + } + return setUserPhpPathVer(username, phpver, phpPath) + } else { + reIsVer := regexp.MustCompile(`(\d\.?\d+)$`) + matches := reIsVer.FindStringSubmatch(php_ver) + vers := "" + if len(matches) > 1 { + vers = strings.TrimSpace(matches[1]) + } + return setUserPhpPathVer(username, vers, php_ver) + } +} + +func isFileExists(path string) (bool, bool, error) { + if fi, err := os.Stat(path); err == nil { + return true, fi.IsDir(), nil + } else if errors.Is(err, os.ErrNotExist) { + return false, false, nil + } else { + return false, false, err + } +} + +func setUserPhpPathVer(username string, php_ver string, php_path string) error { + un := escapeUserName(username) + if un == "" { + return fmt.Errorf("incorrect username %s", username) + } + var root_path string + var uid int = -1 + if un == "root" { + root_path = fmt.Sprintf(PATH_TO_USER_CONFIG_ROOT, "system") + } else { + user_found, err := user.Lookup(un) + if err != nil { + return err + } + root_path = path.Join(user_found.HomeDir, PATH_TO_LOCAL_PHP_ROOT) + Uid_conv, _ := strconv.ParseInt(user_found.Uid, 10, 64) + uid = int(Uid_conv) + } + + exists, isdir, err := isFileExists(root_path) + if err != nil { + return err + } + if exists { + if !isdir { + err := os.RemoveAll(root_path) + if err != nil { + return err + } + err = os.Mkdir(root_path, 0755) + if err != nil { + return err + } + } + } else { + err := os.Mkdir(root_path, 0755) + if err != nil { + return err + } + } + path_to_php_config := path.Join(root_path, PATH_TO_CONFIG_NAME) + f, err := os.OpenFile(path_to_php_config, os.O_WRONLY, 0400) + if err != nil { + return err + } + f.WriteString(php_path) + f.Close() + return os.Chown(path_to_php_config, uid, -1) +} + +func removeUserInfo(username string) error { + un := escapeUserName(username) + if un == "" { + return nil + } + if un == "root" { + return nil + } + user_found, err := user.Lookup(un) + if err != nil { + return err + } + root_path := path.Join(user_found.HomeDir, PATH_TO_LOCAL_PHP_ROOT) + return os.RemoveAll(root_path) +} + +func inner_main() int { + parser := argparse.NewParser("php-selector", "Set version php cli for user") + vConvertExisting := parser.NewCommand("add", "Add existing users to php-selector") + vSetPhp := parser.NewCommand("set", "Set php for user: set username 74 or set username /usr/bin/php74") + vSetPhp_username := vSetPhp.StringPositional(&argparse.Options{Required: true, Help: "username"}) + vSetPhp_ver := vSetPhp.StringPositional(&argparse.Options{Required: true, Help: "path to php or php version"}) + vSetSystem := parser.NewCommand("system", "Set system php version") + vSetSystem_ver := vSetSystem.StringPositional(&argparse.Options{Required: true, Help: "path to php or php version"}) + vRemoveUser := parser.NewCommand("remove", "remove php info for user (reset to system)") + vRemoveUser_username := vRemoveUser.StringPositional(&argparse.Options{Required: true, Help: "username to delete"}) + vRemoveUserAll := parser.NewCommand("remove-all", "remove php info for all users") + err := parser.Parse(os.Args) + if err != nil { + fmt.Fprintln(os.Stderr, parser.Usage(err)) + return 1 + } + + lock_file := path.Join(fmt.Sprintf(PATH_TO_USER_CONFIG_ROOT, "system"), LOCK_PATH) + lock := flk.New(lock_file) + err = lock.Lock() + if err != nil { + fmt.Fprintf(os.Stderr, "Error locking %s\n", err) + return 1 + } + defer lock.Close() + + if vConvertExisting.Happened() { + config_path, err := filepath.Glob(PATH_TO_USER_CONFIG) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading of hestia configs %s\n", err) + return 1 + } + for _, v := range config_path { + reUserName := regexp.MustCompile(`^/usr/local/hestia/data/users/(.+)/user.conf`) + matches := reUserName.FindStringSubmatch(v) + if len(matches) > 1 { + uName := strings.TrimSpace(matches[1]) + php_path, php_ver, err := getUserPHPVerHestia(uName) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue + } + err = setUserPhpPathVer(uName, php_ver, php_path) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue + } + } + } + + } else if vSetPhp.Happened() { + username := strings.TrimSpace(*vSetPhp_username) + php_ver := strings.TrimSpace(*vSetPhp_ver) + if username == "" || php_ver == "" { + fmt.Fprintln(os.Stderr, "Username or php version shouldn't be empty") + return 1 + } + err := setUserPHPVer(username, php_ver) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + } else if vSetSystem.Happened() { + php_ver := strings.TrimSpace(*vSetSystem_ver) + if php_ver == "" { + fmt.Fprintln(os.Stderr, "Username or php version shouldn't be empty") + return 1 + } + err := setUserPHPVer("root", php_ver) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + } else if vRemoveUser.Happened() { + username := strings.TrimSpace(*vRemoveUser_username) + if username == "" { + fmt.Fprintln(os.Stderr, "Username shouldn't be empty") + return 1 + } + err := removeUserInfo(username) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + } else if vRemoveUserAll.Happened() { + config_path, err := filepath.Glob(PATH_TO_USER_CONFIG) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading of hestia configs %s\n", err) + return 1 + } + for _, v := range config_path { + reUserName := regexp.MustCompile(`^/usr/local/hestia/data/users/(.+)/user.conf`) + matches := reUserName.FindStringSubmatch(v) + if len(matches) > 1 { + uName := strings.TrimSpace(matches[1]) + err := removeUserInfo(uName) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + } + } + + } + return 0 +} + +func main() { + user, err := user.Current() + if err != nil { + fmt.Fprintf(os.Stderr, "User information error %s\n", err) + os.Exit(1) + } + if user.Username != "root" { + fmt.Fprintln(os.Stderr, "Program should start under privileged user") + os.Exit(1) + } + os.Exit(inner_main()) +} diff --git a/src/main/phpselector.go b/src/main/phpselector.go index 9ab8f67..2ad5c22 100644 --- a/src/main/phpselector.go +++ b/src/main/phpselector.go @@ -1,47 +1,40 @@ package main import ( - "bufio" "fmt" "os" "os/user" + "path" "strings" "syscall" ) const ( - PATH_TO_USER_CONFIG = "/usr/local/hestia/data/users/%s/user.conf" + PATH_TO_USER_CONFIG_FULL = "/etc/hestia_php_selector/%s/php.path" + PATH_TO_LOCAL_PHP = "php_sel/php.path" ) func isExecOther(mode os.FileMode) bool { return mode&0001 != 0 } -func getPHPVerFromConf(path string) (string, error) { - f, err := os.Open(path) +func getPHPVerFromConf(path1 string) (string, error) { + _, err := os.Stat(path1) if err != nil { - return "", fmt.Errorf("user config reading error %s", err) - } - defer f.Close() - scanner := bufio.NewScanner(f) - - for scanner.Scan() { - line := scanner.Text() - formatedLine := strings.SplitN(strings.TrimSpace(line), "=", 2) - if len(formatedLine) < 2 { - return "", fmt.Errorf("incorrect string formatting in config %s", line) - } - if strings.TrimSpace(formatedLine[0]) == "PHPCLI" { - result := strings.Trim(formatedLine[1], "' ") - return result, nil + sys_path := fmt.Sprintf(PATH_TO_USER_CONFIG_FULL, "system") + if path1 == sys_path { + return "", fmt.Errorf("user config reading error %s", err) + } else { + return getPHPVerFromConf(sys_path) } } - - if err := scanner.Err(); err != nil { + content, err := os.ReadFile(path1) + if err != nil { return "", fmt.Errorf("user config reading error %s", err) } + result := strings.Split(strings.Trim(string(content), "\n "), "\n")[0] + return result, nil - return "", nil } func getUserPHPVer() (string, error) { @@ -49,24 +42,32 @@ func getUserPHPVer() (string, error) { if err != nil { return "", fmt.Errorf("user information error %s", err) } - pathToConf := fmt.Sprintf(PATH_TO_USER_CONFIG, user.Username) + username := user.Username + if username == "root" { + username = "system" + } + var pathToConf string + if username == "system" { + pathToConf = fmt.Sprintf(PATH_TO_USER_CONFIG_FULL, username) + } else { + pathToConf = path.Join(user.HomeDir, PATH_TO_LOCAL_PHP) + } phpVer, err := getPHPVerFromConf(pathToConf) if err != nil { return "", err } - phpPath := "/usr/bin/php" + phpVer - if finfo, err := os.Stat(phpPath); err != nil { + if finfo, err := os.Stat(phpVer); err != nil { return "", err } else { fmode := finfo.Mode() if !isExecOther(fmode) { - return "", fmt.Errorf("not executable by others %s", phpPath) + return "", fmt.Errorf("not executable by others %s", phpVer) } if strings.Contains(finfo.Name(), "hestiacp-php-selector") { return "", fmt.Errorf("infinite symbolic link") } } - return phpPath, nil + return phpVer, nil } func inner_main() int {