package internal import ( "errors" "fmt" "io/fs" "os" "os/user" "path/filepath" "strconv" "strings" "syscall" "code.gitea.io/sdk/gitea" "github.com/spf13/viper" ) var ( ErrorPIDInUse = errors.New("daemon is already running") ErrLockFailed = errors.New("failed to acquire a lock on the PID file") ErrBlankCert = errors.New("cert is blank") ) type Domain struct { name *string config *viper.Viper description *string gtClient *gitea.Client } // 0x01 func createPIDFile() { file, err := os.Create("/var/run/certman.pid") if err != nil { fmt.Printf("0x01: Error creating PID file: %v\n", err) return } err = file.Close() if err != nil { fmt.Printf("0x01: Error closing PID file: %v\n", err) return } } // 0x02 func ClearPIDFile() { file, err := os.OpenFile("/var/run/certman.pid", os.O_RDWR|os.O_CREATE, 0644) if err != nil { fmt.Printf("0x02: Error opening PID file: %v\n", err) return } defer file.Close() err = file.Truncate(0) if err != nil { fmt.Printf("0x02: Error writing PID file: %v\n", err) return } } // 0x03 func CreateOrUpdatePIDFile(filename string) error { pidBytes, err := os.ReadFile(filename) if err != nil { fmt.Printf("0x03: Error reading PID file: %v\n", err) return err } pidStr := strings.TrimSpace(string(pidBytes)) isPidFileEmpty := pidStr == "" if !isPidFileEmpty { pid, err := strconv.Atoi(pidStr) if err != nil { fmt.Printf("0x03: Error parsing PID file: %v\n", err) return err } isProcActive, err := isProcessActive(pid) if err != nil { fmt.Printf("0x03: Error checking if process is active: %v\n", err) return err } if isProcActive { return ErrorPIDInUse } } pidFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) if err != nil { if os.IsNotExist(err) { createPIDFile() } else { fmt.Printf("0x03: Error opening PID file: %v\n", err) return err } } defer pidFile.Close() if err := syscall.Flock(int(pidFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { if errors.Is(err, syscall.EWOULDBLOCK) { return ErrLockFailed } return fmt.Errorf("error locking PID file: %w", err) } curPid := os.Getpid() if _, err := pidFile.Write([]byte(strconv.Itoa(curPid))); err != nil { return fmt.Errorf("error writing pid to PID file: %w", err) } return nil } // 0x04 // isProcessActive checks whether the process with the provided PID is running. func isProcessActive(pid int) (bool, error) { if pid <= 0 { return false, errors.New("invalid process ID") } process, err := os.FindProcess(pid) if err != nil { // On Unix systems, os.FindProcess always succeeds and returns a process with the given pid, irrespective of whether the process exists. return false, nil } err = process.Signal(syscall.Signal(0)) if err != nil { if errors.Is(err, syscall.ESRCH) { // The process does not exist return false, nil } else if errors.Is(err, os.ErrProcessDone) { return false, nil } // Some other unexpected error return false, err } // The process exists and is active return true, nil } // 0x05 func DaemonProcess() (*os.Process, error) { pidBytes, err := os.ReadFile("/var/run/certman.pid") if err != nil { fmt.Printf("0x05: Error getting PID from /var/run/certman.pid: %v\n", err) return nil, err } pidStr := strings.TrimSpace(string(pidBytes)) daemonPid, err := strconv.Atoi(pidStr) if err != nil { fmt.Printf("0x05: Error converting PID string to int (%s): %v\n", pidStr, err) return nil, err } isProcActive, err := isProcessActive(daemonPid) if err != nil { fmt.Printf("0x05: Error checking if process is active: %v\n", err) } if !isProcActive { return nil, errors.New("process is not active") } proc, err := os.FindProcess(daemonPid) if err != nil { fmt.Printf("0x05: Error finding process with PID %d: %v\n", daemonPid, err) return nil, err } return proc, nil } func createFile(fileName string, filePermission os.FileMode, data []byte) { fileInfo, err := os.Stat(fileName) if err != nil { if os.IsNotExist(err) { file, err := os.Create(fileName) if err != nil { fmt.Println("Error creating configuration file: ", err) os.Exit(1) } _, err = file.Write(data) if err != nil { fmt.Println("Error writing to file: ", err) os.Exit(1) } err = file.Chmod(filePermission) if err != nil { fmt.Println("Error changing file permission: ", err) } } else { fmt.Println("Error opening configuration file: ", err) os.Exit(1) } } else { if fileInfo.Size() == 0 { file, err := os.Create(fileName) if err != nil { fmt.Println("Error creating configuration file: ", err) os.Exit(1) } _, err = file.Write(data) if err != nil { fmt.Println("Error writing to file:", err) os.Exit(1) } } } } func LinkFile(source, target, domain, extension string) error { if target == "" { return ErrBlankCert } linkInfo, err := os.Stat(target) if err != nil { if os.IsNotExist(err) { err = os.Symlink(source, target) if err != nil { return err } return nil } return err } if linkInfo.IsDir() { target = filepath.Join(target, domain+extension) err = os.Symlink(source, target) if err != nil { return err } } return nil } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func contains(slice []string, value string) (sliceHas bool, index int) { for i, entry := range slice { if entry == value { return true, i } } return false, -1 } func insert(a []string, index int, value string) []string { last := len(a) - 1 a = append(a, a[last]) copy(a[index+1:], a[index:last]) a[index] = value return a } func SanitizeDomainKey(s string) string { s = strings.TrimSpace(strings.ToLower(s)) r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_") return r.Replace(s) } // DomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors func DomainCertsDir(domain string) (string, error) { domainConfig, exists := domainStore.Get(domain) if !exists { return "", ErrConfigNotFound } return DomainCertsDirWConf(domain, domainConfig) } // DomainCertsDirWConf Can return ErrBlankConfigEntry or other errors func DomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) { effectiveDataRoot, err := EffectiveString(domainConfig, "Certificates.data_root") if err != nil { return "", err } return filepath.Join(effectiveDataRoot, "certificates", domain), nil } func DomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) { domain := domainConfig.GetString("Domain.domain_name") return DomainCertsDirWConf(domain, domainConfig) } func ChownRecursive(path string, uid, gid int) error { return filepath.WalkDir(path, func(name string, d fs.DirEntry, err error) error { if err != nil { return err // Stop if we encounter a permission error on a specific file } // Apply ownership change to the current item return os.Chown(name, uid, gid) }) } func LookupGID(group string) (int, error) { g, err := user.LookupGroup(group) if err != nil { return 0, err } return strconv.Atoi(g.Gid) } // MakeCredential resolves username/groupname to uid/gid for syscall.Credential. // Note: actually *using* different credentials typically requires the server // process to have appropriate privileges (often root). func MakeCredential(username, groupname string) (*syscall.Credential, error) { var uid, gid uint32 var haveUID, haveGID bool if username != "" { u, err := user.Lookup(username) if err != nil { return nil, fmt.Errorf("unknown user") } parsed, err := strconv.ParseUint(u.Uid, 10, 32) if err != nil { return nil, fmt.Errorf("bad uid") } uid = uint32(parsed) haveUID = true // If group not explicitly provided, default to user's primary group. if groupname == "" && u.Gid != "" { parsedG, err := strconv.ParseUint(u.Gid, 10, 32) if err == nil { gid = uint32(parsedG) haveGID = true } } } if groupname != "" { g, err := user.LookupGroup(groupname) if err != nil { return nil, fmt.Errorf("unknown group") } parsed, err := strconv.ParseUint(g.Gid, 10, 32) if err != nil { return nil, fmt.Errorf("bad gid") } gid = uint32(parsed) haveGID = true } // If only group was provided, keep current uid. if !haveUID { uid = uint32(os.Getuid()) } if !haveGID { gid = uint32(os.Getgid()) } return &syscall.Credential{Uid: uid, Gid: gid}, nil }