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