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