670 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
log.Println("branch", data.DefaultBranch)
509
if data.DefaultBranch == "" {
510
data.DefaultBranch = h.c.Repo.MainBranch
511
}
512
513
did := data.Did
514
name := data.Name
515
defaultBranch := data.DefaultBranch
516
517
relativeRepoPath := filepath.Join(did, name)
518
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
519
err := git.InitBare(repoPath, defaultBranch)
520
if err != nil {
521
l.Error("initializing bare repo", "error", err.Error())
522
if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
523
writeError(w, "That repo already exists!", http.StatusConflict)
524
return
525
} else {
526
writeError(w, err.Error(), http.StatusInternalServerError)
527
return
528
}
529
}
530
531
// add perms for this user to access the repo
532
err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
533
if err != nil {
534
l.Error("adding repo permissions", "error", err.Error())
535
writeError(w, err.Error(), http.StatusInternalServerError)
536
return
537
}
538
539
w.WriteHeader(http.StatusNoContent)
540
}
541
542
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
543
l := h.l.With("handler", "AddMember")
544
545
data := struct {
546
Did string `json:"did"`
547
}{}
548
549
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
550
writeError(w, "invalid request body", http.StatusBadRequest)
551
return
552
}
553
554
did := data.Did
555
556
if err := h.db.AddDid(did); err != nil {
557
l.Error("adding did", "error", err.Error())
558
writeError(w, err.Error(), http.StatusInternalServerError)
559
return
560
}
561
562
h.jc.AddDid(did)
563
if err := h.e.AddMember(ThisServer, did); err != nil {
564
l.Error("adding member", "error", err.Error())
565
writeError(w, err.Error(), http.StatusInternalServerError)
566
return
567
}
568
569
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
570
l.Error("fetching and adding keys", "error", err.Error())
571
writeError(w, err.Error(), http.StatusInternalServerError)
572
return
573
}
574
575
w.WriteHeader(http.StatusNoContent)
576
}
577
578
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
579
l := h.l.With("handler", "AddRepoCollaborator")
580
581
data := struct {
582
Did string `json:"did"`
583
}{}
584
585
ownerDid := chi.URLParam(r, "did")
586
repo := chi.URLParam(r, "name")
587
588
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
589
writeError(w, "invalid request body", http.StatusBadRequest)
590
return
591
}
592
593
if err := h.db.AddDid(data.Did); err != nil {
594
l.Error("adding did", "error", err.Error())
595
writeError(w, err.Error(), http.StatusInternalServerError)
596
return
597
}
598
h.jc.AddDid(data.Did)
599
600
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
601
if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
602
l.Error("adding repo collaborator", "error", err.Error())
603
writeError(w, err.Error(), http.StatusInternalServerError)
604
return
605
}
606
607
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
608
l.Error("fetching and adding keys", "error", err.Error())
609
writeError(w, err.Error(), http.StatusInternalServerError)
610
return
611
}
612
613
w.WriteHeader(http.StatusNoContent)
614
}
615
616
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
617
l := h.l.With("handler", "Init")
618
619
if h.knotInitialized {
620
writeError(w, "knot already initialized", http.StatusConflict)
621
return
622
}
623
624
data := struct {
625
Did string `json:"did"`
626
}{}
627
628
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
629
l.Error("failed to decode request body", "error", err.Error())
630
writeError(w, "invalid request body", http.StatusBadRequest)
631
return
632
}
633
634
if data.Did == "" {
635
l.Error("empty DID in request", "did", data.Did)
636
writeError(w, "did is empty", http.StatusBadRequest)
637
return
638
}
639
640
if err := h.db.AddDid(data.Did); err != nil {
641
l.Error("failed to add DID", "error", err.Error())
642
writeError(w, err.Error(), http.StatusInternalServerError)
643
return
644
}
645
646
h.jc.UpdateDids([]string{data.Did})
647
if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
648
l.Error("adding owner", "error", err.Error())
649
writeError(w, err.Error(), http.StatusInternalServerError)
650
return
651
}
652
653
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
654
l.Error("fetching and adding keys", "error", err.Error())
655
writeError(w, err.Error(), http.StatusInternalServerError)
656
return
657
}
658
659
close(h.init)
660
661
mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
662
mac.Write([]byte("ok"))
663
w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
664
665
w.WriteHeader(http.StatusNoContent)
666
}
667
668
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
669
w.Write([]byte("ok"))
670
}
671