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