mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-28 04:35:40 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			363 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			363 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package utils
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"regexp"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 	"tinyauth/internal/constants"
 | |
| 	"tinyauth/internal/types"
 | |
| 
 | |
| 	"github.com/google/uuid"
 | |
| 	"github.com/rs/zerolog/log"
 | |
| )
 | |
| 
 | |
| // Parses a list of comma separated users in a struct
 | |
| func ParseUsers(users string) (types.Users, error) {
 | |
| 	log.Debug().Msg("Parsing users")
 | |
| 
 | |
| 	// Create a new users struct
 | |
| 	var usersParsed types.Users
 | |
| 
 | |
| 	// Split the users by comma
 | |
| 	userList := strings.Split(users, ",")
 | |
| 
 | |
| 	// Check if there are any users
 | |
| 	if len(userList) == 0 {
 | |
| 		return types.Users{}, errors.New("invalid user format")
 | |
| 	}
 | |
| 
 | |
| 	// Loop through the users and split them by colon
 | |
| 	for _, user := range userList {
 | |
| 		parsed, err := ParseUser(user)
 | |
| 
 | |
| 		// Check if there was an error
 | |
| 		if err != nil {
 | |
| 			return types.Users{}, err
 | |
| 		}
 | |
| 
 | |
| 		// Append the user to the users struct
 | |
| 		usersParsed = append(usersParsed, parsed)
 | |
| 	}
 | |
| 
 | |
| 	log.Debug().Msg("Parsed users")
 | |
| 
 | |
| 	// Return the users struct
 | |
| 	return usersParsed, nil
 | |
| }
 | |
| 
 | |
| // Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
 | |
| func GetUpperDomain(urlSrc string) (string, error) {
 | |
| 	// Make sure the url is valid
 | |
| 	urlParsed, err := url.Parse(urlSrc)
 | |
| 
 | |
| 	// Check if there was an error
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Split the hostname by period
 | |
| 	urlSplitted := strings.Split(urlParsed.Hostname(), ".")
 | |
| 
 | |
| 	// Get the last part of the url
 | |
| 	urlFinal := strings.Join(urlSplitted[1:], ".")
 | |
| 
 | |
| 	// Return the root domain
 | |
| 	return urlFinal, nil
 | |
| }
 | |
| 
 | |
| // Reads a file and returns the contents
 | |
| func ReadFile(file string) (string, error) {
 | |
| 	// Check if the file exists
 | |
| 	_, err := os.Stat(file)
 | |
| 
 | |
| 	// Check if there was an error
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Read the file
 | |
| 	data, err := os.ReadFile(file)
 | |
| 
 | |
| 	// Check if there was an error
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Return the file contents
 | |
| 	return string(data), nil
 | |
| }
 | |
| 
 | |
| // Parses a file into a comma separated list of users
 | |
| func ParseFileToLine(content string) string {
 | |
| 	// Split the content by newline
 | |
| 	lines := strings.Split(content, "\n")
 | |
| 
 | |
| 	// Create a list of users
 | |
| 	users := make([]string, 0)
 | |
| 
 | |
| 	// Loop through the lines, trimming the whitespace and appending to the users list
 | |
| 	for _, line := range lines {
 | |
| 		if strings.TrimSpace(line) == "" {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		users = append(users, strings.TrimSpace(line))
 | |
| 	}
 | |
| 
 | |
| 	// Return the users as a comma separated string
 | |
| 	return strings.Join(users, ",")
 | |
| }
 | |
| 
 | |
| // Get the secret from the config or file
 | |
| func GetSecret(conf string, file string) string {
 | |
| 	// If neither the config or file is set, return an empty string
 | |
| 	if conf == "" && file == "" {
 | |
| 		return ""
 | |
| 	}
 | |
| 
 | |
| 	// If the config is set, return the config (environment variable)
 | |
| 	if conf != "" {
 | |
| 		return conf
 | |
| 	}
 | |
| 
 | |
| 	// If the file is set, read the file
 | |
| 	contents, err := ReadFile(file)
 | |
| 
 | |
| 	// Check if there was an error
 | |
| 	if err != nil {
 | |
| 		return ""
 | |
| 	}
 | |
| 
 | |
| 	// Return the contents of the file
 | |
| 	return ParseSecretFile(contents)
 | |
| }
 | |
| 
 | |
| // Get the users from the config or file
 | |
| func GetUsers(conf string, file string) (types.Users, error) {
 | |
| 	// Create a string to store the users
 | |
| 	var users string
 | |
| 
 | |
| 	// If neither the config or file is set, return an empty users struct
 | |
| 	if conf == "" && file == "" {
 | |
| 		return types.Users{}, nil
 | |
| 	}
 | |
| 
 | |
| 	// If the config (environment) is set, append the users to the users string
 | |
| 	if conf != "" {
 | |
| 		log.Debug().Msg("Using users from config")
 | |
| 		users += conf
 | |
| 	}
 | |
| 
 | |
| 	// If the file is set, read the file and append the users to the users string
 | |
| 	if file != "" {
 | |
| 		// Read the file
 | |
| 		contents, err := ReadFile(file)
 | |
| 
 | |
| 		// If there isn't an error we can append the users to the users string
 | |
| 		if err == nil {
 | |
| 			log.Debug().Msg("Using users from file")
 | |
| 
 | |
| 			// Append the users to the users string
 | |
| 			if users != "" {
 | |
| 				users += ","
 | |
| 			}
 | |
| 
 | |
| 			// Parse the file contents into a comma separated list of users
 | |
| 			users += ParseFileToLine(contents)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Return the parsed users
 | |
| 	return ParseUsers(users)
 | |
| }
 | |
| 
 | |
| // Parse the docker labels to the tinyauth labels struct
 | |
| func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
 | |
| 	// Create a new tinyauth labels struct
 | |
| 	var tinyauthLabels types.TinyauthLabels
 | |
| 
 | |
| 	// Loop through the labels
 | |
| 	for label, value := range labels {
 | |
| 
 | |
| 		// Check if the label is in the tinyauth labels
 | |
| 		if slices.Contains(constants.TinyauthLabels, label) {
 | |
| 
 | |
| 			log.Debug().Str("label", label).Msg("Found label")
 | |
| 
 | |
| 			// Add the label value to the tinyauth labels struct
 | |
| 			switch label {
 | |
| 			case "tinyauth.oauth.whitelist":
 | |
| 				tinyauthLabels.OAuthWhitelist = value
 | |
| 			case "tinyauth.users":
 | |
| 				tinyauthLabels.Users = value
 | |
| 			case "tinyauth.allowed":
 | |
| 				tinyauthLabels.Allowed = value
 | |
| 			case "tinyauth.headers":
 | |
| 				tinyauthLabels.Headers = make(map[string]string)
 | |
| 				headers := strings.Split(value, ",")
 | |
| 				for _, header := range headers {
 | |
| 					headerSplit := strings.Split(header, "=")
 | |
| 					if len(headerSplit) != 2 {
 | |
| 						continue
 | |
| 					}
 | |
| 					tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
 | |
| 				}
 | |
| 			case "tinyauth.oauth.groups":
 | |
| 				tinyauthLabels.OAuthGroups = value
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Return the tinyauth labels
 | |
| 	return tinyauthLabels
 | |
| }
 | |
| 
 | |
| // Check if any of the OAuth providers are configured based on the client id and secret
 | |
| func OAuthConfigured(config types.Config) bool {
 | |
| 	return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
 | |
| }
 | |
| 
 | |
| // Filter helper function
 | |
| func Filter[T any](slice []T, test func(T) bool) (res []T) {
 | |
| 	for _, value := range slice {
 | |
| 		if test(value) {
 | |
| 			res = append(res, value)
 | |
| 		}
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| // Parse user
 | |
| func ParseUser(user string) (types.User, error) {
 | |
| 	// Check if the user is escaped
 | |
| 	if strings.Contains(user, "$$") {
 | |
| 		user = strings.ReplaceAll(user, "$$", "$")
 | |
| 	}
 | |
| 
 | |
| 	// Split the user by colon
 | |
| 	userSplit := strings.Split(user, ":")
 | |
| 
 | |
| 	// Check if the user is in the correct format
 | |
| 	if len(userSplit) < 2 || len(userSplit) > 3 {
 | |
| 		return types.User{}, errors.New("invalid user format")
 | |
| 	}
 | |
| 
 | |
| 	// Check for empty strings
 | |
| 	for _, userPart := range userSplit {
 | |
| 		if strings.TrimSpace(userPart) == "" {
 | |
| 			return types.User{}, errors.New("invalid user format")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Check if the user has a totp secret
 | |
| 	if len(userSplit) == 2 {
 | |
| 		return types.User{
 | |
| 			Username: userSplit[0],
 | |
| 			Password: userSplit[1],
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Return the user struct
 | |
| 	return types.User{
 | |
| 		Username:   userSplit[0],
 | |
| 		Password:   userSplit[1],
 | |
| 		TotpSecret: userSplit[2],
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // Parse secret file
 | |
| func ParseSecretFile(contents string) string {
 | |
| 	// Split to lines
 | |
| 	lines := strings.Split(contents, "\n")
 | |
| 
 | |
| 	// Loop through the lines
 | |
| 	for _, line := range lines {
 | |
| 		// Check if the line is empty
 | |
| 		if strings.TrimSpace(line) == "" {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Return the line
 | |
| 		return strings.TrimSpace(line)
 | |
| 	}
 | |
| 
 | |
| 	// Return an empty string
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| // Check if a string matches a regex or a whitelist
 | |
| func CheckWhitelist(whitelist string, str string) bool {
 | |
| 	// Check if the whitelist is empty
 | |
| 	if len(strings.TrimSpace(whitelist)) == 0 {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// Check if the whitelist is a regex
 | |
| 	if strings.HasPrefix(whitelist, "/") && strings.HasSuffix(whitelist, "/") {
 | |
| 		// Create regex
 | |
| 		re, err := regexp.Compile(whitelist[1 : len(whitelist)-1])
 | |
| 
 | |
| 		// Check if there was an error
 | |
| 		if err != nil {
 | |
| 			log.Error().Err(err).Msg("Error compiling regex")
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// Check if the string matches the regex
 | |
| 		if re.MatchString(str) {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Split the whitelist by comma
 | |
| 	whitelistSplit := strings.Split(whitelist, ",")
 | |
| 
 | |
| 	// Loop through the whitelist
 | |
| 	for _, item := range whitelistSplit {
 | |
| 		// Check if the item matches with the string
 | |
| 		if strings.TrimSpace(item) == str {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Return false if no match was found
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Capitalize just the first letter of a string
 | |
| func Capitalize(str string) string {
 | |
| 	if len(str) == 0 {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:])
 | |
| }
 | |
| 
 | |
| // Sanitize header removes all control characters from a string
 | |
| func SanitizeHeader(header string) string {
 | |
| 	return strings.Map(func(r rune) rune {
 | |
| 		// Allow only printable ASCII characters (32-126) and safe whitespace (space, tab)
 | |
| 		if r == ' ' || r == '\t' || (r >= 32 && r <= 126) {
 | |
| 			return r
 | |
| 		}
 | |
| 		return -1
 | |
| 	}, header)
 | |
| }
 | |
| 
 | |
| // Generate a static identifier from a string
 | |
| func GenerateIdentifier(str string) string {
 | |
| 	// Create a new UUID
 | |
| 	uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str))
 | |
| 
 | |
| 	// Convert the UUID to a string
 | |
| 	uuidString := uuid.String()
 | |
| 
 | |
| 	// Show the UUID
 | |
| 	log.Debug().Str("uuid", uuidString).Msg("Generated UUID")
 | |
| 
 | |
| 	// Convert the UUID to a string
 | |
| 	return strings.Split(uuidString, "-")[0]
 | |
| }
 | 
