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