Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions http/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
FILESTATS
MIRRORSTATS
CHECKSUM
METALINK

UNDEFINED SecureOption = iota
WITHTLS
Expand All @@ -40,6 +41,7 @@ type Context struct {
isMirrorStats bool
isFileStats bool
isChecksum bool
isMetalink bool
isPretty bool
secureOption SecureOption
}
Expand All @@ -60,6 +62,9 @@ func NewContext(w http.ResponseWriter, r *http.Request, t Templates) *Context {
} else if c.paramBool("md5") || c.paramBool("sha1") || c.paramBool("sha256") {
c.typ = CHECKSUM
c.isChecksum = true
} else if c.paramBool("meta4") || strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/metalink4+xml") {
c.typ = METALINK
c.isMetalink = true
} else {
c.typ = STANDARD
}
Expand Down Expand Up @@ -129,6 +134,11 @@ func (c *Context) IsChecksum() bool {
return c.isChecksum
}

// IsMetalink returns true if a Metalink 4 (RFC 5854) document has been requested
func (c *Context) IsMetalink() bool {
return c.isMetalink
}

// IsPretty returns true if the pretty json has been requested
func (c *Context) IsPretty() bool {
return c.isPretty
Expand Down
4 changes: 4 additions & 0 deletions http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ func (h *HTTP) requestDispatcher(w http.ResponseWriter, r *http.Request) {
switch ctx.Type() {
case MIRRORLIST:
fallthrough
case METALINK:
fallthrough
case STANDARD:
h.mirrorHandler(w, r, ctx)
case MIRRORSTATS:
Expand Down Expand Up @@ -318,6 +320,8 @@ func (h *HTTP) mirrorHandler(w http.ResponseWriter, r *http.Request, ctx *Contex

if ctx.IsMirrorlist() {
resultRenderer = &MirrorListRenderer{}
} else if ctx.IsMetalink() {
resultRenderer = &MetalinkRenderer{}
} else {
switch GetConfig().OutputMode {
case "json":
Expand Down
107 changes: 107 additions & 0 deletions http/pagerenderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ package http
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

. "github.com/etix/mirrorbits/config"
"github.com/etix/mirrorbits/core"
"github.com/etix/mirrorbits/mirrors"
)

Expand Down Expand Up @@ -100,6 +104,109 @@ func (w *RedirectRenderer) Write(ctx *Context, results *mirrors.Results) (status
return http.StatusNotFound, nil
}

// Metalink 4.0 (RFC 5854) document structures. The XML namespace on the root
// element produces the required xmlns="urn:ietf:params:xml:ns:metalink".
type metalink struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:metalink metalink"`
Generator string `xml:"generator,omitempty"`
Published string `xml:"published,omitempty"`
Files []metalinkFile `xml:"file"`
}

type metalinkFile struct {
Name string `xml:"name,attr"`
Size int64 `xml:"size,omitempty"`
Hashes []metalinkHash `xml:"hash,omitempty"`
URLs []metalinkURL `xml:"url"`
}

type metalinkHash struct {
Type string `xml:"type,attr"`
Value string `xml:",chardata"`
}

type metalinkURL struct {
Location string `xml:"location,attr,omitempty"`
Priority int `xml:"priority,attr,omitempty"`
Value string `xml:",chardata"`
}

// MetalinkRenderer renders a Metalink 4.0 (RFC 5854) document listing the
// candidate mirrors for the requested file, ordered by preference, so that the
// client (aria2, wget2, ...) can perform the final mirror selection and
// failover itself instead of being redirected to a single mirror.
type MetalinkRenderer struct{}

// Type returns the type of renderer
func (w *MetalinkRenderer) Type() string {
return "METALINK"
}

// Write is used to write the result to the ResponseWriter
func (w *MetalinkRenderer) Write(ctx *Context, results *mirrors.Results) (statusCode int, err error) {
if len(results.MirrorList) == 0 {
// No mirror returned for this request
http.NotFound(ctx.ResponseWriter(), ctx.Request())
return http.StatusNotFound, nil
}

// Path of the file relative to the repository root (no leading slash),
// used to build the per-mirror URLs.
path := strings.TrimPrefix(results.FileInfo.Path, "/")

file := metalinkFile{
// The "name" is the file basename, not the full path. Metalink clients
// (e.g. librepo) may match it by exact string equality, and it is also
// what aria2 uses as the output filename.
Name: filepath.Base(path),
Size: results.FileInfo.Size,
}

// Source file hashes, identical across all mirrors. Hash type names follow
// the IANA registry as required by RFC 5854.
if results.FileInfo.Sha256 != "" {
file.Hashes = append(file.Hashes, metalinkHash{Type: "sha-256", Value: results.FileInfo.Sha256})
}
if results.FileInfo.Sha1 != "" {
file.Hashes = append(file.Hashes, metalinkHash{Type: "sha-1", Value: results.FileInfo.Sha1})
}
if results.FileInfo.Md5 != "" {
file.Hashes = append(file.Hashes, metalinkHash{Type: "md5", Value: results.FileInfo.Md5})
}

// Candidate mirrors, already ordered by preference by the selection engine.
// priority 1 is the most preferred (RFC 5854 §4.1.6).
for i, m := range results.MirrorList {
var location string
if len(m.CountryFields) > 0 {
location = strings.ToLower(m.CountryFields[0])
}
file.URLs = append(file.URLs, metalinkURL{
Location: location,
Priority: i + 1,
Value: m.AbsoluteURL + path,
})
}

doc := metalink{
Generator: "mirrorbits/" + core.VERSION,
Published: time.Now().UTC().Format(time.RFC3339),
Files: []metalinkFile{file},
}

output, err := xml.MarshalIndent(doc, "", " ")
if err != nil {
return http.StatusInternalServerError, err
}

ctx.ResponseWriter().Header().Set("Content-Type", "application/metalink4+xml; charset=utf-8")
ctx.ResponseWriter().Header().Set("Content-Length", strconv.Itoa(len(xml.Header)+len(output)))
ctx.ResponseWriter().Write([]byte(xml.Header))
ctx.ResponseWriter().Write(output)

return http.StatusOK, nil
}

// MirrorListRenderer is used to render the mirrorlist page using the HTML templates
type MirrorListRenderer struct{}

Expand Down
8 changes: 5 additions & 3 deletions http/selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ func (h DefaultEngine) Selection(ctx *Context, cache *mirrors.Cache, fileInfo *f
mlist[i], mlist[j] = mlist[j], mlist[i]
}

// Shortcut
if !ctx.IsMirrorlist() {
// Shortcut: the redirect/json path only needs a handful of mirrors,
// but mirrorlist and metalink want the full candidate list so the
// client can fail over across all of them.
if !ctx.IsMirrorlist() && !ctx.IsMetalink() {
// Reduce the number of mirrors to process
mlist = mlist[:utils.Min(5, len(mlist))]
}
Expand Down Expand Up @@ -121,7 +123,7 @@ func (h DefaultEngine) Selection(ctx *Context, cache *mirrors.Cache, fileInfo *f

if selected > 1 {

if ctx.IsMirrorlist() {
if ctx.IsMirrorlist() || ctx.IsMetalink() {
// Don't reorder the results, just set the percentage
for i := 0; i < selected; i++ {
id := mlist[i].ID
Expand Down