package common import ( "errors" "fmt" "io/fs" "os" "os/user" "path/filepath" "regexp" "strconv" "strings" "syscall" "code.gitea.io/sdk/gitea" ) 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 *AppConfig 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 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) } 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 } func CertsDir(config *AppConfig, domainConfig *DomainConfig) string { if config == nil { return "" } if domainConfig == nil { return "" } if domainConfig.Certificates.DataRoot == "" { if config.Certificates.DataRoot == "" { workDir, err := os.Getwd() if err != nil { return "./" } return workDir } return config.Certificates.DataRoot } return domainConfig.Certificates.DataRoot } var fqdnRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`) func IsValidFQDN(domain string) bool { return len(domain) <= 253 && fqdnRegex.MatchString(domain) }