/
parse.go
236 lines (219 loc) · 7.83 KB
/
parse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
package trustlesshttp
import (
"errors"
"fmt"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/datamodel"
trustlessutils "github.com/ipld/go-trustless-utils"
)
// ParseScope returns the dag-scope query parameter or an error if the dag-scope
// parameter is not one of the supported values.
func ParseScope(req *http.Request) (trustlessutils.DagScope, error) {
if req.URL.Query().Has("dag-scope") {
if ds, err := trustlessutils.ParseDagScope(req.URL.Query().Get("dag-scope")); err != nil {
return ds, errors.New("invalid dag-scope parameter")
} else {
return ds, nil
}
}
return trustlessutils.DagScopeAll, nil
}
// ParseByteRange returns the entity-bytes query parameter if one is set in the
// query string or nil if one is not set. An error is returned if an
// entity-bytes query string is not a valid byte range.
func ParseByteRange(req *http.Request) (*trustlessutils.ByteRange, error) {
if req.URL.Query().Has("entity-bytes") {
br, err := trustlessutils.ParseByteRange(req.URL.Query().Get("entity-bytes"))
if err != nil {
return nil, errors.New("invalid entity-bytes parameter")
}
return &br, nil
}
return nil, nil
}
// ParseFilename returns the filename query parameter or an error if the
// filename extension is not ".car". Lassie only supports returning CAR data.
// See https://specs.ipfs.tech/http-gateways/path-gateway/#filename-request-query-parameter
func ParseFilename(req *http.Request) (string, error) {
// check if provided filename query parameter has .car extension
if req.URL.Query().Has("filename") {
filename := req.URL.Query().Get("filename")
ext := filepath.Ext(filename)
if ext == "" {
return "", errors.New("invalid filename parameter; missing extension")
}
if ext != FilenameExtCar {
return "", fmt.Errorf("invalid filename parameter; unsupported extension: %q", ext)
}
return filename, nil
}
return "", nil
}
// CheckFormat validates that the data being requested is of a compatible
// content type. If the request is valid, a slice of ContentType descriptors
// is returned, in preference order. If the request is invalid, an error is
// returned.
//
// We do this validation because the IPFS Path Gateway spec allows for
// additional response formats that the IPFS Trustless Gateway spec does not
// currently support, so we throw an error in the cases where the request is
// requesting one the unsupported response formats. IPFS Trustless Gateway only
// supports returning CAR, or raw block data.
//
// The spec outlines that the requesting format can be provided
// via the Accept header or the format query parameter.
//
// IPFS Trustless Gateway only allows the application/vnd.ipld.car
// and application/vnd.ipld.raw Accept headers
// https://specs.ipfs.tech/http-gateways/path-gateway/#accept-request-header
//
// IPFS Trustless Gateway only allows the "car" and "raw" format query
// parameters
// https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
func CheckFormat(req *http.Request) ([]ContentType, error) {
format := req.URL.Query().Get("format")
switch format { // initial check, but we also want to check Accept before we allow this
case "", FormatParameterCar, FormatParameterRaw:
default:
return nil, fmt.Errorf("invalid format parameter; unsupported: %q", format)
}
accept := req.Header.Get("Accept")
if accept != "" {
// check if Accept header includes what we need
accepts := ParseAccept(accept)
if len(accepts) == 0 {
return nil, fmt.Errorf("invalid Accept header; unsupported: %q", accept)
}
return accepts, nil // pick the top one we can support
}
if format != "" {
switch format {
case FormatParameterCar:
return []ContentType{DefaultContentType().WithMimeType(MimeTypeCar)}, nil
case FormatParameterRaw:
return []ContentType{DefaultContentType().WithMimeType(MimeTypeRaw)}, nil
}
}
return nil, fmt.Errorf("neither a valid Accept header nor format parameter were provided")
}
// ParseAccept validates a request Accept header and returns whether or not
// duplicate blocks are allowed in the response.
//
// This will operate the same as ParseContentType except that it is less strict
// with the format specifier, allowing for "application/*" and "*/*" as well as
// the standard "application/vnd.ipld.car" and "application/vnd.ipld.raw".
func ParseAccept(acceptHeader string) []ContentType {
acceptTypes := strings.Split(acceptHeader, ",")
accepts := make([]ContentType, 0, len(acceptTypes))
for _, acceptType := range acceptTypes {
accept, valid := parseContentType(acceptType, false)
if valid {
accepts = append(accepts, accept)
}
}
// sort accepts by ContentType#Quality
sort.SliceStable(accepts, func(i, j int) bool {
return accepts[i].Quality > accepts[j].Quality
})
return accepts
}
// ParseContentType validates a response Content-Type header and returns
// a ContentType descriptor form and a boolean to indicate whether or not
// the header value was valid or not.
//
// This will operate similar to ParseAccept except that it strictly only
// allows the "application/vnd.ipld.car" and "application/vnd.ipld.raw"
// Content-Types (and it won't accept comma separated list of content types).
func ParseContentType(contentTypeHeader string) (ContentType, bool) {
return parseContentType(contentTypeHeader, true)
}
func parseContentType(header string, strictType bool) (ContentType, bool) {
typeParts := strings.Split(header, ";")
mime := strings.TrimSpace(typeParts[0])
if mime == MimeTypeCar || mime == MimeTypeRaw || (!strictType && (mime == "*/*" || mime == "application/*")) {
contentType := DefaultContentType().WithMimeType(mime)
// parse additional car attributes outlined in IPIP-412
// https://specs.ipfs.tech/http-gateways/trustless-gateway/
for _, nextPart := range typeParts[1:] {
pair := strings.Split(nextPart, "=")
if len(pair) == 2 {
attr := strings.TrimSpace(pair[0])
value := strings.TrimSpace(pair[1])
if mime == MimeTypeCar {
switch attr {
case "dups":
switch value {
case "y":
contentType.Duplicates = true
case "n":
contentType.Duplicates = false
default:
// don't accept unexpected values
return ContentType{}, false
}
case "version":
switch value {
case MimeTypeCarVersion:
default:
return ContentType{}, false
}
case "order":
switch value {
case "dfs":
contentType.Order = ContentTypeOrderDfs
case "unk":
contentType.Order = ContentTypeOrderUnk
default:
// we only do dfs, which also satisfies unk, future extensions are not yet supported
return ContentType{}, false
}
default:
// ignore others
}
}
if attr == "q" {
// parse quality
quality, err := strconv.ParseFloat(value, 32)
if err != nil || quality < 0 || quality > 1 {
return ContentType{}, false
}
contentType.Quality = float32(quality)
}
}
}
return contentType, true
}
return ContentType{}, false
}
var (
ErrPathNotFound = errors.New("not found")
ErrBadCid = errors.New("failed to parse root CID")
)
// ParseUrlPath parses an incoming IPFS Trustless Gateway path of the form
// /ipfs/<cid>[/<path>] and returns the root CID and the path.
func ParseUrlPath(urlPath string) (cid.Cid, datamodel.Path, error) {
path := datamodel.ParsePath(urlPath)
var seg datamodel.PathSegment
seg, path = path.Shift()
if seg.String() != "ipfs" {
return cid.Undef, datamodel.Path{}, ErrPathNotFound
}
// check if CID path param is missing
if path.Len() == 0 {
// not a valid path to hit
return cid.Undef, datamodel.Path{}, ErrPathNotFound
}
// validate CID path parameter
var cidSeg datamodel.PathSegment
cidSeg, path = path.Shift()
rootCid, err := cid.Parse(cidSeg.String())
if err != nil {
return cid.Undef, datamodel.Path{}, ErrBadCid
}
return rootCid, path, nil
}