Go service (nasmon/) plus nfpm packaging (packaging/), carried over from the home_nas monorepo. Adds rpm packaging alongside the existing deb build (Fedora/RHEL/Rocky dependency names via nfpm overrides, per-format systemd unit path, dpkg+rpm-compatible maintainer scripts) and a Gitea Actions workflow that builds both formats on every push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
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
|
|
}
|
|
|
|
// ValidatePath accepts an absolute, clean path without control characters.
|
|
// Whether it lies under a pool is checked by the caller (needs live state).
|
|
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)")
|
|
}
|
|
if p == "/" {
|
|
return errors.New("refusing to share the root filesystem")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|