1package xrpc
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "net/http"
8 "path/filepath"
9 "strings"
10
11 comatproto "github.com/bluesky-social/indigo/api/atproto"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "github.com/bluesky-social/indigo/xrpc"
14 securejoin "github.com/cyphar/filepath-securejoin"
15 gogit "github.com/go-git/go-git/v5"
16 "tangled.sh/tangled.sh/core/api/tangled"
17 "tangled.sh/tangled.sh/core/hook"
18 "tangled.sh/tangled.sh/core/knotserver/git"
19 "tangled.sh/tangled.sh/core/rbac"
20 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
21)
22
23func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) {
24 l := h.Logger.With("handler", "NewRepo")
25 fail := func(e xrpcerr.XrpcError) {
26 l.Error("failed", "kind", e.Tag, "error", e.Message)
27 writeError(w, e, http.StatusBadRequest)
28 }
29
30 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
31 if !ok {
32 fail(xrpcerr.MissingActorDidError)
33 return
34 }
35
36 isMember, err := h.Enforcer.IsRepoCreateAllowed(actorDid.String(), rbac.ThisServer)
37 if err != nil {
38 fail(xrpcerr.GenericError(err))
39 return
40 }
41 if !isMember {
42 fail(xrpcerr.AccessControlError(actorDid.String()))
43 return
44 }
45
46 var data tangled.RepoCreate_Input
47 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
48 fail(xrpcerr.GenericError(err))
49 return
50 }
51
52 rkey := data.Rkey
53
54 ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String())
55 if err != nil || ident.Handle.IsInvalidHandle() {
56 fail(xrpcerr.GenericError(err))
57 return
58 }
59
60 xrpcc := xrpc.Client{
61 Host: ident.PDSEndpoint(),
62 }
63
64 resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey)
65 if err != nil {
66 fail(xrpcerr.GenericError(err))
67 return
68 }
69
70 repo := resp.Value.Val.(*tangled.Repo)
71
72 defaultBranch := h.Config.Repo.MainBranch
73 if data.DefaultBranch != nil && *data.DefaultBranch != "" {
74 defaultBranch = *data.DefaultBranch
75 }
76
77 if err := validateRepoName(repo.Name); err != nil {
78 l.Error("creating repo", "error", err.Error())
79 fail(xrpcerr.GenericError(err))
80 return
81 }
82
83 relativeRepoPath := filepath.Join(actorDid.String(), repo.Name)
84 repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
85
86 if data.Source != nil && *data.Source != "" {
87 err = git.Fork(repoPath, *data.Source)
88 if err != nil {
89 l.Error("forking repo", "error", err.Error())
90 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
91 return
92 }
93 } else {
94 err = git.InitBare(repoPath, defaultBranch)
95 if err != nil {
96 l.Error("initializing bare repo", "error", err.Error())
97 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
98 fail(xrpcerr.RepoExistsError("repository already exists"))
99 return
100 } else {
101 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
102 return
103 }
104 }
105 }
106
107 // add perms for this user to access the repo
108 err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath)
109 if err != nil {
110 l.Error("adding repo permissions", "error", err.Error())
111 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
112 return
113 }
114
115 hook.SetupRepo(
116 hook.Config(
117 hook.WithScanPath(h.Config.Repo.ScanPath),
118 hook.WithInternalApi(h.Config.Server.InternalListenAddr),
119 ),
120 repoPath,
121 )
122
123 w.WriteHeader(http.StatusOK)
124}
125
126func validateRepoName(name string) error {
127 // check for path traversal attempts
128 if name == "." || name == ".." ||
129 strings.Contains(name, "/") || strings.Contains(name, "\\") {
130 return fmt.Errorf("Repository name contains invalid path characters")
131 }
132
133 // check for sequences that could be used for traversal when normalized
134 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
135 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
136 return fmt.Errorf("Repository name contains invalid path sequence")
137 }
138
139 // then continue with character validation
140 for _, char := range name {
141 if !((char >= 'a' && char <= 'z') ||
142 (char >= 'A' && char <= 'Z') ||
143 (char >= '0' && char <= '9') ||
144 char == '-' || char == '_' || char == '.') {
145 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
146 }
147 }
148
149 // additional check to prevent multiple sequential dots
150 if strings.Contains(name, "..") {
151 return fmt.Errorf("Repository name cannot contain sequential dots")
152 }
153
154 // if all checks pass
155 return nil
156}