656 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
safe := string(sanitize([]byte(contents)))
219
220
resp := types.RepoBlobResponse{
221
Ref: ref,
222
Contents: string(safe),
223
Path: treePath,
224
IsBinary: isBinaryFile,
225
}
226
227
h.showFile(resp, w, l)
228
}
229
230
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
231
name := chi.URLParam(r, "name")
232
file := chi.URLParam(r, "file")
233
234
l := h.l.With("handler", "Archive", "name", name, "file", file)
235
236
// TODO: extend this to add more files compression (e.g.: xz)
237
if !strings.HasSuffix(file, ".tar.gz") {
238
notFound(w)
239
return
240
}
241
242
ref := strings.TrimSuffix(file, ".tar.gz")
243
244
// This allows the browser to use a proper name for the file when
245
// downloading
246
filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
247
setContentDisposition(w, filename)
248
setGZipMIME(w)
249
250
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
251
gr, err := git.Open(path, ref)
252
if err != nil {
253
notFound(w)
254
return
255
}
256
257
gw := gzip.NewWriter(w)
258
defer gw.Close()
259
260
prefix := fmt.Sprintf("%s-%s", name, ref)
261
err = gr.WriteTar(gw, prefix)
262
if err != nil {
263
// once we start writing to the body we can't report error anymore
264
// so we are only left with printing the error.
265
l.Error("writing tar file", "error", err.Error())
266
return
267
}
268
269
err = gw.Flush()
270
if err != nil {
271
// once we start writing to the body we can't report error anymore
272
// so we are only left with printing the error.
273
l.Error("flushing?", "error", err.Error())
274
return
275
}
276
}
277
278
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
279
ref := chi.URLParam(r, "ref")
280
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
281
282
l := h.l.With("handler", "Log", "ref", ref, "path", path)
283
284
gr, err := git.Open(path, ref)
285
if err != nil {
286
notFound(w)
287
return
288
}
289
290
commits, err := gr.Commits()
291
if err != nil {
292
writeError(w, err.Error(), http.StatusInternalServerError)
293
l.Error("fetching commits", "error", err.Error())
294
return
295
}
296
297
// Get page parameters
298
page := 1
299
pageSize := 30
300
301
if pageParam := r.URL.Query().Get("page"); pageParam != "" {
302
if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
303
page = p
304
}
305
}
306
307
if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
308
if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
309
pageSize = ps
310
}
311
}
312
313
// Calculate pagination
314
start := (page - 1) * pageSize
315
end := start + pageSize
316
total := len(commits)
317
318
if start >= total {
319
commits = []*object.Commit{}
320
} else {
321
if end > total {
322
end = total
323
}
324
commits = commits[start:end]
325
}
326
327
resp := types.RepoLogResponse{
328
Commits: commits,
329
Ref: ref,
330
Description: getDescription(path),
331
Log: true,
332
Total: total,
333
Page: page,
334
PerPage: pageSize,
335
}
336
337
writeJSON(w, resp)
338
return
339
}
340
341
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
342
ref := chi.URLParam(r, "ref")
343
344
l := h.l.With("handler", "Diff", "ref", ref)
345
346
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
347
gr, err := git.Open(path, ref)
348
if err != nil {
349
notFound(w)
350
return
351
}
352
353
diff, err := gr.Diff()
354
if err != nil {
355
writeError(w, err.Error(), http.StatusInternalServerError)
356
l.Error("getting diff", "error", err.Error())
357
return
358
}
359
360
resp := types.RepoCommitResponse{
361
Ref: ref,
362
Diff: diff,
363
}
364
365
writeJSON(w, resp)
366
return
367
}
368
369
func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
370
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
371
l := h.l.With("handler", "Refs")
372
373
gr, err := git.Open(path, "")
374
if err != nil {
375
notFound(w)
376
return
377
}
378
379
tags, err := gr.Tags()
380
if err != nil {
381
// Non-fatal, we *should* have at least one branch to show.
382
l.Warn("getting tags", "error", err.Error())
383
}
384
385
rtags := []*types.TagReference{}
386
for _, tag := range tags {
387
tr := types.TagReference{
388
Tag: tag.TagObject(),
389
}
390
391
tr.Reference = types.Reference{
392
Name: tag.Name(),
393
Hash: tag.Hash().String(),
394
}
395
396
if tag.Message() != "" {
397
tr.Message = tag.Message()
398
}
399
400
rtags = append(rtags, &tr)
401
}
402
403
resp := types.RepoTagsResponse{
404
Tags: rtags,
405
}
406
407
writeJSON(w, resp)
408
return
409
}
410
411
func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
412
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
413
l := h.l.With("handler", "Branches")
414
415
gr, err := git.Open(path, "")
416
if err != nil {
417
notFound(w)
418
return
419
}
420
421
branches, err := gr.Branches()
422
if err != nil {
423
l.Error("getting branches", "error", err.Error())
424
writeError(w, err.Error(), http.StatusInternalServerError)
425
return
426
}
427
428
bs := []types.Branch{}
429
for _, branch := range branches {
430
b := types.Branch{}
431
b.Hash = branch.Hash().String()
432
b.Name = branch.Name().Short()
433
bs = append(bs, b)
434
}
435
436
resp := types.RepoBranchesResponse{
437
Branches: bs,
438
}
439
440
writeJSON(w, resp)
441
return
442
}
443
444
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
445
l := h.l.With("handler", "Keys")
446
447
switch r.Method {
448
case http.MethodGet:
449
keys, err := h.db.GetAllPublicKeys()
450
if err != nil {
451
writeError(w, err.Error(), http.StatusInternalServerError)
452
l.Error("getting public keys", "error", err.Error())
453
return
454
}
455
456
data := make([]map[string]interface{}, 0)
457
for _, key := range keys {
458
j := key.JSON()
459
data = append(data, j)
460
}
461
writeJSON(w, data)
462
return
463
464
case http.MethodPut:
465
pk := db.PublicKey{}
466
if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
467
writeError(w, "invalid request body", http.StatusBadRequest)
468
return
469
}
470
471
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
472
if err != nil {
473
writeError(w, "invalid pubkey", http.StatusBadRequest)
474
}
475
476
if err := h.db.AddPublicKey(pk); err != nil {
477
writeError(w, err.Error(), http.StatusInternalServerError)
478
l.Error("adding public key", "error", err.Error())
479
return
480
}
481
482
w.WriteHeader(http.StatusNoContent)
483
return
484
}
485
}
486
487
func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
488
l := h.l.With("handler", "NewRepo")
489
490
data := struct {
491
Did string `json:"did"`
492
Name string `json:"name"`
493
}{}
494
495
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
496
writeError(w, "invalid request body", http.StatusBadRequest)
497
return
498
}
499
500
did := data.Did
501
name := data.Name
502
503
relativeRepoPath := filepath.Join(did, name)
504
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
505
err := git.InitBare(repoPath)
506
if err != nil {
507
l.Error("initializing bare repo", "error", err.Error())
508
if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
509
writeError(w, "That repo already exists!", http.StatusConflict)
510
return
511
} else {
512
writeError(w, err.Error(), http.StatusInternalServerError)
513
return
514
}
515
}
516
517
// add perms for this user to access the repo
518
err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
519
if err != nil {
520
l.Error("adding repo permissions", "error", err.Error())
521
writeError(w, err.Error(), http.StatusInternalServerError)
522
return
523
}
524
525
w.WriteHeader(http.StatusNoContent)
526
}
527
528
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
529
l := h.l.With("handler", "AddMember")
530
531
data := struct {
532
Did string `json:"did"`
533
}{}
534
535
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
536
writeError(w, "invalid request body", http.StatusBadRequest)
537
return
538
}
539
540
did := data.Did
541
542
if err := h.db.AddDid(did); err != nil {
543
l.Error("adding did", "error", err.Error())
544
writeError(w, err.Error(), http.StatusInternalServerError)
545
return
546
}
547
548
h.jc.AddDid(did)
549
if err := h.e.AddMember(ThisServer, did); err != nil {
550
l.Error("adding member", "error", err.Error())
551
writeError(w, err.Error(), http.StatusInternalServerError)
552
return
553
}
554
555
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
556
l.Error("fetching and adding keys", "error", err.Error())
557
writeError(w, err.Error(), http.StatusInternalServerError)
558
return
559
}
560
561
w.WriteHeader(http.StatusNoContent)
562
}
563
564
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
565
l := h.l.With("handler", "AddRepoCollaborator")
566
567
data := struct {
568
Did string `json:"did"`
569
}{}
570
571
ownerDid := chi.URLParam(r, "did")
572
repo := chi.URLParam(r, "name")
573
574
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
575
writeError(w, "invalid request body", http.StatusBadRequest)
576
return
577
}
578
579
if err := h.db.AddDid(data.Did); err != nil {
580
l.Error("adding did", "error", err.Error())
581
writeError(w, err.Error(), http.StatusInternalServerError)
582
return
583
}
584
h.jc.AddDid(data.Did)
585
586
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
587
if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
588
l.Error("adding repo collaborator", "error", err.Error())
589
writeError(w, err.Error(), http.StatusInternalServerError)
590
return
591
}
592
593
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
594
l.Error("fetching and adding keys", "error", err.Error())
595
writeError(w, err.Error(), http.StatusInternalServerError)
596
return
597
}
598
599
w.WriteHeader(http.StatusNoContent)
600
}
601
602
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
603
l := h.l.With("handler", "Init")
604
605
if h.knotInitialized {
606
writeError(w, "knot already initialized", http.StatusConflict)
607
return
608
}
609
610
data := struct {
611
Did string `json:"did"`
612
}{}
613
614
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
615
l.Error("failed to decode request body", "error", err.Error())
616
writeError(w, "invalid request body", http.StatusBadRequest)
617
return
618
}
619
620
if data.Did == "" {
621
l.Error("empty DID in request", "did", data.Did)
622
writeError(w, "did is empty", http.StatusBadRequest)
623
return
624
}
625
626
if err := h.db.AddDid(data.Did); err != nil {
627
l.Error("failed to add DID", "error", err.Error())
628
writeError(w, err.Error(), http.StatusInternalServerError)
629
return
630
}
631
632
h.jc.UpdateDids([]string{data.Did})
633
if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
634
l.Error("adding owner", "error", err.Error())
635
writeError(w, err.Error(), http.StatusInternalServerError)
636
return
637
}
638
639
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
640
l.Error("fetching and adding keys", "error", err.Error())
641
writeError(w, err.Error(), http.StatusInternalServerError)
642
return
643
}
644
645
close(h.init)
646
647
mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
648
mac.Write([]byte("ok"))
649
w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
650
651
w.WriteHeader(http.StatusNoContent)
652
}
653
654
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
655
w.Write([]byte("ok"))
656
}
657