607 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
Ref: types.Reference{
346
Name: tag.Name(),
347
Hash: tag.Hash().String(),
348
},
349
Tag: tag.TagObject(),
350
}
351
352
if tag.Message() != "" {
353
tr.Message = tag.Message()
354
}
355
356
rtags = append(rtags, &tr)
357
}
358
359
resp := types.RepoTagsResponse{
360
Tags: rtags,
361
}
362
363
writeJSON(w, resp)
364
return
365
}
366
367
func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
368
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
369
l := h.l.With("handler", "Branches")
370
371
gr, err := git.Open(path, "")
372
if err != nil {
373
notFound(w)
374
return
375
}
376
377
branches, err := gr.Branches()
378
if err != nil {
379
l.Error("getting branches", "error", err.Error())
380
writeError(w, err.Error(), http.StatusInternalServerError)
381
return
382
}
383
384
bs := []types.Branch{}
385
for _, branch := range branches {
386
b := types.Branch{}
387
b.Hash = branch.Hash().String()
388
b.Name = branch.Name().Short()
389
bs = append(bs, b)
390
}
391
392
resp := types.RepoBranchesResponse{
393
Branches: bs,
394
}
395
396
writeJSON(w, resp)
397
return
398
}
399
400
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
401
l := h.l.With("handler", "Keys")
402
403
switch r.Method {
404
case http.MethodGet:
405
keys, err := h.db.GetAllPublicKeys()
406
if err != nil {
407
writeError(w, err.Error(), http.StatusInternalServerError)
408
l.Error("getting public keys", "error", err.Error())
409
return
410
}
411
412
data := make([]map[string]interface{}, 0)
413
for _, key := range keys {
414
j := key.JSON()
415
data = append(data, j)
416
}
417
writeJSON(w, data)
418
return
419
420
case http.MethodPut:
421
pk := db.PublicKey{}
422
if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
423
writeError(w, "invalid request body", http.StatusBadRequest)
424
return
425
}
426
427
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
428
if err != nil {
429
writeError(w, "invalid pubkey", http.StatusBadRequest)
430
}
431
432
if err := h.db.AddPublicKey(pk); err != nil {
433
writeError(w, err.Error(), http.StatusInternalServerError)
434
l.Error("adding public key", "error", err.Error())
435
return
436
}
437
438
w.WriteHeader(http.StatusNoContent)
439
return
440
}
441
}
442
443
func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
444
l := h.l.With("handler", "NewRepo")
445
446
data := struct {
447
Did string `json:"did"`
448
Name string `json:"name"`
449
}{}
450
451
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
452
writeError(w, "invalid request body", http.StatusBadRequest)
453
return
454
}
455
456
did := data.Did
457
name := data.Name
458
459
relativeRepoPath := filepath.Join(did, name)
460
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
461
err := git.InitBare(repoPath)
462
if err != nil {
463
l.Error("initializing bare repo", "error", err.Error())
464
writeError(w, err.Error(), http.StatusInternalServerError)
465
return
466
}
467
468
// add perms for this user to access the repo
469
err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
470
if err != nil {
471
l.Error("adding repo permissions", "error", err.Error())
472
writeError(w, err.Error(), http.StatusInternalServerError)
473
return
474
}
475
476
w.WriteHeader(http.StatusNoContent)
477
}
478
479
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
480
l := h.l.With("handler", "AddMember")
481
482
data := struct {
483
Did string `json:"did"`
484
}{}
485
486
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
487
writeError(w, "invalid request body", http.StatusBadRequest)
488
return
489
}
490
491
did := data.Did
492
493
if err := h.db.AddDid(did); err != nil {
494
l.Error("adding did", "error", err.Error())
495
writeError(w, err.Error(), http.StatusInternalServerError)
496
return
497
}
498
499
h.jc.AddDid(did)
500
if err := h.e.AddMember(ThisServer, did); err != nil {
501
l.Error("adding member", "error", err.Error())
502
writeError(w, err.Error(), http.StatusInternalServerError)
503
return
504
}
505
506
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
507
l.Error("fetching and adding keys", "error", err.Error())
508
writeError(w, err.Error(), http.StatusInternalServerError)
509
return
510
}
511
512
w.WriteHeader(http.StatusNoContent)
513
}
514
515
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
516
l := h.l.With("handler", "AddRepoCollaborator")
517
518
data := struct {
519
Did string `json:"did"`
520
}{}
521
522
ownerDid := chi.URLParam(r, "did")
523
repo := chi.URLParam(r, "name")
524
525
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
526
writeError(w, "invalid request body", http.StatusBadRequest)
527
return
528
}
529
530
if err := h.db.AddDid(data.Did); err != nil {
531
l.Error("adding did", "error", err.Error())
532
writeError(w, err.Error(), http.StatusInternalServerError)
533
return
534
}
535
h.jc.AddDid(data.Did)
536
537
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
538
if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
539
l.Error("adding repo collaborator", "error", err.Error())
540
writeError(w, err.Error(), http.StatusInternalServerError)
541
return
542
}
543
544
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
545
l.Error("fetching and adding keys", "error", err.Error())
546
writeError(w, err.Error(), http.StatusInternalServerError)
547
return
548
}
549
550
w.WriteHeader(http.StatusNoContent)
551
}
552
553
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
554
l := h.l.With("handler", "Init")
555
556
if h.knotInitialized {
557
writeError(w, "knot already initialized", http.StatusConflict)
558
return
559
}
560
561
data := struct {
562
Did string `json:"did"`
563
}{}
564
565
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
566
l.Error("failed to decode request body", "error", err.Error())
567
writeError(w, "invalid request body", http.StatusBadRequest)
568
return
569
}
570
571
if data.Did == "" {
572
l.Error("empty DID in request", "did", data.Did)
573
writeError(w, "did is empty", http.StatusBadRequest)
574
return
575
}
576
577
if err := h.db.AddDid(data.Did); err != nil {
578
l.Error("failed to add DID", "error", err.Error())
579
writeError(w, err.Error(), http.StatusInternalServerError)
580
return
581
}
582
583
h.jc.UpdateDids([]string{data.Did})
584
if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
585
l.Error("adding owner", "error", err.Error())
586
writeError(w, err.Error(), http.StatusInternalServerError)
587
return
588
}
589
590
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
591
l.Error("fetching and adding keys", "error", err.Error())
592
writeError(w, err.Error(), http.StatusInternalServerError)
593
return
594
}
595
596
close(h.init)
597
598
mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
599
mac.Write([]byte("ok"))
600
w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
601
602
w.WriteHeader(http.StatusNoContent)
603
}
604
605
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
606
w.Write([]byte("ok"))
607
}
608