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