Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/internal/net/util.go
1560 views
1
package net
2
3
import (
4
"fmt"
5
"io"
6
"math"
7
"mime/multipart"
8
"net/http"
9
"net/textproto"
10
"strings"
11
"time"
12
13
"github.com/alist-org/alist/v3/pkg/utils"
14
15
"github.com/alist-org/alist/v3/pkg/http_range"
16
log "github.com/sirupsen/logrus"
17
)
18
19
// scanETag determines if a syntactically valid ETag is present at s. If so,
20
// the ETag and remaining text after consuming ETag is returned. Otherwise,
21
// it returns "", "".
22
func scanETag(s string) (etag string, remain string) {
23
s = textproto.TrimString(s)
24
start := 0
25
if strings.HasPrefix(s, "W/") {
26
start = 2
27
}
28
if len(s[start:]) < 2 || s[start] != '"' {
29
return "", ""
30
}
31
// ETag is either W/"text" or "text".
32
// See RFC 7232 2.3.
33
for i := start + 1; i < len(s); i++ {
34
c := s[i]
35
switch {
36
// Character values allowed in ETags.
37
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
38
case c == '"':
39
return s[:i+1], s[i+1:]
40
default:
41
return "", ""
42
}
43
}
44
return "", ""
45
}
46
47
// etagStrongMatch reports whether a and b match using strong ETag comparison.
48
// Assumes a and b are valid ETags.
49
func etagStrongMatch(a, b string) bool {
50
return a == b && a != "" && a[0] == '"'
51
}
52
53
// etagWeakMatch reports whether a and b match using weak ETag comparison.
54
// Assumes a and b are valid ETags.
55
func etagWeakMatch(a, b string) bool {
56
return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
57
}
58
59
// condResult is the result of an HTTP request precondition check.
60
// See https://tools.ietf.org/html/rfc7232 section 3.
61
type condResult int
62
63
const (
64
condNone condResult = iota
65
condTrue
66
condFalse
67
)
68
69
func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult {
70
im := r.Header.Get("If-Match")
71
if im == "" {
72
return condNone
73
}
74
r.Header.Del("If-Match")
75
for {
76
im = textproto.TrimString(im)
77
if len(im) == 0 {
78
break
79
}
80
if im[0] == ',' {
81
im = im[1:]
82
continue
83
}
84
if im[0] == '*' {
85
return condTrue
86
}
87
etag, remain := scanETag(im)
88
if etag == "" {
89
break
90
}
91
if etagStrongMatch(etag, w.Header().Get("Etag")) {
92
return condTrue
93
}
94
im = remain
95
}
96
97
return condFalse
98
}
99
100
func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {
101
ius := r.Header.Get("If-Unmodified-Since")
102
if ius == "" {
103
return condNone
104
}
105
r.Header.Del("If-Unmodified-Since")
106
if isZeroTime(modtime) {
107
return condNone
108
}
109
t, err := http.ParseTime(ius)
110
if err != nil {
111
return condNone
112
}
113
114
// The Last-Modified header truncates sub-second precision so
115
// the modtime needs to be truncated too.
116
modtime = modtime.Truncate(time.Second)
117
if ret := modtime.Compare(t); ret <= 0 {
118
return condTrue
119
}
120
return condFalse
121
}
122
123
func checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult {
124
inm := r.Header.Get("If-None-Match")
125
if inm == "" {
126
return condNone
127
}
128
r.Header.Del("If-None-Match")
129
buf := inm
130
for {
131
buf = textproto.TrimString(buf)
132
if len(buf) == 0 {
133
break
134
}
135
if buf[0] == ',' {
136
buf = buf[1:]
137
continue
138
}
139
if buf[0] == '*' {
140
return condFalse
141
}
142
etag, remain := scanETag(buf)
143
if etag == "" {
144
break
145
}
146
if etagWeakMatch(etag, w.Header().Get("Etag")) {
147
return condFalse
148
}
149
buf = remain
150
}
151
return condTrue
152
}
153
154
func checkIfModifiedSince(r *http.Request, modtime time.Time) condResult {
155
if r.Method != "GET" && r.Method != "HEAD" {
156
return condNone
157
}
158
ims := r.Header.Get("If-Modified-Since")
159
if ims == "" {
160
return condNone
161
}
162
r.Header.Del("If-Modified-Since")
163
if isZeroTime(modtime) {
164
return condNone
165
}
166
t, err := http.ParseTime(ims)
167
if err != nil {
168
return condNone
169
}
170
// The Last-Modified header truncates sub-second precision so
171
// the modtime needs to be truncated too.
172
modtime = modtime.Truncate(time.Second)
173
if ret := modtime.Compare(t); ret <= 0 {
174
return condFalse
175
}
176
return condTrue
177
}
178
179
func checkIfRange(w http.ResponseWriter, r *http.Request, modtime time.Time) condResult {
180
if r.Method != "GET" && r.Method != "HEAD" {
181
return condNone
182
}
183
ir := r.Header.Get("If-Range")
184
if ir == "" {
185
return condNone
186
}
187
r.Header.Del("If-Range")
188
etag, _ := scanETag(ir)
189
if etag != "" {
190
if etagStrongMatch(etag, w.Header().Get("Etag")) {
191
return condTrue
192
}
193
return condFalse
194
}
195
// The If-Range value is typically the ETag value, but it may also be
196
// the modtime date. See golang.org/issue/8367.
197
if modtime.IsZero() {
198
return condFalse
199
}
200
t, err := http.ParseTime(ir)
201
if err != nil {
202
return condFalse
203
}
204
if t.Unix() == modtime.Unix() {
205
return condTrue
206
}
207
return condFalse
208
}
209
210
var unixEpochTime = time.Unix(0, 0)
211
212
// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
213
func isZeroTime(t time.Time) bool {
214
return t.IsZero() || t.Equal(unixEpochTime)
215
}
216
217
func setLastModified(w http.ResponseWriter, modtime time.Time) {
218
if !isZeroTime(modtime) {
219
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
220
}
221
}
222
223
func writeNotModified(w http.ResponseWriter) {
224
// RFC 7232 section 4.1:
225
// a sender SHOULD NOT generate representation metadata other than the
226
// above listed fields unless said metadata exists for the purpose of
227
// guiding cache updates (e.g., Last-Modified might be useful if the
228
// response does not have an ETag field).
229
h := w.Header()
230
delete(h, "Content-Type")
231
delete(h, "Content-Length")
232
delete(h, "Content-Encoding")
233
if h.Get("Etag") != "" {
234
delete(h, "Last-Modified")
235
}
236
w.WriteHeader(http.StatusNotModified)
237
}
238
239
// checkPreconditions evaluates request preconditions and reports whether a precondition
240
// resulted in sending StatusNotModified or StatusPreconditionFailed.
241
func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool, rangeHeader string) {
242
// This function carefully follows RFC 7232 section 6.
243
ch := checkIfMatch(w, r)
244
if ch == condNone {
245
ch = checkIfUnmodifiedSince(r, modtime)
246
}
247
if ch == condFalse {
248
w.WriteHeader(http.StatusPreconditionFailed)
249
return true, ""
250
}
251
switch checkIfNoneMatch(w, r) {
252
case condFalse:
253
if r.Method == "GET" || r.Method == "HEAD" {
254
writeNotModified(w)
255
return true, ""
256
}
257
w.WriteHeader(http.StatusPreconditionFailed)
258
return true, ""
259
case condNone:
260
if checkIfModifiedSince(r, modtime) == condFalse {
261
writeNotModified(w)
262
return true, ""
263
}
264
}
265
266
rangeHeader = r.Header.Get("Range")
267
if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
268
rangeHeader = ""
269
}
270
return false, rangeHeader
271
}
272
273
func sumRangesSize(ranges []http_range.Range) (size int64) {
274
for _, ra := range ranges {
275
size += ra.Length
276
}
277
return
278
}
279
280
// countingWriter counts how many bytes have been written to it.
281
type countingWriter int64
282
283
func (w *countingWriter) Write(p []byte) (n int, err error) {
284
*w += countingWriter(len(p))
285
return len(p), nil
286
}
287
288
// rangesMIMESize returns the number of bytes it takes to encode the
289
// provided ranges as a multipart response.
290
func rangesMIMESize(ranges []http_range.Range, contentType string, contentSize int64) (encSize int64, err error) {
291
var w countingWriter
292
mw := multipart.NewWriter(&w)
293
for _, ra := range ranges {
294
_, err := mw.CreatePart(ra.MimeHeader(contentType, contentSize))
295
if err != nil {
296
return 0, err
297
}
298
encSize += ra.Length
299
}
300
err = mw.Close()
301
if err != nil {
302
return 0, err
303
}
304
encSize += int64(w)
305
return encSize, nil
306
}
307
308
// LimitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
309
type LimitedReadCloser struct {
310
rc io.ReadCloser
311
remaining int
312
}
313
314
func (l *LimitedReadCloser) Read(buf []byte) (int, error) {
315
if l.remaining <= 0 {
316
return 0, io.EOF
317
}
318
319
if len(buf) > l.remaining {
320
buf = buf[0:l.remaining]
321
}
322
323
n, err := l.rc.Read(buf)
324
l.remaining -= n
325
326
return n, err
327
}
328
329
func (l *LimitedReadCloser) Close() error {
330
return l.rc.Close()
331
}
332
333
// GetRangedHttpReader some http server doesn't support "Range" header,
334
// so this function read readCloser with whole data, skip offset, then return ReaderCloser.
335
func GetRangedHttpReader(readCloser io.ReadCloser, offset, length int64) (io.ReadCloser, error) {
336
var length_int int
337
if length > math.MaxInt {
338
return nil, fmt.Errorf("doesnot support length bigger than int32 max ")
339
}
340
length_int = int(length)
341
342
if offset > 100*1024*1024 {
343
log.Warnf("offset is more than 100MB, if loading data from internet, high-latency and wasting of bandwidth is expected")
344
}
345
346
if _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(readCloser, offset)); err != nil {
347
return nil, err
348
}
349
350
// return an io.ReadCloser that is limited to `length` bytes.
351
return &LimitedReadCloser{readCloser, length_int}, nil
352
}
353
354