1package blob23import (4 "context"5 "errors"6 "io"7 "net"8 "net/http"9 "net/url"10 "os"11 "path/filepath"12 "sort"13 "strconv"14 "strings"15 "time"16)1718// DetectMediaType recognises images, video, audio and PDF from the first19// bytes of a blob. Anything else stays an octet stream so uploads can never20// be reinterpreted as a page or a script.21func DetectMediaType(head []byte) string {22 detected := contentType(http.DetectContentType(head))23 for _, prefix := range []string{"image/", "video/", "audio/"} {24 if strings.HasPrefix(detected, prefix) {25 return detected26 }27 }28 if detected == "application/pdf" {29 return detected30 }31 return "application/octet-stream"32}3334func detectFileType(name string) string {35 file, err := os.Open(name)36 if err != nil {37 return "application/octet-stream"38 }39 defer file.Close()40 head := make([]byte, 512)41 n, _ := io.ReadFull(file, head)42 return DetectMediaType(head[:n])43}4445func contentType(raw string) string {46 raw = strings.ToLower(strings.TrimSpace(strings.Split(raw, ";")[0]))47 if raw == "" {48 return "application/octet-stream"49 }50 return raw51}52func extensionSuffix(typ string) string {53 if ext := extensions[typ]; ext != "" {54 return "." + ext55 }56 return ""57}5859func typeForExtension(raw string) string {60 u, err := url.Parse(raw)61 if err != nil {62 return "application/octet-stream"63 }64 name := strings.ToLower(filepath.Ext(u.Path))65 for typ, ext := range extensions {66 if "."+ext == name {67 return typ68 }69 }70 return "application/octet-stream"71}72func contentTypes() []string {73 out := make([]string, 0, len(extensions))74 for typ := range extensions {75 out = append(out, typ)76 }77 sort.Strings(out)78 return out79}8081func parsePageCount(raw string) int {82 value, err := strconv.Atoi(raw)83 if err != nil || value < 1 {84 return 2085 }86 if value > 100 {87 return 10088 }89 return value90}9192func parsePage(raw string) int {93 value, err := strconv.Atoi(raw)94 if err != nil || value < 0 {95 return 096 }97 return value98}99100func parseListLimit(raw string) (int, error) {101 if raw == "" {102 return 20, nil103 }104 value, err := strconv.Atoi(raw)105 if err != nil || value < 1 || value > 100 {106 return 0, errors.New("invalid: limit must be between 1 and 100")107 }108 return value, nil109}110func chooseStatus(created bool, status int) int {111 if created {112 return status113 }114 return http.StatusOK115}116func statusFor(err error) int {117 if errors.Is(err, ErrHashMismatch) {118 return http.StatusConflict119 }120 if errors.Is(err, ErrMultipartConflict) {121 return http.StatusConflict122 }123 if errors.Is(err, ErrFileTooLarge) {124 return http.StatusRequestEntityTooLarge125 }126 if errors.Is(err, ErrQuotaExceeded) {127 return http.StatusForbidden128 }129 if errors.Is(err, ErrBlocked) || errors.Is(err, ErrNotEncrypted) {130 return http.StatusForbidden131 }132 if strings.HasPrefix(err.Error(), "auth-required:") {133 return http.StatusUnauthorized134 }135 if strings.HasPrefix(err.Error(), "invalid:") {136 return http.StatusBadRequest137 }138 if strings.HasPrefix(err.Error(), "restricted:") {139 return http.StatusForbidden140 }141 return http.StatusInternalServerError142}143144func uniqueStrings(values []string) []string {145 seen := make(map[string]struct{}, len(values))146 result := make([]string, 0, len(values))147 for _, value := range values {148 if _, ok := seen[value]; ok || value == "" {149 continue150 }151 seen[value] = struct{}{}152 result = append(result, value)153 }154 return result155}156157func validateMirrorURL(raw string, resolve ResolveIP) error {158 u, err := url.Parse(raw)159 if err != nil || u.Scheme != "https" || u.Hostname() == "" {160 return errors.New("invalid: only https urls can be mirrored")161 }162 ips, err := resolve(context.Background(), u.Hostname())163 if err != nil || len(ips) == 0 {164 return errors.New("invalid: origin hostname did not resolve")165 }166 for _, ip := range ips {167 if !publicIP(ip) {168 return errors.New("invalid: origin resolves to a private address")169 }170 }171 return nil172}173174func publicIP(ip net.IP) bool {175 if ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() {176 return false177 }178 if ip4 := ip.To4(); ip4 != nil {179 if ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 {180 return false181 }182 if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 0 {183 return false184 }185 if ip4[0] == 198 && (ip4[1] == 18 || ip4[1] == 19) {186 return false187 }188 }189 return true190}191func (s *Service) safeClient() *http.Client {192 base, ok := http.DefaultTransport.(*http.Transport)193 if !ok {194 base = &http.Transport{}195 }196 transport := base.Clone()197 dialer := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}198 transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {199 host, port, err := net.SplitHostPort(address)200 if err != nil {201 return nil, err202 }203 ips, err := s.resolve(ctx, host)204 if err != nil {205 return nil, err206 }207 for _, ip := range ips {208 if !publicIP(ip) {209 continue210 }211 conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))212 if err == nil {213 return conn, nil214 }215 }216 return nil, errors.New("origin has no reachable public address")217 }218 return &http.Client{Timeout: 5 * time.Minute, Transport: transport, CheckRedirect: func(req *http.Request, _ []*http.Request) error {219 return validateMirrorURL(req.URL.String(), s.resolve)220 }}221}