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