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