1package knotserver
2
3import (
4 "compress/gzip"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "log"
12 "net/http"
13 "path/filepath"
14 "strconv"
15 "strings"
16
17 securejoin "github.com/cyphar/filepath-securejoin"
18 "github.com/gliderlabs/ssh"
19 "github.com/go-chi/chi/v5"
20 gogit "github.com/go-git/go-git/v5"
21 "github.com/go-git/go-git/v5/plumbing"
22 "github.com/go-git/go-git/v5/plumbing/object"
23 "github.com/sotangled/tangled/knotserver/db"
24 "github.com/sotangled/tangled/knotserver/git"
25 "github.com/sotangled/tangled/types"
26)
27
28func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
29 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
30}
31
32func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
33 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
34 l := h.l.With("path", path, "handler", "RepoIndex")
35 ref := chi.URLParam(r, "ref")
36
37 gr, err := git.Open(path, ref)
38 if err != nil {
39 log.Println(err)
40 if errors.Is(err, plumbing.ErrReferenceNotFound) {
41 resp := types.RepoIndexResponse{
42 IsEmpty: true,
43 }
44 writeJSON(w, resp)
45 return
46 } else {
47 l.Error("opening repo", "error", err.Error())
48 notFound(w)
49 return
50 }
51 }
52
53 commits, err := gr.Commits()
54 total := len(commits)
55 if err != nil {
56 writeError(w, err.Error(), http.StatusInternalServerError)
57 l.Error("fetching commits", "error", err.Error())
58 return
59 }
60 if len(commits) > 10 {
61 commits = commits[:10]
62 }
63
64 branches, err := gr.Branches()
65 if err != nil {
66 l.Error("getting branches", "error", err.Error())
67 writeError(w, err.Error(), http.StatusInternalServerError)
68 return
69 }
70
71 bs := []types.Branch{}
72 for _, branch := range branches {
73 b := types.Branch{}
74 b.Hash = branch.Hash().String()
75 b.Name = branch.Name().Short()
76 bs = append(bs, b)
77 }
78
79 tags, err := gr.Tags()
80 if err != nil {
81 // Non-fatal, we *should* have at least one branch to show.
82 l.Warn("getting tags", "error", err.Error())
83 }
84
85 rtags := []*types.TagReference{}
86 for _, tag := range tags {
87 tr := types.TagReference{
88 Tag: tag.TagObject(),
89 }
90
91 tr.Reference = types.Reference{
92 Name: tag.Name(),
93 Hash: tag.Hash().String(),
94 }
95
96 if tag.Message() != "" {
97 tr.Message = tag.Message()
98 }
99
100 rtags = append(rtags, &tr)
101 }
102
103 var readmeContent string
104 var readmeFile string
105 for _, readme := range h.c.Repo.Readme {
106 content, _ := gr.FileContent(readme)
107 if len(content) > 0 {
108 readmeContent = string(content)
109 readmeFile = readme
110 }
111 }
112
113 files, err := gr.FileTree("")
114 if err != nil {
115 writeError(w, err.Error(), http.StatusInternalServerError)
116 l.Error("file tree", "error", err.Error())
117 return
118 }
119
120 if ref == "" {
121 mainBranch, err := gr.FindMainBranch()
122 if err != nil {
123 writeError(w, err.Error(), http.StatusInternalServerError)
124 l.Error("finding main branch", "error", err.Error())
125 return
126 }
127 ref = mainBranch
128 }
129
130 resp := types.RepoIndexResponse{
131 IsEmpty: false,
132 Ref: ref,
133 Commits: commits,
134 Description: getDescription(path),
135 Readme: readmeContent,
136 ReadmeFileName: readmeFile,
137 Files: files,
138 Branches: bs,
139 Tags: rtags,
140 TotalCommits: total,
141 }
142
143 writeJSON(w, resp)
144 return
145}
146
147func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
148 treePath := chi.URLParam(r, "*")
149 ref := chi.URLParam(r, "ref")
150
151 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
152
153 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
154 gr, err := git.Open(path, ref)
155 if err != nil {
156 notFound(w)
157 return
158 }
159
160 files, err := gr.FileTree(treePath)
161 if err != nil {
162 writeError(w, err.Error(), http.StatusInternalServerError)
163 l.Error("file tree", "error", err.Error())
164 return
165 }
166
167 resp := types.RepoTreeResponse{
168 Ref: ref,
169 Parent: treePath,
170 Description: getDescription(path),
171 DotDot: filepath.Dir(treePath),
172 Files: files,
173 }
174
175 writeJSON(w, resp)
176 return
177}
178
179func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
180 treePath := chi.URLParam(r, "*")
181 ref := chi.URLParam(r, "ref")
182
183 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath)
184
185 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
186 gr, err := git.Open(path, ref)
187 if err != nil {
188 notFound(w)
189 return
190 }
191
192 var isBinaryFile bool = false
193 contents, err := gr.FileContent(treePath)
194 if errors.Is(err, git.ErrBinaryFile) {
195 isBinaryFile = true
196 } else if errors.Is(err, object.ErrFileNotFound) {
197 notFound(w)
198 return
199 } else if err != nil {
200 writeError(w, err.Error(), http.StatusInternalServerError)
201 return
202 }
203
204 bytes := []byte(contents)
205 // safe := string(sanitize(bytes))
206 sizeHint := len(bytes)
207
208 resp := types.RepoBlobResponse{
209 Ref: ref,
210 Contents: string(bytes),
211 Path: treePath,
212 IsBinary: isBinaryFile,
213 SizeHint: uint64(sizeHint),
214 }
215
216 h.showFile(resp, w, l)
217}
218
219func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
220 name := chi.URLParam(r, "name")
221 file := chi.URLParam(r, "file")
222
223 l := h.l.With("handler", "Archive", "name", name, "file", file)
224
225 // TODO: extend this to add more files compression (e.g.: xz)
226 if !strings.HasSuffix(file, ".tar.gz") {
227 notFound(w)
228 return
229 }
230
231 ref := strings.TrimSuffix(file, ".tar.gz")
232
233 // This allows the browser to use a proper name for the file when
234 // downloading
235 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
236 setContentDisposition(w, filename)
237 setGZipMIME(w)
238
239 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
240 gr, err := git.Open(path, ref)
241 if err != nil {
242 notFound(w)
243 return
244 }
245
246 gw := gzip.NewWriter(w)
247 defer gw.Close()
248
249 prefix := fmt.Sprintf("%s-%s", name, ref)
250 err = gr.WriteTar(gw, prefix)
251 if err != nil {
252 // once we start writing to the body we can't report error anymore
253 // so we are only left with printing the error.
254 l.Error("writing tar file", "error", err.Error())
255 return
256 }
257
258 err = gw.Flush()
259 if err != nil {
260 // once we start writing to the body we can't report error anymore
261 // so we are only left with printing the error.
262 l.Error("flushing?", "error", err.Error())
263 return
264 }
265}
266
267func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
268 ref := chi.URLParam(r, "ref")
269 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
270
271 l := h.l.With("handler", "Log", "ref", ref, "path", path)
272
273 gr, err := git.Open(path, ref)
274 if err != nil {
275 notFound(w)
276 return
277 }
278
279 commits, err := gr.Commits()
280 if err != nil {
281 writeError(w, err.Error(), http.StatusInternalServerError)
282 l.Error("fetching commits", "error", err.Error())
283 return
284 }
285
286 // Get page parameters
287 page := 1
288 pageSize := 30
289
290 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
291 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
292 page = p
293 }
294 }
295
296 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
297 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
298 pageSize = ps
299 }
300 }
301
302 // Calculate pagination
303 start := (page - 1) * pageSize
304 end := start + pageSize
305 total := len(commits)
306
307 if start >= total {
308 commits = []*object.Commit{}
309 } else {
310 if end > total {
311 end = total
312 }
313 commits = commits[start:end]
314 }
315
316 resp := types.RepoLogResponse{
317 Commits: commits,
318 Ref: ref,
319 Description: getDescription(path),
320 Log: true,
321 Total: total,
322 Page: page,
323 PerPage: pageSize,
324 }
325
326 writeJSON(w, resp)
327 return
328}
329
330func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
331 ref := chi.URLParam(r, "ref")
332
333 l := h.l.With("handler", "Diff", "ref", ref)
334
335 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
336 gr, err := git.Open(path, ref)
337 if err != nil {
338 notFound(w)
339 return
340 }
341
342 diff, err := gr.Diff()
343 if err != nil {
344 writeError(w, err.Error(), http.StatusInternalServerError)
345 l.Error("getting diff", "error", err.Error())
346 return
347 }
348
349 resp := types.RepoCommitResponse{
350 Ref: ref,
351 Diff: diff,
352 }
353
354 writeJSON(w, resp)
355 return
356}
357
358func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
359 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
360 l := h.l.With("handler", "Refs")
361
362 gr, err := git.Open(path, "")
363 if err != nil {
364 notFound(w)
365 return
366 }
367
368 tags, err := gr.Tags()
369 if err != nil {
370 // Non-fatal, we *should* have at least one branch to show.
371 l.Warn("getting tags", "error", err.Error())
372 }
373
374 rtags := []*types.TagReference{}
375 for _, tag := range tags {
376 tr := types.TagReference{
377 Tag: tag.TagObject(),
378 }
379
380 tr.Reference = types.Reference{
381 Name: tag.Name(),
382 Hash: tag.Hash().String(),
383 }
384
385 if tag.Message() != "" {
386 tr.Message = tag.Message()
387 }
388
389 rtags = append(rtags, &tr)
390 }
391
392 resp := types.RepoTagsResponse{
393 Tags: rtags,
394 }
395
396 writeJSON(w, resp)
397 return
398}
399
400func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
401 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
402 l := h.l.With("handler", "Branches")
403
404 gr, err := git.Open(path, "")
405 if err != nil {
406 notFound(w)
407 return
408 }
409
410 branches, err := gr.Branches()
411 if err != nil {
412 l.Error("getting branches", "error", err.Error())
413 writeError(w, err.Error(), http.StatusInternalServerError)
414 return
415 }
416
417 bs := []types.Branch{}
418 for _, branch := range branches {
419 b := types.Branch{}
420 b.Hash = branch.Hash().String()
421 b.Name = branch.Name().Short()
422 bs = append(bs, b)
423 }
424
425 resp := types.RepoBranchesResponse{
426 Branches: bs,
427 }
428
429 writeJSON(w, resp)
430 return
431}
432
433func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
434 l := h.l.With("handler", "Keys")
435
436 switch r.Method {
437 case http.MethodGet:
438 keys, err := h.db.GetAllPublicKeys()
439 if err != nil {
440 writeError(w, err.Error(), http.StatusInternalServerError)
441 l.Error("getting public keys", "error", err.Error())
442 return
443 }
444
445 data := make([]map[string]interface{}, 0)
446 for _, key := range keys {
447 j := key.JSON()
448 data = append(data, j)
449 }
450 writeJSON(w, data)
451 return
452
453 case http.MethodPut:
454 pk := db.PublicKey{}
455 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
456 writeError(w, "invalid request body", http.StatusBadRequest)
457 return
458 }
459
460 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
461 if err != nil {
462 writeError(w, "invalid pubkey", http.StatusBadRequest)
463 }
464
465 if err := h.db.AddPublicKey(pk); err != nil {
466 writeError(w, err.Error(), http.StatusInternalServerError)
467 l.Error("adding public key", "error", err.Error())
468 return
469 }
470
471 w.WriteHeader(http.StatusNoContent)
472 return
473 }
474}
475
476func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
477 l := h.l.With("handler", "NewRepo")
478
479 data := struct {
480 Did string `json:"did"`
481 Name string `json:"name"`
482 DefaultBranch string `json:"default_branch,omitempty"`
483 }{}
484
485 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
486 writeError(w, "invalid request body", http.StatusBadRequest)
487 return
488 }
489
490 log.Println("branch", data.DefaultBranch)
491 if data.DefaultBranch == "" {
492 data.DefaultBranch = h.c.Repo.MainBranch
493 }
494
495 did := data.Did
496 name := data.Name
497 defaultBranch := data.DefaultBranch
498
499 relativeRepoPath := filepath.Join(did, name)
500 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
501 err := git.InitBare(repoPath, defaultBranch)
502 if err != nil {
503 l.Error("initializing bare repo", "error", err.Error())
504 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
505 writeError(w, "That repo already exists!", http.StatusConflict)
506 return
507 } else {
508 writeError(w, err.Error(), http.StatusInternalServerError)
509 return
510 }
511 }
512
513 // add perms for this user to access the repo
514 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
515 if err != nil {
516 l.Error("adding repo permissions", "error", err.Error())
517 writeError(w, err.Error(), http.StatusInternalServerError)
518 return
519 }
520
521 w.WriteHeader(http.StatusNoContent)
522}
523
524func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
525 l := h.l.With("handler", "AddMember")
526
527 data := struct {
528 Did string `json:"did"`
529 }{}
530
531 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
532 writeError(w, "invalid request body", http.StatusBadRequest)
533 return
534 }
535
536 did := data.Did
537
538 if err := h.db.AddDid(did); err != nil {
539 l.Error("adding did", "error", err.Error())
540 writeError(w, err.Error(), http.StatusInternalServerError)
541 return
542 }
543
544 h.jc.AddDid(did)
545 if err := h.e.AddMember(ThisServer, did); err != nil {
546 l.Error("adding member", "error", err.Error())
547 writeError(w, err.Error(), http.StatusInternalServerError)
548 return
549 }
550
551 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
552 l.Error("fetching and adding keys", "error", err.Error())
553 writeError(w, err.Error(), http.StatusInternalServerError)
554 return
555 }
556
557 w.WriteHeader(http.StatusNoContent)
558}
559
560func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
561 l := h.l.With("handler", "AddRepoCollaborator")
562
563 data := struct {
564 Did string `json:"did"`
565 }{}
566
567 ownerDid := chi.URLParam(r, "did")
568 repo := chi.URLParam(r, "name")
569
570 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
571 writeError(w, "invalid request body", http.StatusBadRequest)
572 return
573 }
574
575 if err := h.db.AddDid(data.Did); err != nil {
576 l.Error("adding did", "error", err.Error())
577 writeError(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580 h.jc.AddDid(data.Did)
581
582 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
583 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
584 l.Error("adding repo collaborator", "error", err.Error())
585 writeError(w, err.Error(), http.StatusInternalServerError)
586 return
587 }
588
589 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
590 l.Error("fetching and adding keys", "error", err.Error())
591 writeError(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
594
595 w.WriteHeader(http.StatusNoContent)
596}
597
598func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
599 l := h.l.With("handler", "Init")
600
601 if h.knotInitialized {
602 writeError(w, "knot already initialized", http.StatusConflict)
603 return
604 }
605
606 data := struct {
607 Did string `json:"did"`
608 }{}
609
610 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
611 l.Error("failed to decode request body", "error", err.Error())
612 writeError(w, "invalid request body", http.StatusBadRequest)
613 return
614 }
615
616 if data.Did == "" {
617 l.Error("empty DID in request", "did", data.Did)
618 writeError(w, "did is empty", http.StatusBadRequest)
619 return
620 }
621
622 if err := h.db.AddDid(data.Did); err != nil {
623 l.Error("failed to add DID", "error", err.Error())
624 writeError(w, err.Error(), http.StatusInternalServerError)
625 return
626 }
627
628 h.jc.UpdateDids([]string{data.Did})
629 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
630 l.Error("adding owner", "error", err.Error())
631 writeError(w, err.Error(), http.StatusInternalServerError)
632 return
633 }
634
635 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
636 l.Error("fetching and adding keys", "error", err.Error())
637 writeError(w, err.Error(), http.StatusInternalServerError)
638 return
639 }
640
641 close(h.init)
642
643 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
644 mac.Write([]byte("ok"))
645 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
646
647 w.WriteHeader(http.StatusNoContent)
648}
649
650func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
651 w.Write([]byte("ok"))
652}