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 data := make(map[string]any)
126 data["ref"] = ref
127 data["parent"] = treePath
128 data["desc"] = getDescription(path)
129 data["dotdot"] = filepath.Dir(treePath)
130
131 h.listFiles(files, data, w)
132 return
133}
134
135func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) {
136 var raw bool
137 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
138 raw = rawParam
139 }
140
141 treePath := chi.URLParam(r, "*")
142 ref := chi.URLParam(r, "ref")
143
144 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath)
145
146 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
147 gr, err := git.Open(path, ref)
148 if err != nil {
149 notFound(w)
150 return
151 }
152
153 contents, err := gr.FileContent(treePath)
154 if err != nil {
155 writeError(w, err.Error(), http.StatusInternalServerError)
156 return
157 }
158 data := make(map[string]any)
159 data["ref"] = ref
160 data["desc"] = getDescription(path)
161 data["path"] = treePath
162
163 safe := sanitize([]byte(contents))
164
165 if raw {
166 h.showRaw(string(safe), w)
167 } else {
168 h.showFile(string(safe), data, w, l)
169 }
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 data := make(map[string]interface{})
270 data["commits"] = commits
271 data["ref"] = ref
272 data["desc"] = getDescription(path)
273 data["log"] = true
274 data["total"] = total
275 data["page"] = page
276 data["per_page"] = pageSize
277
278 writeJSON(w, data)
279 return
280}
281
282func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
283 ref := chi.URLParam(r, "ref")
284
285 l := h.l.With("handler", "Diff", "ref", ref)
286
287 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
288 gr, err := git.Open(path, ref)
289 if err != nil {
290 notFound(w)
291 return
292 }
293
294 diff, err := gr.Diff()
295 if err != nil {
296 writeError(w, err.Error(), http.StatusInternalServerError)
297 l.Error("getting diff", "error", err.Error())
298 return
299 }
300
301 data := make(map[string]interface{})
302
303 data["commit"] = diff.Commit
304 data["stat"] = diff.Stat
305 data["diff"] = diff.Diff
306 data["ref"] = ref
307 data["desc"] = getDescription(path)
308
309 writeJSON(w, data)
310 return
311}
312
313func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
314 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
315 l := h.l.With("handler", "Refs")
316
317 gr, err := git.Open(path, "")
318 if err != nil {
319 notFound(w)
320 return
321 }
322
323 tags, err := gr.Tags()
324 if err != nil {
325 // Non-fatal, we *should* have at least one branch to show.
326 l.Error("getting tags", "error", err.Error())
327 }
328
329 branches, err := gr.Branches()
330 if err != nil {
331 l.Error("getting branches", "error", err.Error())
332 writeError(w, err.Error(), http.StatusInternalServerError)
333 return
334 }
335
336 data := make(map[string]interface{})
337
338 data["branches"] = branches
339 data["tags"] = tags
340 data["desc"] = getDescription(path)
341
342 writeJSON(w, data)
343 return
344}
345
346func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
347 l := h.l.With("handler", "Keys")
348
349 switch r.Method {
350 case http.MethodGet:
351 keys, err := h.db.GetAllPublicKeys()
352 if err != nil {
353 writeError(w, err.Error(), http.StatusInternalServerError)
354 l.Error("getting public keys", "error", err.Error())
355 return
356 }
357
358 data := make([]map[string]interface{}, 0)
359 for _, key := range keys {
360 j := key.JSON()
361 data = append(data, j)
362 }
363 writeJSON(w, data)
364 return
365
366 case http.MethodPut:
367 pk := db.PublicKey{}
368 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
369 writeError(w, "invalid request body", http.StatusBadRequest)
370 return
371 }
372
373 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
374 if err != nil {
375 writeError(w, "invalid pubkey", http.StatusBadRequest)
376 }
377
378 if err := h.db.AddPublicKey(pk); err != nil {
379 writeError(w, err.Error(), http.StatusInternalServerError)
380 l.Error("adding public key", "error", err.Error())
381 return
382 }
383
384 w.WriteHeader(http.StatusNoContent)
385 return
386 }
387}
388
389func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
390 l := h.l.With("handler", "NewRepo")
391
392 data := struct {
393 Did string `json:"did"`
394 Name string `json:"name"`
395 }{}
396
397 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
398 writeError(w, "invalid request body", http.StatusBadRequest)
399 return
400 }
401
402 did := data.Did
403 name := data.Name
404
405 relativeRepoPath := filepath.Join(did, name)
406 repoPath := filepath.Join(h.c.Repo.ScanPath, relativeRepoPath)
407 err := git.InitBare(repoPath)
408 if err != nil {
409 l.Error("initializing bare repo", "error", err.Error())
410 writeError(w, err.Error(), http.StatusInternalServerError)
411 return
412 }
413
414 // add perms for this user to access the repo
415 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
416 if err != nil {
417 l.Error("adding repo permissions", "error", err.Error())
418 writeError(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421
422 w.WriteHeader(http.StatusNoContent)
423}
424
425func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
426 l := h.l.With("handler", "AddMember")
427
428 data := struct {
429 Did string `json:"did"`
430 }{}
431
432 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
433 writeError(w, "invalid request body", http.StatusBadRequest)
434 return
435 }
436
437 did := data.Did
438
439 if err := h.db.AddDid(did); err != nil {
440 l.Error("adding did", "error", err.Error())
441 writeError(w, err.Error(), http.StatusInternalServerError)
442 return
443 }
444
445 h.jc.UpdateDids([]string{did})
446 if err := h.e.AddMember(ThisServer, did); err != nil {
447 l.Error("adding member", "error", err.Error())
448 writeError(w, err.Error(), http.StatusInternalServerError)
449 return
450 }
451
452 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
453 l.Error("fetching and adding keys", "error", err.Error())
454 writeError(w, err.Error(), http.StatusInternalServerError)
455 return
456 }
457
458 w.WriteHeader(http.StatusNoContent)
459}
460
461func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
462 l := h.l.With("handler", "AddRepoCollaborator")
463
464 data := struct {
465 Did string `json:"did"`
466 }{}
467
468 ownerDid := chi.URLParam(r, "did")
469 repo := chi.URLParam(r, "name")
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 if err := h.db.AddDid(data.Did); err != nil {
477 l.Error("adding did", "error", err.Error())
478 writeError(w, err.Error(), http.StatusInternalServerError)
479 return
480 }
481 h.jc.UpdateDids([]string{data.Did})
482
483 repoName := filepath.Join(ownerDid, repo)
484 if err := h.e.AddRepo(data.Did, ThisServer, repoName); err != nil {
485 l.Error("adding repo collaborator", "error", err.Error())
486 writeError(w, err.Error(), http.StatusInternalServerError)
487 return
488 }
489
490 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
491 l.Error("fetching and adding keys", "error", err.Error())
492 writeError(w, err.Error(), http.StatusInternalServerError)
493 return
494 }
495
496 w.WriteHeader(http.StatusOK)
497}
498
499func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
500 l := h.l.With("handler", "Init")
501
502 if h.knotInitialized {
503 writeError(w, "knot already initialized", http.StatusConflict)
504 return
505 }
506
507 data := struct {
508 Did string `json:"did"`
509 }{}
510
511 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
512 l.Error("failed to decode request body", "error", err.Error())
513 writeError(w, "invalid request body", http.StatusBadRequest)
514 return
515 }
516
517 if data.Did == "" {
518 l.Error("empty DID in request", "did", data.Did)
519 writeError(w, "did is empty", http.StatusBadRequest)
520 return
521 }
522
523 if err := h.db.AddDid(data.Did); err != nil {
524 l.Error("failed to add DID", "error", err.Error())
525 writeError(w, err.Error(), http.StatusInternalServerError)
526 return
527 }
528
529 h.jc.UpdateDids([]string{data.Did})
530 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
531 l.Error("adding owner", "error", err.Error())
532 writeError(w, err.Error(), http.StatusInternalServerError)
533 return
534 }
535
536 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
537 l.Error("fetching and adding keys", "error", err.Error())
538 writeError(w, err.Error(), http.StatusInternalServerError)
539 return
540 }
541
542 close(h.init)
543
544 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
545 mac.Write([]byte("ok"))
546 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
547
548 w.WriteHeader(http.StatusNoContent)
549}
550
551func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
552 w.Write([]byte("ok"))
553}