@@ -10,6 +10,7 @@ import (
10
10
"fmt"
11
11
"mime"
12
12
"net/http"
13
+ "net/url"
13
14
"strings"
14
15
"time"
15
16
)
@@ -22,6 +23,7 @@ const (
22
23
headerOperationState = "Nexus-Operation-State"
23
24
headerOperationID = "Nexus-Operation-Id"
24
25
headerRequestID = "Nexus-Request-Id"
26
+ headerLink = "Nexus-Link"
25
27
26
28
// HeaderRequestTimeout is the total time to complete a Nexus HTTP request.
27
29
HeaderRequestTimeout = "Request-Timeout"
@@ -145,6 +147,33 @@ func addCallbackHeaderToHTTPHeader(nexusHeader Header, httpHeader http.Header) h
145
147
return httpHeader
146
148
}
147
149
150
+ func addLinksToHTTPHeader (links []Link , httpHeader http.Header ) error {
151
+ for _ , link := range links {
152
+ encodedLink , err := encodeLink (link )
153
+ if err != nil {
154
+ return err
155
+ }
156
+ httpHeader .Add (headerLink , encodedLink )
157
+ }
158
+ return nil
159
+ }
160
+
161
+ func getLinksFromHeader (httpHeader http.Header ) ([]Link , error ) {
162
+ var links []Link
163
+ headerValues := httpHeader .Values (headerLink )
164
+ if len (headerValues ) == 0 {
165
+ return nil , nil
166
+ }
167
+ for _ , encodedLink := range strings .Split (strings .Join (headerValues , "," ), "," ) {
168
+ link , err := decodeLink (encodedLink )
169
+ if err != nil {
170
+ return nil , err
171
+ }
172
+ links = append (links , link )
173
+ }
174
+ return links , nil
175
+ }
176
+
148
177
func httpHeaderToNexusHeader (httpHeader http.Header , excludePrefixes ... string ) Header {
149
178
header := Header {}
150
179
headerLoop:
@@ -176,3 +205,131 @@ func addContextTimeoutToHTTPHeader(ctx context.Context, httpHeader http.Header)
176
205
httpHeader .Set (HeaderRequestTimeout , time .Until (deadline ).String ())
177
206
return httpHeader
178
207
}
208
+
209
+ // Link contains an URL and a Type that can be used to decode the URL.
210
+ // Links can contain any arbitrary information as a percent-encoded URL.
211
+ // It can be used to pass information about the caller to the handler, or vice-versa.
212
+ type Link struct {
213
+ // URL information about the link.
214
+ // It must be URL percent-encoded.
215
+ URL * url.URL
216
+ // Type can describe an actual data type for decoding the URL.
217
+ // Valid chars: alphanumeric, '_', '.', '/'
218
+ Type string
219
+ }
220
+
221
+ const linkTypeKey = "type"
222
+
223
+ // decodeLink encodes the link to Nexus-Link header value.
224
+ // It follows the same format of HTTP Link header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
225
+ func encodeLink (link Link ) (string , error ) {
226
+ if err := validateLinkURL (link .URL ); err != nil {
227
+ return "" , fmt .Errorf ("failed to encode link: %w" , err )
228
+ }
229
+ if err := validateLinkType (link .Type ); err != nil {
230
+ return "" , fmt .Errorf ("failed to encode link: %w" , err )
231
+ }
232
+ return fmt .Sprintf (`<%s>; %s="%s"` , link .URL .String (), linkTypeKey , link .Type ), nil
233
+ }
234
+
235
+ // decodeLink decodes the Nexus-Link header values.
236
+ // It must have the same format of HTTP Link header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
237
+ func decodeLink (encodedLink string ) (Link , error ) {
238
+ var link Link
239
+ encodedLink = strings .TrimSpace (encodedLink )
240
+ if len (encodedLink ) == 0 {
241
+ return link , fmt .Errorf ("failed to parse link header: value is empty" )
242
+ }
243
+
244
+ if encodedLink [0 ] != '<' {
245
+ return link , fmt .Errorf ("failed to parse link header: invalid format: %s" , encodedLink )
246
+ }
247
+ urlEnd := strings .Index (encodedLink , ">" )
248
+ if urlEnd == - 1 {
249
+ return link , fmt .Errorf ("failed to parse link header: invalid format: %s" , encodedLink )
250
+ }
251
+ urlStr := strings .TrimSpace (encodedLink [1 :urlEnd ])
252
+ if len (urlStr ) == 0 {
253
+ return link , fmt .Errorf ("failed to parse link header: url is empty" )
254
+ }
255
+ u , err := url .Parse (urlStr )
256
+ if err != nil {
257
+ return link , fmt .Errorf ("failed to parse link header: invalid url: %s" , urlStr )
258
+ }
259
+ if err := validateLinkURL (u ); err != nil {
260
+ return link , fmt .Errorf ("failed to parse link header: %w" , err )
261
+ }
262
+ link .URL = u
263
+
264
+ params := strings .Split (encodedLink [urlEnd + 1 :], ";" )
265
+ // must contain at least one semi-colon, and first param must be empty since
266
+ // it corresponds to the url part parsed above.
267
+ if len (params ) < 2 {
268
+ return link , fmt .Errorf ("failed to parse link header: invalid format: %s" , encodedLink )
269
+ }
270
+ if strings .TrimSpace (params [0 ]) != "" {
271
+ return link , fmt .Errorf ("failed to parse link header: invalid format: %s" , encodedLink )
272
+ }
273
+
274
+ typeKeyFound := false
275
+ for _ , param := range params [1 :] {
276
+ param = strings .TrimSpace (param )
277
+ if len (param ) == 0 {
278
+ return link , fmt .Errorf ("failed to parse link header: parameter is empty: %s" , encodedLink )
279
+ }
280
+ kv := strings .SplitN (param , "=" , 2 )
281
+ if len (kv ) != 2 {
282
+ return link , fmt .Errorf ("failed to parse link header: invalid parameter format: %s" , param )
283
+ }
284
+ key := strings .TrimSpace (kv [0 ])
285
+ val := strings .TrimSpace (kv [1 ])
286
+ if strings .HasPrefix (val , `"` ) != strings .HasSuffix (val , `"` ) {
287
+ return link , fmt .Errorf (
288
+ "failed to parse link header: parameter value missing double-quote: %s" ,
289
+ param ,
290
+ )
291
+ }
292
+ if strings .HasPrefix (val , `"` ) {
293
+ val = val [1 : len (val )- 1 ]
294
+ }
295
+ if key == linkTypeKey {
296
+ if err := validateLinkType (val ); err != nil {
297
+ return link , fmt .Errorf ("failed to parse link header: %w" , err )
298
+ }
299
+ link .Type = val
300
+ typeKeyFound = true
301
+ }
302
+ }
303
+ if ! typeKeyFound {
304
+ return link , fmt .Errorf (
305
+ "failed to parse link header: %q key not found: %s" ,
306
+ linkTypeKey ,
307
+ encodedLink ,
308
+ )
309
+ }
310
+
311
+ return link , nil
312
+ }
313
+
314
+ func validateLinkURL (value * url.URL ) error {
315
+ if value == nil || value .String () == "" {
316
+ return fmt .Errorf ("url is empty" )
317
+ }
318
+ _ , err := url .ParseQuery (value .RawQuery )
319
+ if err != nil {
320
+ return fmt .Errorf ("url query not percent-encoded: %s" , value )
321
+ }
322
+ return nil
323
+ }
324
+
325
+ func validateLinkType (value string ) error {
326
+ if len (value ) == 0 {
327
+ return fmt .Errorf ("link type is empty" )
328
+ }
329
+ for _ , c := range value {
330
+ if ! (c >= 'a' && c <= 'z' ) && ! (c >= 'A' && c <= 'Z' ) && ! (c >= '0' && c <= '9' ) && c != '_' && c != '.' && c != '/' {
331
+ return fmt .Errorf ("link type contains invalid char (valid chars: alphanumeric, '_', '.', '/')" )
332
+ }
333
+ }
334
+ return nil
335
+ }
0 commit comments