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