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