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