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