~james/core

core/migrator/api/validation.go -rw-r--r-- 2.9 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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[:])
}