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