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