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