forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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 Ref: types.Reference{
346 Name: tag.Name(),
347 Hash: tag.Hash().String(),
348 },
349 Tag: tag.TagObject(),
350 }
351
352 if tag.Message() != "" {
353 tr.Message = tag.Message()
354 }
355
356 rtags = append(rtags, &tr)
357 }
358
359 resp := types.RepoTagsResponse{
360 Tags: rtags,
361 }
362
363 writeJSON(w, resp)
364 return
365}
366
367func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
368 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
369 l := h.l.With("handler", "Branches")
370
371 gr, err := git.Open(path, "")
372 if err != nil {
373 notFound(w)
374 return
375 }
376
377 branches, err := gr.Branches()
378 if err != nil {
379 l.Error("getting branches", "error", err.Error())
380 writeError(w, err.Error(), http.StatusInternalServerError)
381 return
382 }
383
384 bs := []types.Branch{}
385 for _, branch := range branches {
386 b := types.Branch{}
387 b.Hash = branch.Hash().String()
388 b.Name = branch.Name().Short()
389 bs = append(bs, b)
390 }
391
392 resp := types.RepoBranchesResponse{
393 Branches: bs,
394 }
395
396 writeJSON(w, resp)
397 return
398}
399
400func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
401 l := h.l.With("handler", "Keys")
402
403 switch r.Method {
404 case http.MethodGet:
405 keys, err := h.db.GetAllPublicKeys()
406 if err != nil {
407 writeError(w, err.Error(), http.StatusInternalServerError)
408 l.Error("getting public keys", "error", err.Error())
409 return
410 }
411
412 data := make([]map[string]interface{}, 0)
413 for _, key := range keys {
414 j := key.JSON()
415 data = append(data, j)
416 }
417 writeJSON(w, data)
418 return
419
420 case http.MethodPut:
421 pk := db.PublicKey{}
422 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
423 writeError(w, "invalid request body", http.StatusBadRequest)
424 return
425 }
426
427 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
428 if err != nil {
429 writeError(w, "invalid pubkey", http.StatusBadRequest)
430 }
431
432 if err := h.db.AddPublicKey(pk); err != nil {
433 writeError(w, err.Error(), http.StatusInternalServerError)
434 l.Error("adding public key", "error", err.Error())
435 return
436 }
437
438 w.WriteHeader(http.StatusNoContent)
439 return
440 }
441}
442
443func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
444 l := h.l.With("handler", "NewRepo")
445
446 data := struct {
447 Did string `json:"did"`
448 Name string `json:"name"`
449 }{}
450
451 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
452 writeError(w, "invalid request body", http.StatusBadRequest)
453 return
454 }
455
456 did := data.Did
457 name := data.Name
458
459 relativeRepoPath := filepath.Join(did, name)
460 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
461 err := git.InitBare(repoPath)
462 if err != nil {
463 l.Error("initializing bare repo", "error", err.Error())
464 writeError(w, err.Error(), http.StatusInternalServerError)
465 return
466 }
467
468 // add perms for this user to access the repo
469 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
470 if err != nil {
471 l.Error("adding repo permissions", "error", err.Error())
472 writeError(w, err.Error(), http.StatusInternalServerError)
473 return
474 }
475
476 w.WriteHeader(http.StatusNoContent)
477}
478
479func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
480 l := h.l.With("handler", "AddMember")
481
482 data := struct {
483 Did string `json:"did"`
484 }{}
485
486 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
487 writeError(w, "invalid request body", http.StatusBadRequest)
488 return
489 }
490
491 did := data.Did
492
493 if err := h.db.AddDid(did); err != nil {
494 l.Error("adding did", "error", err.Error())
495 writeError(w, err.Error(), http.StatusInternalServerError)
496 return
497 }
498
499 h.jc.AddDid(did)
500 if err := h.e.AddMember(ThisServer, did); err != nil {
501 l.Error("adding member", "error", err.Error())
502 writeError(w, err.Error(), http.StatusInternalServerError)
503 return
504 }
505
506 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
507 l.Error("fetching and adding keys", "error", err.Error())
508 writeError(w, err.Error(), http.StatusInternalServerError)
509 return
510 }
511
512 w.WriteHeader(http.StatusNoContent)
513}
514
515func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
516 l := h.l.With("handler", "AddRepoCollaborator")
517
518 data := struct {
519 Did string `json:"did"`
520 }{}
521
522 ownerDid := chi.URLParam(r, "did")
523 repo := chi.URLParam(r, "name")
524
525 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
526 writeError(w, "invalid request body", http.StatusBadRequest)
527 return
528 }
529
530 if err := h.db.AddDid(data.Did); err != nil {
531 l.Error("adding did", "error", err.Error())
532 writeError(w, err.Error(), http.StatusInternalServerError)
533 return
534 }
535 h.jc.AddDid(data.Did)
536
537 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
538 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
539 l.Error("adding repo collaborator", "error", err.Error())
540 writeError(w, err.Error(), http.StatusInternalServerError)
541 return
542 }
543
544 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
545 l.Error("fetching and adding keys", "error", err.Error())
546 writeError(w, err.Error(), http.StatusInternalServerError)
547 return
548 }
549
550 w.WriteHeader(http.StatusNoContent)
551}
552
553func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
554 l := h.l.With("handler", "Init")
555
556 if h.knotInitialized {
557 writeError(w, "knot already initialized", http.StatusConflict)
558 return
559 }
560
561 data := struct {
562 Did string `json:"did"`
563 }{}
564
565 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
566 l.Error("failed to decode request body", "error", err.Error())
567 writeError(w, "invalid request body", http.StatusBadRequest)
568 return
569 }
570
571 if data.Did == "" {
572 l.Error("empty DID in request", "did", data.Did)
573 writeError(w, "did is empty", http.StatusBadRequest)
574 return
575 }
576
577 if err := h.db.AddDid(data.Did); err != nil {
578 l.Error("failed to add DID", "error", err.Error())
579 writeError(w, err.Error(), http.StatusInternalServerError)
580 return
581 }
582
583 h.jc.UpdateDids([]string{data.Did})
584 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
585 l.Error("adding owner", "error", err.Error())
586 writeError(w, err.Error(), http.StatusInternalServerError)
587 return
588 }
589
590 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
591 l.Error("fetching and adding keys", "error", err.Error())
592 writeError(w, err.Error(), http.StatusInternalServerError)
593 return
594 }
595
596 close(h.init)
597
598 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
599 mac.Write([]byte("ok"))
600 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
601
602 w.WriteHeader(http.StatusNoContent)
603}
604
605func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
606 w.Write([]byte("ok"))
607}