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