package shares import ( "errors" "fmt" "net" "path" "regexp" "strings" ) var ( reShareName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$`) reWorkgroup = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$`) reUser = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`) reHost = regexp.MustCompile(`^[A-Za-z0-9*][A-Za-z0-9*.-]{0,253}$`) reComment = regexp.MustCompile(`^[^\r\n\[\]]{0,120}$`) ) // reserved Samba section names that must not be used as share names. var reservedShares = map[string]bool{"global": true, "homes": true, "printers": true, "print$": true} func ValidateShareName(s string) error { if !reShareName.MatchString(s) { return errors.New("share name: 1-80 characters, letters, digits, _ . - and must start with a letter or digit") } if reservedShares[strings.ToLower(s)] { return fmt.Errorf("share name %q is reserved by Samba", s) } return nil } func ValidateWorkgroup(s string) error { if !reWorkgroup.MatchString(s) { return errors.New("workgroup: 1-15 characters, letters, digits, _ -") } return nil } func ValidateUserName(s string) error { if !reUser.MatchString(s) { return errors.New("user name: lowercase letters, digits, _ -, up to 32 characters") } if s == "root" || s == "nobody" { return fmt.Errorf("user %s cannot be an SMB user", s) } return nil } func ValidateComment(s string) error { if !reComment.MatchString(s) { return errors.New("comment: up to 120 characters, no [ ] or line breaks") } return nil } // systemRoots are OS trees that must never be shared. var systemRoots = []string{"/etc", "/proc", "/sys", "/dev", "/boot", "/run", "/usr", "/bin", "/sbin", "/lib", "/lib32", "/lib64", "/libx32", "/var", "/root", "/snap", "/lost+found"} // ValidateNotSystem refuses the root filesystem and system directories. func ValidateNotSystem(p string) error { if p == "/" { return errors.New("refusing to share the root filesystem") } if UnderRoots(p, systemRoots) { return fmt.Errorf("%s is a system directory and cannot be shared", p) } return nil } // ValidatePath accepts an absolute, clean, non-system path without control // characters. Existence and symlinks are checked by the caller. func ValidatePath(p string) error { if p == "" { return errors.New("path is required") } if !strings.HasPrefix(p, "/") || path.Clean(p) != p || strings.ContainsAny(p, "\"\\\r\n\t") { return errors.New("path must be absolute and normalised (no .., trailing / or quotes)") } return ValidateNotSystem(p) } // UnderRoots reports whether p equals or lies below one of the roots. func UnderRoots(p string, roots []string) bool { for _, r := range roots { if r == "" || r == "/" { continue } if p == r || strings.HasPrefix(p, r+"/") { return true } } return false } // ValidateClient accepts *, a host name (wildcards allowed), an IP or a CIDR. func ValidateClient(c string) error { if c == "*" { return nil } if strings.Contains(c, "/") { if _, _, err := net.ParseCIDR(c); err != nil { return fmt.Errorf("client %q: bad network, use e.g. 192.168.1.0/24", c) } return nil } if net.ParseIP(c) != nil || reHost.MatchString(c) { return nil } return fmt.Errorf("client %q: use a host name, IP address, network (CIDR) or *", c) } func (s SMBShare) Validate() error { if err := ValidateShareName(s.Name); err != nil { return err } if err := ValidatePath(s.Path); err != nil { return err } if err := ValidateComment(s.Comment); err != nil { return err } if !s.Guest && len(s.Users) == 0 { return fmt.Errorf("share %s: allow guests or pick at least one user", s.Name) } for _, u := range s.Users { if err := ValidateUserName(u); err != nil { return err } } return nil } func (e NFSExport) Validate() error { if err := ValidatePath(e.Path); err != nil { return err } if len(e.Clients) == 0 { return fmt.Errorf("export %s: at least one client is required (or *)", e.Path) } for _, c := range e.Clients { if err := ValidateClient(c); err != nil { return err } } switch e.Squash { case "root", "all", "none": default: return errors.New("squash must be root, all or none") } return nil } // SplitList turns "a, b;c\nd" into ["a","b","c","d"]. func SplitList(s string) []string { var out []string for _, f := range strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == ';' || r == '\n' || r == ' ' || r == '\t' || r == '\r' }) { if f = strings.TrimSpace(f); f != "" { out = append(out, f) } } return out }