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