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