268 lines
5.3 KiB
Go
268 lines
5.3 KiB
Go
![]() |
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/ed25519"
|
||
|
"encoding/base32"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"slices"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
restful "github.com/emicklei/go-restful"
|
||
|
"github.com/pierrec/lz4"
|
||
|
"m.cluseau.fr/go/httperr"
|
||
|
)
|
||
|
|
||
|
type DownloadSet struct {
|
||
|
Expiry time.Time
|
||
|
Items []DownloadSetItem
|
||
|
}
|
||
|
|
||
|
func (s DownloadSet) Contains(kind, name, asset string) bool {
|
||
|
for _, item := range s.Items {
|
||
|
if item.Kind == kind && item.Name == name &&
|
||
|
slices.Contains(item.Assets, asset) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (s DownloadSet) Encode() string {
|
||
|
buf := new(strings.Builder)
|
||
|
s.EncodeTo(buf)
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
func (s DownloadSet) EncodeTo(buf *strings.Builder) {
|
||
|
buf.WriteString(strconv.FormatInt(s.Expiry.Unix(), 16))
|
||
|
|
||
|
for _, item := range s.Items {
|
||
|
buf.WriteByte('|')
|
||
|
item.EncodeTo(buf)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *DownloadSet) Decode(encoded string) (err error) {
|
||
|
exp, rem, _ := strings.Cut(encoded, "|")
|
||
|
|
||
|
expUnix, err := strconv.ParseInt(exp, 16, 64)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
s.Expiry = time.Unix(expUnix, 0)
|
||
|
|
||
|
if rem == "" {
|
||
|
s.Items = nil
|
||
|
} else {
|
||
|
itemStrs := strings.Split(rem, "|")
|
||
|
s.Items = make([]DownloadSetItem, len(itemStrs))
|
||
|
for i, itemStr := range itemStrs {
|
||
|
s.Items[i].Decode(itemStr)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
type DownloadSetItem struct {
|
||
|
Kind string
|
||
|
Name string
|
||
|
Assets []string
|
||
|
}
|
||
|
|
||
|
func (i DownloadSetItem) EncodeTo(buf *strings.Builder) {
|
||
|
buf.WriteString(i.Kind)
|
||
|
buf.WriteByte(':')
|
||
|
buf.WriteString(i.Name)
|
||
|
|
||
|
for _, asset := range i.Assets {
|
||
|
buf.WriteByte(':')
|
||
|
buf.WriteString(asset)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (i *DownloadSetItem) Decode(encoded string) {
|
||
|
rem := encoded
|
||
|
i.Kind, rem, _ = strings.Cut(rem, ":")
|
||
|
i.Name, rem, _ = strings.Cut(rem, ":")
|
||
|
|
||
|
if rem == "" {
|
||
|
i.Assets = nil
|
||
|
} else {
|
||
|
i.Assets = strings.Split(rem, ":")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type DownloadSetReq struct {
|
||
|
Expiry string
|
||
|
Items []DownloadSetItem
|
||
|
}
|
||
|
|
||
|
func wsSignDownloadSet(req *restful.Request, resp *restful.Response) {
|
||
|
setReq := DownloadSetReq{}
|
||
|
if err := req.ReadEntity(&setReq); err != nil {
|
||
|
wsError(resp, err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
exp, err := parseCertDuration(setReq.Expiry, time.Now())
|
||
|
if err != nil {
|
||
|
wsError(resp, err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
set := DownloadSet{
|
||
|
Expiry: exp,
|
||
|
Items: setReq.Items,
|
||
|
}
|
||
|
|
||
|
buf := new(bytes.Buffer)
|
||
|
{
|
||
|
setBytes := []byte(set.Encode())
|
||
|
|
||
|
w := lz4.NewWriter(buf)
|
||
|
w.Write(setBytes)
|
||
|
w.Close()
|
||
|
}
|
||
|
|
||
|
setBytes := buf.Bytes()
|
||
|
|
||
|
privkey, pubkey := dlsSigningKeys()
|
||
|
sig := ed25519.Sign(privkey, setBytes)
|
||
|
|
||
|
if !ed25519.Verify(pubkey, setBytes, sig) {
|
||
|
wsError(resp, fmt.Errorf("signature self-check failed"))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
buf = bytes.NewBuffer(make([]byte, 0, 1+len(sig)+len(setBytes)))
|
||
|
buf.WriteByte(byte(len(sig)))
|
||
|
buf.Write(sig)
|
||
|
buf.Write(setBytes)
|
||
|
|
||
|
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
|
||
|
resp.WriteEntity(enc.EncodeToString(buf.Bytes()))
|
||
|
}
|
||
|
|
||
|
func getDlSet(req *restful.Request) (*DownloadSet, *httperr.Error) {
|
||
|
setStr := req.QueryParameter("set")
|
||
|
|
||
|
setBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(setStr)
|
||
|
if err != nil {
|
||
|
err := httperr.BadRequest("invalid set")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
if len(setBytes) == 0 {
|
||
|
err := httperr.BadRequest("invalid set")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
sigLen := int(setBytes[0])
|
||
|
setBytes = setBytes[1:]
|
||
|
|
||
|
if len(setBytes) < sigLen {
|
||
|
err := httperr.BadRequest("invalid set")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
sig := setBytes[:sigLen]
|
||
|
setBytes = setBytes[sigLen:]
|
||
|
|
||
|
_, pubkey := dlsSigningKeys()
|
||
|
if !ed25519.Verify(pubkey, setBytes, sig) {
|
||
|
err := httperr.BadRequest("invalid signature")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
setBytes, err = io.ReadAll(lz4.NewReader(bytes.NewBuffer(setBytes)))
|
||
|
if err != nil {
|
||
|
err := httperr.BadRequest("invalid data")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
fmt.Println(string(setBytes))
|
||
|
|
||
|
set := DownloadSet{}
|
||
|
if err := set.Decode(string(setBytes)); err != nil {
|
||
|
err := httperr.BadRequest("invalid set: " + err.Error())
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
if time.Now().After(set.Expiry) {
|
||
|
err := httperr.BadRequest("set expired")
|
||
|
return nil, &err
|
||
|
}
|
||
|
|
||
|
return &set, nil
|
||
|
}
|
||
|
|
||
|
func wsDownloadSetAsset(req *restful.Request, resp *restful.Response) {
|
||
|
set, err := getDlSet(req)
|
||
|
if err != nil {
|
||
|
wsError(resp, *err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
kind := req.PathParameter("kind")
|
||
|
name := req.PathParameter("name")
|
||
|
asset := req.PathParameter("asset")
|
||
|
|
||
|
if !set.Contains(kind, name, asset) {
|
||
|
wsNotFound(resp)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
downloadAsset(req, resp, kind, name, asset)
|
||
|
}
|
||
|
|
||
|
func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
||
|
setStr := req.QueryParameter("set")
|
||
|
set, err := getDlSet(req)
|
||
|
if err != nil {
|
||
|
resp.WriteHeader(err.Status)
|
||
|
resp.Write([]byte(`<!doctype html>
|
||
|
<html>
|
||
|
<head>
|
||
|
<title>` + err.Error() + `</title>
|
||
|
<style>
|
||
|
@import url('/ui/style.css');
|
||
|
@import url('/ui/app.css');
|
||
|
</style>
|
||
|
</head>
|
||
|
<body><h1>` + err.Error() + `</h1></body>
|
||
|
</html>`))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
buf := new(bytes.Buffer)
|
||
|
buf.WriteString(`<!doctype html>
|
||
|
<html>
|
||
|
<head>
|
||
|
<title>Download set</title>
|
||
|
<style>
|
||
|
@import url('/ui/style.css');
|
||
|
@import url('/ui/app.css');
|
||
|
</style>
|
||
|
</head>
|
||
|
<body><h1>Download set</h1>
|
||
|
`)
|
||
|
|
||
|
for _, item := range set.Items {
|
||
|
fmt.Fprintf(buf, "<h2>%s %s</h2>", strings.Title(item.Kind), item.Name)
|
||
|
fmt.Fprintf(buf, "<p class=\"download-links\">\n")
|
||
|
for _, asset := range item.Assets {
|
||
|
fmt.Fprintf(buf, " <a href=\"/public/download-set/%s/%s/%s?set=%s\" download>%s</a>\n", item.Kind, item.Name, asset, setStr, asset)
|
||
|
}
|
||
|
fmt.Fprintf(buf, `</p>`)
|
||
|
}
|
||
|
|
||
|
buf.WriteString("</body></html>")
|
||
|
buf.WriteTo(resp)
|
||
|
}
|