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