From 75cd1a5bc1ea7eb25a54b397f95b6169a6c99cd0 Mon Sep 17 00:00:00 2001
From: Sergey Matveev <stargrave@stargrave.org>
Date: Mon, 20 Dec 2021 15:45:01 +0300
Subject: [PATCH] Full HTTP Metalink headers

---
 doc/index.texi  |  7 ++---
 handler.go      | 37 ++++++++++++++++++++++----
 meta4/parse.go  | 60 +++++++++++++++++++++++++++++++++++++++++
 meta4/scheme.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 167 insertions(+), 8 deletions(-)
 create mode 100644 meta4/parse.go
 create mode 100644 meta4/scheme.go

diff --git a/doc/index.texi b/doc/index.texi
index cfb612f..750d7f3 100644
--- a/doc/index.texi
+++ b/doc/index.texi
@@ -48,9 +48,10 @@ read-only @url{https://en.wikipedia.org/wiki/WebDAV, WebDAV} support.
 
 @item Per-domain HTTP basic authorization and TLS client authentication.
 
-@item If corresponding @file{.meta4} files are found, then @code{Link}
-header is generated automatically to that
-@url{http://www.metalinker.org/, Metalink}.
+@item If corresponding @file{.meta4} files are found, it is parsed and
+additional @code{Link}s with @code{Digest}s headers are generated
+automatically, based on that @url{http://www.metalinker.org/, Metalink}
+file.
 
 @item Very friendly to @url{http://cr.yp.to/daemontools.html, daemontools}.
 Can drop (UID, GID, groups) privileges.
diff --git a/handler.go b/handler.go
index 52d9a10..7685981 100644
--- a/handler.go
+++ b/handler.go
@@ -20,6 +20,7 @@ package godlighty
 import (
 	"bytes"
 	"compress/gzip"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -35,13 +36,13 @@ import (
 	"time"
 
 	"github.com/klauspost/compress/zstd"
+	"go.stargrave.org/godlighty/meta4"
 	"golang.org/x/net/webdav"
 )
 
 const (
-	Index    = "index.html"
-	Readme   = "README"
-	Meta4Ext = ".meta4"
+	Index  = "index.html"
+	Readme = "README"
 )
 
 var (
@@ -240,9 +241,35 @@ IndexLookup:
 	}
 	defer fd.Close()
 
-	if _, err = os.Stat(pth + Meta4Ext); err == nil {
-		w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
+	if meta4fi, err := os.Stat(pth + meta4.Ext); err == nil {
+		if meta4fi.Size() > meta4.MaxSize {
+			goto SkipMeta4
+		}
+		meta4Raw, err := ioutil.ReadFile(pth + meta4.Ext)
+		if err != nil {
+			goto SkipMeta4
+		}
+		base := path.Base(pth)
+		forHTTP, err := meta4.Parse(base, meta4Raw)
+		if err != nil {
+			goto SkipMeta4
+		}
+		w.Header().Add("Link", "<"+base+meta4.Ext+
+			`>; rel=describedby; type="application/metalink4+xml"`,
+		)
+		for _, u := range forHTTP.URLs {
+			w.Header().Add("Link", "<"+u+">; rel=duplicate")
+		}
+		if forHTTP.SHA256 != nil {
+			w.Header().Add("Digest", "SHA-256="+
+				base64.StdEncoding.EncodeToString(forHTTP.SHA256))
+		}
+		if forHTTP.SHA512 != nil {
+			w.Header().Add("Digest", "SHA-512="+
+				base64.StdEncoding.EncodeToString(forHTTP.SHA512))
+		}
 	}
+SkipMeta4:
 
 	if contentType == "" {
 		contentType = mediaType(path.Base(pth), cfg.MIMEs)
diff --git a/meta4/parse.go b/meta4/parse.go
new file mode 100644
index 0000000..92ff37f
--- /dev/null
+++ b/meta4/parse.go
@@ -0,0 +1,60 @@
+/*
+godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package meta4
+
+import (
+	"encoding/hex"
+	"encoding/xml"
+)
+
+type ForHTTP struct {
+	SHA256 []byte
+	SHA512 []byte
+	URLs   []string
+}
+
+func Parse(fn string, data []byte) (*ForHTTP, error) {
+	var meta Metalink
+	err := xml.Unmarshal(data, &meta)
+	if err != nil {
+		return nil, err
+	}
+	for _, f := range meta.Files {
+		if f.Name != fn {
+			continue
+		}
+		forHTTP := ForHTTP{}
+		for _, h := range f.Hashes {
+			digest, err := hex.DecodeString(h.Hash)
+			if err != nil {
+				return nil, err
+			}
+			switch h.Type {
+			case HashSHA256:
+				forHTTP.SHA256 = digest
+			case HashSHA512:
+				forHTTP.SHA512 = digest
+			}
+		}
+		for _, u := range f.URLs {
+			forHTTP.URLs = append(forHTTP.URLs, u.URL)
+		}
+		return &forHTTP, nil
+	}
+	return nil, nil
+}
diff --git a/meta4/scheme.go b/meta4/scheme.go
new file mode 100644
index 0000000..da46e24
--- /dev/null
+++ b/meta4/scheme.go
@@ -0,0 +1,71 @@
+/*
+godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package meta4
+
+import (
+	"encoding/xml"
+	"time"
+)
+
+const (
+	HashSHA256 = "sha-256"
+	HashSHA512 = "sha-512"
+	Ext        = ".meta4"
+	MaxSize    = 1 << 16
+)
+
+type Metalink struct {
+	XMLName   xml.Name  `xml:"urn:ietf:params:xml:ns:metalink metalink"`
+	Files     []File    `xml:"file"`
+	Generator string    `xml:"generator,,omitempty"`
+	Published time.Time `xml:"published,,omitempty"`
+}
+
+type File struct {
+	XMLName     xml.Name   `xml:"file"`
+	Name        string     `xml:"name,attr"`
+	Description string     `xml:"description,,omitempty"`
+	Hashes      []Hash     `xml:"hash,,omitempty"`
+	MetaURLs    []MetaURL  `xml:"metaurl,,omitempty"`
+	Signature   *Signature `xml:"signature"`
+	Size        uint64     `xml:"size,,omitempty"`
+	URLs        []URL      `xml:"url,,omitempty"`
+}
+
+type URL struct {
+	XMLName xml.Name `xml:"url"`
+	URL     string   `xml:",chardata"`
+}
+
+type Signature struct {
+	XMLName   xml.Name `xml:"signature"`
+	MediaType string   `xml:"mediatype,attr"`
+	Signature string   `xml:",cdata"`
+}
+
+type Hash struct {
+	XMLName xml.Name `xml:"hash"`
+	Type    string   `xml:"type,attr"`
+	Hash    string   `xml:",chardata"`
+}
+
+type MetaURL struct {
+	XMLName   xml.Name `xml:"metaurl"`
+	MediaType string   `xml:"mediatype,attr"`
+	URL       string   `xml:",chardata"`
+}
-- 
2.51.0