fix installer
This commit is contained in:
parent
318fb51085
commit
6319da650c
31 changed files with 2391 additions and 87 deletions
143
internal/avatar/avatar.go
Normal file
143
internal/avatar/avatar.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// Package avatar validates and addresses profile pictures stored in PostgreSQL.
|
||||
//
|
||||
// Images are kept as their uploaded bytes. Go Loose never re-encodes them; it
|
||||
// sniffs the format, bounds the decoded dimensions to reject decompression
|
||||
// bombs, and serves the bytes back with the sniffed content type behind an
|
||||
// unguessable key so that other applications can embed the picture with a plain
|
||||
// image tag.
|
||||
package avatar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxBytes bounds an uploaded picture. Base64 inflates it by 4/3, so a
|
||||
// request carrying an avatar must allow roughly MaxBytes*4/3 bytes.
|
||||
MaxBytes = 2 << 20
|
||||
// MaxDimension bounds decoded pixels so that a small upload cannot expand
|
||||
// into an unreasonable image when a client renders it.
|
||||
MaxDimension = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmpty = errors.New("the picture is empty")
|
||||
ErrTooLarge = fmt.Errorf("the picture must be %d KiB or smaller", MaxBytes/1024)
|
||||
ErrUnsupported = errors.New("the picture must be a JPEG, PNG, or GIF image")
|
||||
ErrDimensions = fmt.Errorf("the picture must not exceed %d by %d pixels", MaxDimension, MaxDimension)
|
||||
ErrEncoding = errors.New("the picture is not valid base64")
|
||||
)
|
||||
|
||||
// allowed maps the format names reported by image.DecodeConfig to content types.
|
||||
// Only the decoders registered above are accepted, so an unsupported upload
|
||||
// fails validation instead of being stored and served.
|
||||
var allowed = map[string]string{
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
"gif": "image/gif",
|
||||
}
|
||||
|
||||
// Parse decodes a base64 payload, with or without a data URL prefix, and
|
||||
// returns the image bytes together with the sniffed content type.
|
||||
func Parse(value string) ([]byte, string, error) {
|
||||
payload, declared := trimDataURL(value)
|
||||
if payload == "" {
|
||||
return nil, "", ErrEmpty
|
||||
}
|
||||
if base64.StdEncoding.DecodedLen(len(payload)) > MaxBytes {
|
||||
return nil, "", ErrTooLarge
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return nil, "", ErrEncoding
|
||||
}
|
||||
contentType, err := Inspect(data)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if declared != "" && !strings.EqualFold(declared, contentType) {
|
||||
return nil, "", ErrUnsupported
|
||||
}
|
||||
return data, contentType, nil
|
||||
}
|
||||
|
||||
// Inspect validates raw image bytes and returns their content type.
|
||||
func Inspect(data []byte) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", ErrEmpty
|
||||
}
|
||||
if len(data) > MaxBytes {
|
||||
return "", ErrTooLarge
|
||||
}
|
||||
decoded, format, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", ErrUnsupported
|
||||
}
|
||||
contentType, known := allowed[format]
|
||||
if !known {
|
||||
return "", ErrUnsupported
|
||||
}
|
||||
if decoded.Width > MaxDimension || decoded.Height > MaxDimension {
|
||||
return "", ErrDimensions
|
||||
}
|
||||
return contentType, nil
|
||||
}
|
||||
|
||||
// NewKey returns an unguessable locator for a stored picture. The key is a
|
||||
// capability-free random value: it authorizes reading a picture that is
|
||||
// intended to be public, not access to anything else about the user.
|
||||
func NewKey() (string, error) {
|
||||
value := make([]byte, 32)
|
||||
if _, err := rand.Read(value); err != nil {
|
||||
return "", fmt.Errorf("generate avatar key: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
// ValidKey reports whether a key has the shape produced by NewKey, which keeps
|
||||
// obviously malformed lookups out of the database.
|
||||
func ValidKey(key string) bool {
|
||||
if len(key) != 43 {
|
||||
return false
|
||||
}
|
||||
_, err := base64.RawURLEncoding.DecodeString(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ETag returns a strong validator derived from the stored bytes so that clients
|
||||
// and proxies can cache the picture and revalidate cheaply.
|
||||
func ETag(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return `"` + hex.EncodeToString(sum[:16]) + `"`
|
||||
}
|
||||
|
||||
func trimDataURL(value string) (payload, mediaType string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if !strings.HasPrefix(strings.ToLower(value), "data:") {
|
||||
return value, ""
|
||||
}
|
||||
comma := strings.IndexByte(value, ',')
|
||||
if comma < 0 {
|
||||
return "", ""
|
||||
}
|
||||
header := value[len("data:"):comma]
|
||||
parameters := strings.Split(header, ";")
|
||||
mediaType = strings.ToLower(strings.TrimSpace(parameters[0]))
|
||||
for _, parameter := range parameters[1:] {
|
||||
if strings.EqualFold(strings.TrimSpace(parameter), "base64") {
|
||||
return strings.TrimSpace(value[comma+1:]), mediaType
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue