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