this repo has no description
1package knotserver
2
3import (
4 "compress/gzip"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "html/template"
12 "log"
13 "net/http"
14 "path/filepath"
15 "strconv"
16 "strings"
17
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)
26
27func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
28 w.Write([]byte("This is a knot, part of the wider Tangle network: https://knots.sh"))
29}
30
31func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
32 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
33
34 gr, err := git.Open(path, "")
35 if err != nil {
36 if errors.Is(err, plumbing.ErrReferenceNotFound) {
37 writeMsg(w, "repo empty")
38 return
39 } else {
40 log.Println(err)
41 notFound(w)
42 return
43 }
44 }
45 commits, err := gr.Commits()
46 if err != nil {
47 writeError(w, err.Error(), http.StatusInternalServerError)
48 log.Println(err)
49 return
50 }
51
52 var readmeContent template.HTML
53 for _, readme := range h.c.Repo.Readme {
54 ext := filepath.Ext(readme)
55 content, _ := gr.FileContent(readme)
56 if len(content) > 0 {
57 switch ext {
58 case ".md", ".mkd", ".markdown":
59 unsafe := blackfriday.Run(
60 []byte(content),
61 blackfriday.WithExtensions(blackfriday.CommonExtensions),
62 )
63 html := sanitize(unsafe)
64 readmeContent = template.HTML(html)
65 default:
66 safe := sanitize([]byte(content))
67 readmeContent = template.HTML(
68 fmt.Sprintf(`<pre>%s</pre>`, safe),
69 )
70 }
71 break
72 }
73 }
74
75 if readmeContent == "" {
76 log.Printf("no readme found for %s", path)
77 }
78
79 mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch)
80 if err != nil {
81 writeError(w, err.Error(), http.StatusInternalServerError)
82 log.Println(err)
83 return
84 }
85
86 if len(commits) >= 3 {
87 commits = commits[:3]
88 }
89 data := make(map[string]any)
90 data["ref"] = mainBranch
91 data["readme"] = readmeContent
92 data["commits"] = commits
93 data["desc"] = getDescription(path)
94
95 writeJSON(w, data)
96 return
97}
98
99func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
100 treePath := chi.URLParam(r, "*")
101 ref := chi.URLParam(r, "ref")
102
103 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
104 gr, err := git.Open(path, ref)
105 if err != nil {
106 notFound(w)
107 return
108 }
109
110 files, err := gr.FileTree(treePath)
111 if err != nil {
112 writeError(w, err.Error(), http.StatusInternalServerError)
113 log.Println(err)
114 return
115 }
116
117 data := make(map[string]any)
118 data["ref"] = ref
119 data["parent"] = treePath
120 data["desc"] = getDescription(path)
121 data["dotdot"] = filepath.Dir(treePath)
122
123 h.listFiles(files, data, w)
124 return
125}
126
127func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) {
128 var raw bool
129 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
130 raw = rawParam
131 }
132
133 treePath := chi.URLParam(r, "*")
134 ref := chi.URLParam(r, "ref")
135
136 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
137 gr, err := git.Open(path, ref)
138 if err != nil {
139 notFound(w)
140 return
141 }
142
143 contents, err := gr.FileContent(treePath)
144 if err != nil {
145 writeError(w, err.Error(), http.StatusInternalServerError)
146 return
147 }
148 data := make(map[string]any)
149 data["ref"] = ref
150 data["desc"] = getDescription(path)
151 data["path"] = treePath
152
153 safe := sanitize([]byte(contents))
154
155 if raw {
156 h.showRaw(string(safe), w)
157 } else {
158 h.showFile(string(safe), data, w)
159 }
160}
161
162func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
163 name := chi.URLParam(r, "name")
164 file := chi.URLParam(r, "file")
165
166 // TODO: extend this to add more files compression (e.g.: xz)
167 if !strings.HasSuffix(file, ".tar.gz") {
168 notFound(w)
169 return
170 }
171
172 ref := strings.TrimSuffix(file, ".tar.gz")
173
174 // This allows the browser to use a proper name for the file when
175 // downloading
176 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
177 setContentDisposition(w, filename)
178 setGZipMIME(w)
179
180 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
181 gr, err := git.Open(path, ref)
182 if err != nil {
183 notFound(w)
184 return
185 }
186
187 gw := gzip.NewWriter(w)
188 defer gw.Close()
189
190 prefix := fmt.Sprintf("%s-%s", name, ref)
191 err = gr.WriteTar(gw, prefix)
192 if err != nil {
193 // once we start writing to the body we can't report error anymore
194 // so we are only left with printing the error.
195 log.Println(err)
196 return
197 }
198
199 err = gw.Flush()
200 if err != nil {
201 // once we start writing to the body we can't report error anymore
202 // so we are only left with printing the error.
203 log.Println(err)
204 return
205 }
206}
207
208func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
209 fmt.Println(r.URL.Path)
210 ref := chi.URLParam(r, "ref")
211
212 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
213 gr, err := git.Open(path, ref)
214 if err != nil {
215 notFound(w)
216 return
217 }
218
219 commits, err := gr.Commits()
220 if err != nil {
221 writeError(w, err.Error(), http.StatusInternalServerError)
222 log.Println(err)
223 return
224 }
225
226 // Get page parameters
227 page := 1
228 pageSize := 30
229
230 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
231 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
232 page = p
233 }
234 }
235
236 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
237 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
238 pageSize = ps
239 }
240 }
241
242 // Calculate pagination
243 start := (page - 1) * pageSize
244 end := start + pageSize
245 total := len(commits)
246
247 if start >= total {
248 commits = []*object.Commit{}
249 } else {
250 if end > total {
251 end = total
252 }
253 commits = commits[start:end]
254 }
255
256 data := make(map[string]interface{})
257 data["commits"] = commits
258 data["ref"] = ref
259 data["desc"] = getDescription(path)
260 data["log"] = true
261 data["total"] = total
262 data["page"] = page
263 data["per_page"] = pageSize
264
265 writeJSON(w, data)
266 return
267}
268
269func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
270 ref := chi.URLParam(r, "ref")
271
272 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
273 gr, err := git.Open(path, ref)
274 if err != nil {
275 notFound(w)
276 return
277 }
278
279 diff, err := gr.Diff()
280 if err != nil {
281 writeError(w, err.Error(), http.StatusInternalServerError)
282 log.Println(err)
283 return
284 }
285
286 data := make(map[string]interface{})
287
288 data["commit"] = diff.Commit
289 data["stat"] = diff.Stat
290 data["diff"] = diff.Diff
291 data["ref"] = ref
292 data["desc"] = getDescription(path)
293
294 writeJSON(w, data)
295 return
296}
297
298func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
299 path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
300 gr, err := git.Open(path, "")
301 if err != nil {
302 notFound(w)
303 return
304 }
305
306 tags, err := gr.Tags()
307 if err != nil {
308 // Non-fatal, we *should* have at least one branch to show.
309 log.Println(err)
310 }
311
312 branches, err := gr.Branches()
313 if err != nil {
314 log.Println(err)
315 writeError(w, err.Error(), http.StatusInternalServerError)
316 return
317 }
318
319 data := make(map[string]interface{})
320
321 data["branches"] = branches
322 data["tags"] = tags
323 data["desc"] = getDescription(path)
324
325 writeJSON(w, data)
326 return
327}
328
329func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
330 switch r.Method {
331 case http.MethodGet:
332 keys, err := h.db.GetAllPublicKeys()
333 if err != nil {
334 writeError(w, err.Error(), http.StatusInternalServerError)
335 log.Println(err)
336 return
337 }
338
339 data := make([]map[string]interface{}, 0)
340 for _, key := range keys {
341 j := key.JSON()
342 data = append(data, j)
343 }
344 writeJSON(w, data)
345 return
346
347 case http.MethodPut:
348 pk := db.PublicKey{}
349 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
350 writeError(w, "invalid request body", http.StatusBadRequest)
351 return
352 }
353
354 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
355 if err != nil {
356 writeError(w, "invalid pubkey", http.StatusBadRequest)
357 }
358
359 if err := h.db.AddPublicKey(pk); err != nil {
360 writeError(w, err.Error(), http.StatusInternalServerError)
361 log.Printf("adding public key: %s", err)
362 return
363 }
364
365 w.WriteHeader(http.StatusNoContent)
366 return
367 }
368}
369
370func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
371 data := struct {
372 DID string `json:"did"`
373 Name string `json:"name"`
374 }{}
375
376 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
377 writeError(w, "invalid request body", http.StatusBadRequest)
378 return
379 }
380
381 did := data.DID
382 name := data.Name
383
384 repoPath := filepath.Join(h.c.Repo.ScanPath, did, name)
385 err := git.InitBare(repoPath)
386 if err != nil {
387 writeError(w, err.Error(), http.StatusInternalServerError)
388 return
389 }
390
391 w.WriteHeader(http.StatusNoContent)
392}
393
394// TODO: make this set the initial user as the owner
395func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
396 if h.knotInitialized {
397 writeError(w, "knot already initialized", http.StatusConflict)
398 return
399 }
400
401 data := struct {
402 DID string `json:"did"`
403 PublicKey string `json:"key"`
404 Created string `json:"created"`
405 }{}
406
407 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
408 writeError(w, "invalid request body", http.StatusBadRequest)
409 return
410 }
411
412 did := data.DID
413 key := data.PublicKey
414 created := data.Created
415
416 if did == "" {
417 writeError(w, "did is empty", http.StatusBadRequest)
418 return
419 }
420
421 if key == "" {
422 writeError(w, "key is empty", http.StatusBadRequest)
423 return
424 }
425
426 if created == "" {
427 writeError(w, "created timestamp is empty", http.StatusBadRequest)
428 return
429 }
430
431 if err := h.db.AddDID(did); err == nil {
432 pk := db.PublicKey{
433 Did: did,
434 }
435 pk.Key = key
436 pk.Created = created
437 err := h.db.AddPublicKey(pk)
438 if err != nil {
439 writeError(w, err.Error(), http.StatusInternalServerError)
440 return
441 }
442 } else {
443 writeError(w, err.Error(), http.StatusInternalServerError)
444 return
445 }
446
447 h.js.UpdateDids([]string{did})
448 // Signal that the knot is ready
449 close(h.init)
450 w.WriteHeader(http.StatusNoContent)
451}
452
453func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
454 log.Println("got health check")
455 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
456 mac.Write([]byte("ok"))
457 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
458
459 w.Write([]byte("ok"))
460}