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