···
20
-
securejoin "github.com/cyphar/filepath-securejoin"
21
-
"github.com/gliderlabs/ssh"
"github.com/go-chi/chi/v5"
23
-
"github.com/go-git/go-git/v5/plumbing"
24
-
"github.com/go-git/go-git/v5/plumbing/object"
25
-
"tangled.sh/tangled.sh/core/hook"
11
+
"tangled.sh/tangled.sh/core/idresolver"
12
+
"tangled.sh/tangled.sh/core/jetstream"
13
+
"tangled.sh/tangled.sh/core/knotserver/config"
"tangled.sh/tangled.sh/core/knotserver/db"
27
-
"tangled.sh/tangled.sh/core/knotserver/git"
28
-
"tangled.sh/tangled.sh/core/patchutil"
15
+
"tangled.sh/tangled.sh/core/knotserver/xrpc"
16
+
tlog "tangled.sh/tangled.sh/core/log"
17
+
"tangled.sh/tangled.sh/core/notifier"
"tangled.sh/tangled.sh/core/rbac"
30
-
"tangled.sh/tangled.sh/core/types"
19
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
33
-
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
34
-
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
22
+
type Handle struct {
25
+
jc *jetstream.JetstreamClient
28
+
n *notifier.Notifier
29
+
resolver *idresolver.Resolver
37
-
func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
38
-
w.Header().Set("Content-Type", "application/json")
32
+
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
33
+
r := chi.NewRouter()
40
-
capabilities := map[string]any{
41
-
"pull_requests": map[string]any{
42
-
"format_patch": true,
43
-
"patch_submissions": true,
44
-
"branch_submissions": true,
45
-
"fork_submissions": true,
42
+
resolver: idresolver.DefaultResolver(),
49
-
jsonData, err := json.Marshal(capabilities)
45
+
err := e.AddKnot(rbac.ThisServer)
51
-
http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
47
+
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
58
-
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
59
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
60
-
l := h.l.With("path", path, "handler", "RepoIndex")
61
-
ref := chi.URLParam(r, "ref")
62
-
ref, _ = url.PathUnescape(ref)
64
-
gr, err := git.Open(path, ref)
66
-
plain, err2 := git.PlainOpen(path)
68
-
l.Error("opening repo", "error", err2.Error())
72
-
branches, _ := plain.Branches()
76
-
if errors.Is(err, plumbing.ErrReferenceNotFound) {
77
-
resp := types.RepoIndexResponse{
84
-
l.Error("opening repo", "error", err.Error())
51
+
if err = h.configureOwner(); err != nil {
91
-
commits []*object.Commit
93
-
branches []types.Branch
94
-
files []types.NiceTree
98
-
var wg sync.WaitGroup
99
-
errorsCh := make(chan error, 5)
104
-
cs, err := gr.Commits(0, 60)
106
-
errorsCh <- fmt.Errorf("commits: %w", err)
115
-
t, err := gr.TotalCommits()
117
-
errorsCh <- fmt.Errorf("calculating total: %w", err)
54
+
h.l.Info("owner set", "did", h.c.Server.Owner)
55
+
h.jc.AddDid(h.c.Server.Owner)
126
-
bs, err := gr.Branches()
128
-
errorsCh <- fmt.Errorf("fetching branches: %w", err)
137
-
ts, err := gr.Tags()
139
-
errorsCh <- fmt.Errorf("fetching tags: %w", err)
148
-
fs, err := gr.FileTree(r.Context(), "")
150
-
errorsCh <- fmt.Errorf("fetching filetree: %w", err)
160
-
for err := range errorsCh {
161
-
l.Error("loading repo", "error", err.Error())
162
-
writeError(w, err.Error(), http.StatusInternalServerError)
166
-
rtags := []*types.TagReference{}
167
-
for _, tag := range tags {
168
-
var target *object.Tag
169
-
if tag.Target != plumbing.ZeroHash {
172
-
tr := types.TagReference{
176
-
tr.Reference = types.Reference{
178
-
Hash: tag.Hash.String(),
181
-
if tag.Message != "" {
182
-
tr.Message = tag.Message
185
-
rtags = append(rtags, &tr)
188
-
var readmeContent string
189
-
var readmeFile string
190
-
for _, readme := range h.c.Repo.Readme {
191
-
content, _ := gr.FileContent(readme)
192
-
if len(content) > 0 {
193
-
readmeContent = string(content)
194
-
readmeFile = readme
199
-
mainBranch, err := gr.FindMainBranch()
201
-
writeError(w, err.Error(), http.StatusInternalServerError)
202
-
l.Error("finding main branch", "error", err.Error())
208
-
resp := types.RepoIndexResponse{
212
-
Description: getDescription(path),
213
-
Readme: readmeContent,
214
-
ReadmeFileName: readmeFile,
216
-
Branches: branches,
218
-
TotalCommits: total,
225
-
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
226
-
treePath := chi.URLParam(r, "*")
227
-
ref := chi.URLParam(r, "ref")
228
-
ref, _ = url.PathUnescape(ref)
230
-
l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
232
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
233
-
gr, err := git.Open(path, ref)
57
+
// configure known-dids in jetstream consumer
58
+
dids, err := h.db.GetAllDids()
60
+
return nil, fmt.Errorf("failed to get all dids: %w", err)
239
-
files, err := gr.FileTree(r.Context(), treePath)
241
-
writeError(w, err.Error(), http.StatusInternalServerError)
242
-
l.Error("file tree", "error", err.Error())
62
+
for _, d := range dids {
246
-
resp := types.RepoTreeResponse{
249
-
Description: getDescription(path),
250
-
DotDot: filepath.Dir(treePath),
258
-
func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
259
-
treePath := chi.URLParam(r, "*")
260
-
ref := chi.URLParam(r, "ref")
261
-
ref, _ = url.PathUnescape(ref)
263
-
l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
265
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
266
-
gr, err := git.Open(path, ref)
66
+
err = h.jc.StartJetstream(ctx, h.processMessages)
68
+
return nil, fmt.Errorf("failed to start jetstream: %w", err)
272
-
contents, err := gr.RawContent(treePath)
274
-
writeError(w, err.Error(), http.StatusBadRequest)
275
-
l.Error("file content", "error", err.Error())
72
+
r.Get("/capabilities", h.Capabilities)
73
+
r.Get("/version", h.Version)
74
+
r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
75
+
w.Write([]byte(h.c.Server.Owner))
77
+
r.Route("/{did}", func(r chi.Router) {
79
+
r.Route("/{name}", func(r chi.Router) {
279
-
mimeType := http.DetectContentType(contents)
81
+
r.Route("/languages", func(r chi.Router) {
82
+
r.Get("/", h.RepoLanguages)
83
+
r.Get("/{ref}", h.RepoLanguages)
281
-
// exception for svg
282
-
if filepath.Ext(treePath) == ".svg" {
283
-
mimeType = "image/svg+xml"
86
+
r.Get("/", h.RepoIndex)
87
+
r.Get("/info/refs", h.InfoRefs)
88
+
r.Post("/git-upload-pack", h.UploadPack)
89
+
r.Post("/git-receive-pack", h.ReceivePack)
90
+
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
286
-
contentHash := sha256.Sum256(contents)
287
-
eTag := fmt.Sprintf("\"%x\"", contentHash)
92
+
r.Route("/tree/{ref}", func(r chi.Router) {
93
+
r.Get("/", h.RepoIndex)
94
+
r.Get("/*", h.RepoTree)
289
-
// allow image, video, and text/plain files to be served directly
291
-
case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
292
-
if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
293
-
w.WriteHeader(http.StatusNotModified)
296
-
w.Header().Set("ETag", eTag)
97
+
r.Route("/blob/{ref}", func(r chi.Router) {
298
-
case strings.HasPrefix(mimeType, "text/plain"):
299
-
w.Header().Set("Cache-Control", "public, no-cache")
302
-
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
303
-
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
307
-
w.Header().Set("Content-Type", mimeType)
311
-
func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
312
-
treePath := chi.URLParam(r, "*")
313
-
ref := chi.URLParam(r, "ref")
314
-
ref, _ = url.PathUnescape(ref)
316
-
l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
318
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
319
-
gr, err := git.Open(path, ref)
325
-
var isBinaryFile bool = false
326
-
contents, err := gr.FileContent(treePath)
327
-
if errors.Is(err, git.ErrBinaryFile) {
328
-
isBinaryFile = true
329
-
} else if errors.Is(err, object.ErrFileNotFound) {
332
-
} else if err != nil {
333
-
writeError(w, err.Error(), http.StatusInternalServerError)
337
-
bytes := []byte(contents)
338
-
// safe := string(sanitize(bytes))
339
-
sizeHint := len(bytes)
341
-
resp := types.RepoBlobResponse{
343
-
Contents: string(bytes),
345
-
IsBinary: isBinaryFile,
346
-
SizeHint: uint64(sizeHint),
349
-
h.showFile(resp, w, l)
352
-
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
353
-
name := chi.URLParam(r, "name")
354
-
file := chi.URLParam(r, "file")
356
-
l := h.l.With("handler", "Archive", "name", name, "file", file)
358
-
// TODO: extend this to add more files compression (e.g.: xz)
359
-
if !strings.HasSuffix(file, ".tar.gz") {
364
-
ref := strings.TrimSuffix(file, ".tar.gz")
366
-
unescapedRef, err := url.PathUnescape(ref)
372
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
374
-
// This allows the browser to use a proper name for the file when
376
-
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
377
-
setContentDisposition(w, filename)
380
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
381
-
gr, err := git.Open(path, unescapedRef)
387
-
gw := gzip.NewWriter(w)
390
-
prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
391
-
err = gr.WriteTar(gw, prefix)
393
-
// once we start writing to the body we can't report error anymore
394
-
// so we are only left with printing the error.
395
-
l.Error("writing tar file", "error", err.Error())
401
-
// once we start writing to the body we can't report error anymore
402
-
// so we are only left with printing the error.
403
-
l.Error("flushing?", "error", err.Error())
408
-
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
409
-
ref := chi.URLParam(r, "ref")
410
-
ref, _ = url.PathUnescape(ref)
412
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
414
-
l := h.l.With("handler", "Log", "ref", ref, "path", path)
416
-
gr, err := git.Open(path, ref)
422
-
// Get page parameters
426
-
if pageParam := r.URL.Query().Get("page"); pageParam != "" {
427
-
if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
432
-
if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
433
-
if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
438
-
// convert to offset/limit
439
-
offset := (page - 1) * pageSize
442
-
commits, err := gr.Commits(offset, limit)
444
-
writeError(w, err.Error(), http.StatusInternalServerError)
445
-
l.Error("fetching commits", "error", err.Error())
449
-
total := len(commits)
451
-
resp := types.RepoLogResponse{
454
-
Description: getDescription(path),
465
-
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
466
-
ref := chi.URLParam(r, "ref")
467
-
ref, _ = url.PathUnescape(ref)
469
-
l := h.l.With("handler", "Diff", "ref", ref)
471
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
472
-
gr, err := git.Open(path, ref)
478
-
diff, err := gr.Diff()
480
-
writeError(w, err.Error(), http.StatusInternalServerError)
481
-
l.Error("getting diff", "error", err.Error())
485
-
resp := types.RepoCommitResponse{
494
-
func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
495
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
496
-
l := h.l.With("handler", "Refs")
498
-
gr, err := git.Open(path, "")
504
-
tags, err := gr.Tags()
506
-
// Non-fatal, we *should* have at least one branch to show.
507
-
l.Warn("getting tags", "error", err.Error())
510
-
rtags := []*types.TagReference{}
511
-
for _, tag := range tags {
512
-
var target *object.Tag
513
-
if tag.Target != plumbing.ZeroHash {
516
-
tr := types.TagReference{
520
-
tr.Reference = types.Reference{
522
-
Hash: tag.Hash.String(),
525
-
if tag.Message != "" {
526
-
tr.Message = tag.Message
529
-
rtags = append(rtags, &tr)
532
-
resp := types.RepoTagsResponse{
540
-
func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
541
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
543
-
gr, err := git.PlainOpen(path)
101
+
r.Route("/raw/{ref}", func(r chi.Router) {
102
+
r.Get("/*", h.BlobRaw)
549
-
branches, _ := gr.Branches()
551
-
resp := types.RepoBranchesResponse{
552
-
Branches: branches,
559
-
func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
560
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561
-
branchName := chi.URLParam(r, "branch")
562
-
branchName, _ = url.PathUnescape(branchName)
564
-
l := h.l.With("handler", "Branch")
566
-
gr, err := git.PlainOpen(path)
572
-
ref, err := gr.Branch(branchName)
574
-
l.Error("getting branch", "error", err.Error())
575
-
writeError(w, err.Error(), http.StatusInternalServerError)
105
+
r.Get("/log/{ref}", h.Log)
106
+
r.Get("/archive/{file}", h.Archive)
107
+
r.Get("/commit/{ref}", h.Diff)
108
+
r.Get("/tags", h.Tags)
109
+
r.Route("/branches", func(r chi.Router) {
110
+
r.Get("/", h.Branches)
111
+
r.Get("/{branch}", h.Branch)
112
+
r.Get("/default", h.DefaultBranch)
579
-
commit, err := gr.Commit(ref.Hash())
581
-
l.Error("getting commit object", "error", err.Error())
582
-
writeError(w, err.Error(), http.StatusInternalServerError)
118
+
r.Mount("/xrpc", h.XrpcRouter())
586
-
defaultBranch, err := gr.FindMainBranch()
589
-
l.Error("getting default branch", "error", err.Error())
590
-
// do not quit though
591
-
} else if defaultBranch == branchName {
120
+
// Socket that streams git oplogs
121
+
r.Get("/events", h.Events)
595
-
resp := types.RepoBranchResponse{
596
-
Branch: types.Branch{
597
-
Reference: types.Reference{
598
-
Name: ref.Name().Short(),
599
-
Hash: ref.Hash().String(),
602
-
IsDefault: isDefault,
123
+
// All public keys on the knot.
124
+
r.Get("/keys", h.Keys)
610
-
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
611
-
l := h.l.With("handler", "Keys")
614
-
case http.MethodGet:
615
-
keys, err := h.db.GetAllPublicKeys()
617
-
writeError(w, err.Error(), http.StatusInternalServerError)
618
-
l.Error("getting public keys", "error", err.Error())
622
-
data := make([]map[string]any, 0)
623
-
for _, key := range keys {
625
-
data = append(data, j)
630
-
case http.MethodPut:
631
-
pk := db.PublicKey{}
632
-
if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
633
-
writeError(w, "invalid request body", http.StatusBadRequest)
129
+
func (h *Handle) XrpcRouter() http.Handler {
130
+
logger := tlog.New("knots")
637
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
639
-
writeError(w, "invalid pubkey", http.StatusBadRequest)
132
+
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
642
-
if err := h.db.AddPublicKey(pk); err != nil {
643
-
writeError(w, err.Error(), http.StatusInternalServerError)
644
-
l.Error("adding public key", "error", err.Error())
648
-
w.WriteHeader(http.StatusNoContent)
134
+
xrpc := &xrpc.Xrpc{
141
+
Resolver: h.resolver,
142
+
ServiceAuth: serviceAuth,
144
+
return xrpc.Router()
653
-
func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
654
-
l := h.l.With("handler", "RepoForkAheadBehind")
147
+
// version is set during build time.
657
-
Did string `json:"did"`
658
-
Source string `json:"source"`
659
-
Name string `json:"name,omitempty"`
660
-
HiddenRef string `json:"hiddenref"`
663
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
664
-
writeError(w, "invalid request body", http.StatusBadRequest)
669
-
source := data.Source
671
-
if did == "" || source == "" {
672
-
l.Error("invalid request body, empty did or name")
673
-
w.WriteHeader(http.StatusBadRequest)
678
-
if data.Name != "" {
681
-
name = filepath.Base(source)
684
-
branch := chi.URLParam(r, "branch")
685
-
branch, _ = url.PathUnescape(branch)
687
-
relativeRepoPath := filepath.Join(did, name)
688
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
690
-
gr, err := git.PlainOpen(repoPath)
697
-
forkCommit, err := gr.ResolveRevision(branch)
699
-
l.Error("error resolving ref revision", "msg", err.Error())
700
-
writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
704
-
sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
706
-
l.Error("error resolving hidden ref revision", "msg", err.Error())
707
-
writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
711
-
status := types.UpToDate
712
-
if forkCommit.Hash.String() != sourceCommit.Hash.String() {
713
-
isAncestor, err := forkCommit.IsAncestor(sourceCommit)
715
-
log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
150
+
func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
152
+
info, ok := debug.ReadBuildInfo()
154
+
http.Error(w, "failed to read build info", http.StatusInternalServerError)
720
-
status = types.FastForwardable
722
-
status = types.Conflict
159
+
for _, mod := range info.Deps {
160
+
if mod.Path == "tangled.sh/tangled.sh/knotserver" {
161
+
version = mod.Version
726
-
w.Header().Set("Content-Type", "application/json")
727
-
json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
730
-
func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
731
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
732
-
ref := chi.URLParam(r, "ref")
733
-
ref, _ = url.PathUnescape(ref)
735
-
l := h.l.With("handler", "RepoLanguages")
737
-
gr, err := git.Open(repoPath, ref)
739
-
l.Error("opening repo", "error", err.Error())
744
-
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
747
-
sizes, err := gr.AnalyzeLanguages(ctx)
749
-
l.Error("failed to analyze languages", "error", err.Error())
750
-
writeError(w, err.Error(), http.StatusNoContent)
754
-
resp := types.RepoLanguageResponse{Languages: sizes}
759
-
func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
760
-
l := h.l.With("handler", "RepoForkSync")
763
-
Did string `json:"did"`
764
-
Source string `json:"source"`
765
-
Name string `json:"name,omitempty"`
768
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
769
-
writeError(w, "invalid request body", http.StatusBadRequest)
774
-
source := data.Source
776
-
if did == "" || source == "" {
777
-
l.Error("invalid request body, empty did or name")
778
-
w.WriteHeader(http.StatusBadRequest)
783
-
if data.Name != "" {
786
-
name = filepath.Base(source)
789
-
branch := chi.URLParam(r, "*")
790
-
branch, _ = url.PathUnescape(branch)
792
-
relativeRepoPath := filepath.Join(did, name)
793
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
795
-
gr, err := git.Open(repoPath, branch)
804
-
l.Error("error syncing repo fork", "error", err.Error())
805
-
writeError(w, err.Error(), http.StatusInternalServerError)
809
-
w.WriteHeader(http.StatusNoContent)
812
-
func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
813
-
l := h.l.With("handler", "RepoFork")
816
-
Did string `json:"did"`
817
-
Source string `json:"source"`
818
-
Name string `json:"name,omitempty"`
821
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
822
-
writeError(w, "invalid request body", http.StatusBadRequest)
827
-
source := data.Source
829
-
if did == "" || source == "" {
830
-
l.Error("invalid request body, empty did or name")
831
-
w.WriteHeader(http.StatusBadRequest)
836
-
if data.Name != "" {
839
-
name = filepath.Base(source)
842
-
relativeRepoPath := filepath.Join(did, name)
843
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
845
-
err := git.Fork(repoPath, source)
847
-
l.Error("forking repo", "error", err.Error())
848
-
writeError(w, err.Error(), http.StatusInternalServerError)
852
-
// add perms for this user to access the repo
853
-
err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
855
-
l.Error("adding repo permissions", "error", err.Error())
856
-
writeError(w, err.Error(), http.StatusInternalServerError)
862
-
hook.WithScanPath(h.c.Repo.ScanPath),
863
-
hook.WithInternalApi(h.c.Server.InternalListenAddr),
868
-
w.WriteHeader(http.StatusNoContent)
871
-
func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
872
-
l := h.l.With("handler", "RemoveRepo")
875
-
Did string `json:"did"`
876
-
Name string `json:"name"`
879
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
880
-
writeError(w, "invalid request body", http.StatusBadRequest)
887
-
if did == "" || name == "" {
888
-
l.Error("invalid request body, empty did or name")
889
-
w.WriteHeader(http.StatusBadRequest)
893
-
relativeRepoPath := filepath.Join(did, name)
894
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
895
-
err := os.RemoveAll(repoPath)
897
-
l.Error("removing repo", "error", err.Error())
898
-
writeError(w, err.Error(), http.StatusInternalServerError)
902
-
w.WriteHeader(http.StatusNoContent)
905
-
func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
906
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
908
-
data := types.MergeRequest{}
910
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
911
-
writeError(w, err.Error(), http.StatusBadRequest)
912
-
h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
916
-
mo := &git.MergeOptions{
917
-
AuthorName: data.AuthorName,
918
-
AuthorEmail: data.AuthorEmail,
919
-
CommitBody: data.CommitBody,
920
-
CommitMessage: data.CommitMessage,
923
-
patch := data.Patch
924
-
branch := data.Branch
925
-
gr, err := git.Open(path, branch)
931
-
mo.FormatPatch = patchutil.IsFormatPatch(patch)
933
-
if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
934
-
var mergeErr *git.ErrMerge
935
-
if errors.As(err, &mergeErr) {
936
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
937
-
for i, conflict := range mergeErr.Conflicts {
938
-
conflicts[i] = types.ConflictInfo{
939
-
Filename: conflict.Filename,
940
-
Reason: conflict.Reason,
943
-
response := types.MergeCheckResponse{
944
-
IsConflicted: true,
945
-
Conflicts: conflicts,
946
-
Message: mergeErr.Message,
948
-
writeConflict(w, response)
949
-
h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
951
-
writeError(w, err.Error(), http.StatusBadRequest)
952
-
h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
167
+
version = "unknown"
957
-
w.WriteHeader(http.StatusOK)
171
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
172
+
fmt.Fprintf(w, "knotserver/%s", version)
960
-
func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
961
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
964
-
Patch string `json:"patch"`
965
-
Branch string `json:"branch"`
175
+
func (h *Handle) configureOwner() error {
176
+
cfgOwner := h.c.Server.Owner
968
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
969
-
writeError(w, err.Error(), http.StatusBadRequest)
970
-
h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
178
+
rbacDomain := "thisserver"
974
-
patch := data.Patch
975
-
branch := data.Branch
976
-
gr, err := git.Open(path, branch)
180
+
existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
982
-
err = gr.MergeCheck([]byte(patch), branch)
984
-
response := types.MergeCheckResponse{
985
-
IsConflicted: false,
987
-
writeJSON(w, response)
185
+
switch len(existing) {
187
+
// no owner configured, continue
189
+
// find existing owner
190
+
existingOwner := existing[0]
991
-
var mergeErr *git.ErrMerge
992
-
if errors.As(err, &mergeErr) {
993
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
994
-
for i, conflict := range mergeErr.Conflicts {
995
-
conflicts[i] = types.ConflictInfo{
996
-
Filename: conflict.Filename,
997
-
Reason: conflict.Reason,
1000
-
response := types.MergeCheckResponse{
1001
-
IsConflicted: true,
1002
-
Conflicts: conflicts,
1003
-
Message: mergeErr.Message,
192
+
// no ownership change, this is okay
193
+
if existingOwner == h.c.Server.Owner {
1005
-
writeConflict(w, response)
1006
-
h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1009
-
writeError(w, err.Error(), http.StatusInternalServerError)
1010
-
h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1013
-
func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1014
-
rev1 := chi.URLParam(r, "rev1")
1015
-
rev1, _ = url.PathUnescape(rev1)
1017
-
rev2 := chi.URLParam(r, "rev2")
1018
-
rev2, _ = url.PathUnescape(rev2)
1020
-
l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1022
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1023
-
gr, err := git.PlainOpen(path)
1029
-
commit1, err := gr.ResolveRevision(rev1)
1031
-
l.Error("error resolving revision 1", "msg", err.Error())
1032
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1036
-
commit2, err := gr.ResolveRevision(rev2)
1038
-
l.Error("error resolving revision 2", "msg", err.Error())
1039
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1043
-
rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
1045
-
l.Error("error comparing revisions", "msg", err.Error())
1046
-
writeError(w, "error comparing revisions", http.StatusBadRequest)
1050
-
writeJSON(w, types.RepoFormatPatchResponse{
1051
-
Rev1: commit1.Hash.String(),
1052
-
Rev2: commit2.Hash.String(),
1053
-
FormatPatch: formatPatch,
1059
-
func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1060
-
l := h.l.With("handler", "NewHiddenRef")
1062
-
forkRef := chi.URLParam(r, "forkRef")
1063
-
forkRef, _ = url.PathUnescape(forkRef)
1065
-
remoteRef := chi.URLParam(r, "remoteRef")
1066
-
remoteRef, _ = url.PathUnescape(remoteRef)
1068
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1069
-
gr, err := git.PlainOpen(path)
1075
-
err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1077
-
l.Error("error tracking hidden remote ref", "msg", err.Error())
1078
-
writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1082
-
w.WriteHeader(http.StatusNoContent)
1086
-
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1087
-
l := h.l.With("handler", "AddMember")
1090
-
Did string `json:"did"`
1093
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1094
-
writeError(w, "invalid request body", http.StatusBadRequest)
1100
-
if err := h.db.AddDid(did); err != nil {
1101
-
l.Error("adding did", "error", err.Error())
1102
-
writeError(w, err.Error(), http.StatusInternalServerError)
1107
-
if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil {
1108
-
l.Error("adding member", "error", err.Error())
1109
-
writeError(w, err.Error(), http.StatusInternalServerError)
1113
-
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1114
-
l.Error("fetching and adding keys", "error", err.Error())
1115
-
writeError(w, err.Error(), http.StatusInternalServerError)
1119
-
w.WriteHeader(http.StatusNoContent)
1122
-
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1123
-
l := h.l.With("handler", "AddRepoCollaborator")
1126
-
Did string `json:"did"`
1129
-
ownerDid := chi.URLParam(r, "did")
1130
-
repo := chi.URLParam(r, "name")
1132
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1133
-
writeError(w, "invalid request body", http.StatusBadRequest)
1137
-
if err := h.db.AddDid(data.Did); err != nil {
1138
-
l.Error("adding did", "error", err.Error())
1139
-
writeError(w, err.Error(), http.StatusInternalServerError)
1142
-
h.jc.AddDid(data.Did)
1144
-
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1145
-
if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil {
1146
-
l.Error("adding repo collaborator", "error", err.Error())
1147
-
writeError(w, err.Error(), http.StatusInternalServerError)
1151
-
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1152
-
l.Error("fetching and adding keys", "error", err.Error())
1153
-
writeError(w, err.Error(), http.StatusInternalServerError)
1157
-
w.WriteHeader(http.StatusNoContent)
1160
-
func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1161
-
l := h.l.With("handler", "DefaultBranch")
1162
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1164
-
gr, err := git.Open(path, "")
1170
-
branch, err := gr.FindMainBranch()
1172
-
writeError(w, err.Error(), http.StatusInternalServerError)
1173
-
l.Error("getting default branch", "error", err.Error())
1177
-
writeJSON(w, types.RepoDefaultBranchResponse{
1182
-
func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1183
-
l := h.l.With("handler", "SetDefaultBranch")
1184
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1187
-
Branch string `json:"branch"`
1190
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1191
-
writeError(w, err.Error(), http.StatusBadRequest)
1195
-
gr, err := git.PlainOpen(path)
1201
-
err = gr.SetDefaultBranch(data.Branch)
1203
-
writeError(w, err.Error(), http.StatusInternalServerError)
1204
-
l.Error("setting default branch", "error", err.Error())
1208
-
w.WriteHeader(http.StatusNoContent)
1211
-
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1212
-
w.Write([]byte("ok"))
1215
-
func validateRepoName(name string) error {
1216
-
// check for path traversal attempts
1217
-
if name == "." || name == ".." ||
1218
-
strings.Contains(name, "/") || strings.Contains(name, "\\") {
1219
-
return fmt.Errorf("Repository name contains invalid path characters")
1222
-
// check for sequences that could be used for traversal when normalized
1223
-
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1224
-
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1225
-
return fmt.Errorf("Repository name contains invalid path sequence")
1228
-
// then continue with character validation
1229
-
for _, char := range name {
1230
-
if !((char >= 'a' && char <= 'z') ||
1231
-
(char >= 'A' && char <= 'Z') ||
1232
-
(char >= '0' && char <= '9') ||
1233
-
char == '-' || char == '_' || char == '.') {
1234
-
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
197
+
// remove existing owner
198
+
err = h.e.RemoveKnotOwner(rbacDomain, existingOwner)
1238
-
// additional check to prevent multiple sequential dots
1239
-
if strings.Contains(name, "..") {
1240
-
return fmt.Errorf("Repository name cannot contain sequential dots")
203
+
return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
1243
-
// if all checks pass
206
+
return h.e.AddKnotOwner(rbacDomain, cfgOwner)