This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotserver / xrpc / create_repo.go
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}