tinyrelayfile
tinyrelay/internal/blob/format.go/

internal/blob/format.go raw
1package blob
2
3import (
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)
17
18// DetectMediaType recognises images, video, audio and PDF from the first
19// bytes of a blob. Anything else stays an octet stream so uploads can never
20// 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 detected
26 }
27 }
28 if detected == "application/pdf" {
29 return detected
30 }
31 return "application/octet-stream"
32}
33
34func 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}
44
45func 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 raw
51}
52func extensionSuffix(typ string) string {
53 if ext := extensions[typ]; ext != "" {
54 return "." + ext
55 }
56 return ""
57}
58
59func 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 typ
68 }
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 out
79}
80
81func parsePageCount(raw string) int {
82 value, err := strconv.Atoi(raw)
83 if err != nil || value < 1 {
84 return 20
85 }
86 if value > 100 {
87 return 100
88 }
89 return value
90}
91
92func parsePage(raw string) int {
93 value, err := strconv.Atoi(raw)
94 if err != nil || value < 0 {
95 return 0
96 }
97 return value
98}
99
100func parseListLimit(raw string) (int, error) {
101 if raw == "" {
102 return 20, nil
103 }
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, nil
109}
110func chooseStatus(created bool, status int) int {
111 if created {
112 return status
113 }
114 return http.StatusOK
115}
116func statusFor(err error) int {
117 if errors.Is(err, ErrHashMismatch) {
118 return http.StatusConflict
119 }
120 if errors.Is(err, ErrMultipartConflict) {
121 return http.StatusConflict
122 }
123 if errors.Is(err, ErrFileTooLarge) {
124 return http.StatusRequestEntityTooLarge
125 }
126 if errors.Is(err, ErrQuotaExceeded) {
127 return http.StatusForbidden
128 }
129 if errors.Is(err, ErrBlocked) || errors.Is(err, ErrNotEncrypted) {
130 return http.StatusForbidden
131 }
132 if strings.HasPrefix(err.Error(), "auth-required:") {
133 return http.StatusUnauthorized
134 }
135 if strings.HasPrefix(err.Error(), "invalid:") {
136 return http.StatusBadRequest
137 }
138 if strings.HasPrefix(err.Error(), "restricted:") {
139 return http.StatusForbidden
140 }
141 return http.StatusInternalServerError
142}
143
144func 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 continue
150 }
151 seen[value] = struct{}{}
152 result = append(result, value)
153 }
154 return result
155}
156
157func 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 nil
172}
173
174func publicIP(ip net.IP) bool {
175 if ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
176 return false
177 }
178 if ip4 := ip.To4(); ip4 != nil {
179 if ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 {
180 return false
181 }
182 if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 0 {
183 return false
184 }
185 if ip4[0] == 198 && (ip4[1] == 18 || ip4[1] == 19) {
186 return false
187 }
188 }
189 return true
190}
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, err
202 }
203 ips, err := s.resolve(ctx, host)
204 if err != nil {
205 return nil, err
206 }
207 for _, ip := range ips {
208 if !publicIP(ip) {
209 continue
210 }
211 conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
212 if err == nil {
213 return conn, nil
214 }
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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs