package api
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/org_tangled"
)
var (
repoNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
)
func ValidateDID(d string) error {
_, err := syntax.ParseDID(d)
if err != nil {
return fmt.Errorf("invalid DID %q: %w", d, err)
}
return nil
}
func ValidateKnotDID(d string) error {
if err := ValidateDID(d); err != nil {
return err
}
if !strings.HasPrefix(d, "did:web:") {
return fmt.Errorf("knot DID must be a did:web, got %q", d)
}
return nil
}
func ValidateRepoName(raw string) error {
if len(raw) == 0 {
return errors.New("repository name cannot be empty")
}
if len(raw) > 100 {
return errors.New("repository name must be 100 characters or fewer")
}
if strings.Contains(raw, "/") || strings.Contains(raw, "\\") {
return errors.New("repository name contains invalid path characters")
}
if strings.HasPrefix(raw, ".") || strings.HasSuffix(raw, ".") {
return errors.New("repository name contains an invalid path sequence")
}
if strings.Contains(raw, "..") {
return errors.New("repository name cannot contain sequential dots")
}
if !repoNameRegex.MatchString(raw) {
return errors.New("repository name contains invalid characters")
}
if strings.EqualFold(raw, "self") {
return errors.New("repository name 'self' is reserved")
}
return nil
}
func ValidateSourceURL(urlStr string) error {
u, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("malformed URL: %w", err)
}
if u.Scheme != "https" {
return errors.New("source URL protocol must be https")
}
if u.Hostname() == "" {
return errors.New("source URL must name a host")
}
if u.User != nil {
return errors.New("credentials are not permitted in source URL")
}
if u.Port() != "" && u.Port() != "443" {
return errors.New("custom port not permitted in source URL")
}
if u.RawQuery != "" || u.Fragment != "" {
return errors.New("query and fragment parameters are not permitted in source URL")
}
if strings.Trim(u.Path, "/") == "" {
return errors.New("source URL must name a repository path")
}
return nil
}
func ValidateGitHubSourceURL(urlStr, githubHost string) error {
if err := ValidateSourceURL(urlStr); err != nil {
return err
}
u, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("malformed URL: %w", err)
}
if !strings.EqualFold(u.Hostname(), githubHost) {
return fmt.Errorf("a private source must be on %s, the only host a token is presented to", githubHost)
}
return nil
}
func ComputeRequestDigest(jobs []*org_tangled.TempMigratorDefs_NewJob) string {
var sb strings.Builder
for _, j := range jobs {
sb.WriteString(fmt.Sprintf("%s|%s|%s|%t|%s;",
j.Name,
j.KnotDid,
strings.TrimSuffix(strings.ToLower(j.SourceUrl), ".git"),
j.Private != nil && *j.Private,
DescriptionOf(j),
))
}
sum := sha256.Sum256([]byte(sb.String()))
return hex.EncodeToString(sum[:])
}