baggage.go (15409B)
1 // Copyright The OpenTelemetry Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package baggage // import "go.opentelemetry.io/otel/baggage" 16 17 import ( 18 "errors" 19 "fmt" 20 "net/url" 21 "regexp" 22 "strings" 23 24 "go.opentelemetry.io/otel/internal/baggage" 25 ) 26 27 const ( 28 maxMembers = 180 29 maxBytesPerMembers = 4096 30 maxBytesPerBaggageString = 8192 31 32 listDelimiter = "," 33 keyValueDelimiter = "=" 34 propertyDelimiter = ";" 35 36 keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)` 37 valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)` 38 keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*` 39 ) 40 41 var ( 42 keyRe = regexp.MustCompile(`^` + keyDef + `$`) 43 valueRe = regexp.MustCompile(`^` + valueDef + `$`) 44 propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`) 45 ) 46 47 var ( 48 errInvalidKey = errors.New("invalid key") 49 errInvalidValue = errors.New("invalid value") 50 errInvalidProperty = errors.New("invalid baggage list-member property") 51 errInvalidMember = errors.New("invalid baggage list-member") 52 errMemberNumber = errors.New("too many list-members in baggage-string") 53 errMemberBytes = errors.New("list-member too large") 54 errBaggageBytes = errors.New("baggage-string too large") 55 ) 56 57 // Property is an additional metadata entry for a baggage list-member. 58 type Property struct { 59 key, value string 60 61 // hasValue indicates if a zero-value value means the property does not 62 // have a value or if it was the zero-value. 63 hasValue bool 64 65 // hasData indicates whether the created property contains data or not. 66 // Properties that do not contain data are invalid with no other check 67 // required. 68 hasData bool 69 } 70 71 // NewKeyProperty returns a new Property for key. 72 // 73 // If key is invalid, an error will be returned. 74 func NewKeyProperty(key string) (Property, error) { 75 if !keyRe.MatchString(key) { 76 return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) 77 } 78 79 p := Property{key: key, hasData: true} 80 return p, nil 81 } 82 83 // NewKeyValueProperty returns a new Property for key with value. 84 // 85 // If key or value are invalid, an error will be returned. 86 func NewKeyValueProperty(key, value string) (Property, error) { 87 if !keyRe.MatchString(key) { 88 return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) 89 } 90 if !valueRe.MatchString(value) { 91 return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value) 92 } 93 94 p := Property{ 95 key: key, 96 value: value, 97 hasValue: true, 98 hasData: true, 99 } 100 return p, nil 101 } 102 103 func newInvalidProperty() Property { 104 return Property{} 105 } 106 107 // parseProperty attempts to decode a Property from the passed string. It 108 // returns an error if the input is invalid according to the W3C Baggage 109 // specification. 110 func parseProperty(property string) (Property, error) { 111 if property == "" { 112 return newInvalidProperty(), nil 113 } 114 115 match := propertyRe.FindStringSubmatch(property) 116 if len(match) != 4 { 117 return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property) 118 } 119 120 p := Property{hasData: true} 121 if match[1] != "" { 122 p.key = match[1] 123 } else { 124 p.key = match[2] 125 p.value = match[3] 126 p.hasValue = true 127 } 128 129 return p, nil 130 } 131 132 // validate ensures p conforms to the W3C Baggage specification, returning an 133 // error otherwise. 134 func (p Property) validate() error { 135 errFunc := func(err error) error { 136 return fmt.Errorf("invalid property: %w", err) 137 } 138 139 if !p.hasData { 140 return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p)) 141 } 142 143 if !keyRe.MatchString(p.key) { 144 return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key)) 145 } 146 if p.hasValue && !valueRe.MatchString(p.value) { 147 return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value)) 148 } 149 if !p.hasValue && p.value != "" { 150 return errFunc(errors.New("inconsistent value")) 151 } 152 return nil 153 } 154 155 // Key returns the Property key. 156 func (p Property) Key() string { 157 return p.key 158 } 159 160 // Value returns the Property value. Additionally, a boolean value is returned 161 // indicating if the returned value is the empty if the Property has a value 162 // that is empty or if the value is not set. 163 func (p Property) Value() (string, bool) { 164 return p.value, p.hasValue 165 } 166 167 // String encodes Property into a string compliant with the W3C Baggage 168 // specification. 169 func (p Property) String() string { 170 if p.hasValue { 171 return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value) 172 } 173 return p.key 174 } 175 176 type properties []Property 177 178 func fromInternalProperties(iProps []baggage.Property) properties { 179 if len(iProps) == 0 { 180 return nil 181 } 182 183 props := make(properties, len(iProps)) 184 for i, p := range iProps { 185 props[i] = Property{ 186 key: p.Key, 187 value: p.Value, 188 hasValue: p.HasValue, 189 } 190 } 191 return props 192 } 193 194 func (p properties) asInternal() []baggage.Property { 195 if len(p) == 0 { 196 return nil 197 } 198 199 iProps := make([]baggage.Property, len(p)) 200 for i, prop := range p { 201 iProps[i] = baggage.Property{ 202 Key: prop.key, 203 Value: prop.value, 204 HasValue: prop.hasValue, 205 } 206 } 207 return iProps 208 } 209 210 func (p properties) Copy() properties { 211 if len(p) == 0 { 212 return nil 213 } 214 215 props := make(properties, len(p)) 216 copy(props, p) 217 return props 218 } 219 220 // validate ensures each Property in p conforms to the W3C Baggage 221 // specification, returning an error otherwise. 222 func (p properties) validate() error { 223 for _, prop := range p { 224 if err := prop.validate(); err != nil { 225 return err 226 } 227 } 228 return nil 229 } 230 231 // String encodes properties into a string compliant with the W3C Baggage 232 // specification. 233 func (p properties) String() string { 234 props := make([]string, len(p)) 235 for i, prop := range p { 236 props[i] = prop.String() 237 } 238 return strings.Join(props, propertyDelimiter) 239 } 240 241 // Member is a list-member of a baggage-string as defined by the W3C Baggage 242 // specification. 243 type Member struct { 244 key, value string 245 properties properties 246 247 // hasData indicates whether the created property contains data or not. 248 // Properties that do not contain data are invalid with no other check 249 // required. 250 hasData bool 251 } 252 253 // NewMember returns a new Member from the passed arguments. The key will be 254 // used directly while the value will be url decoded after validation. An error 255 // is returned if the created Member would be invalid according to the W3C 256 // Baggage specification. 257 func NewMember(key, value string, props ...Property) (Member, error) { 258 m := Member{ 259 key: key, 260 value: value, 261 properties: properties(props).Copy(), 262 hasData: true, 263 } 264 if err := m.validate(); err != nil { 265 return newInvalidMember(), err 266 } 267 decodedValue, err := url.QueryUnescape(value) 268 if err != nil { 269 return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) 270 } 271 m.value = decodedValue 272 return m, nil 273 } 274 275 func newInvalidMember() Member { 276 return Member{} 277 } 278 279 // parseMember attempts to decode a Member from the passed string. It returns 280 // an error if the input is invalid according to the W3C Baggage 281 // specification. 282 func parseMember(member string) (Member, error) { 283 if n := len(member); n > maxBytesPerMembers { 284 return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n) 285 } 286 287 var ( 288 key, value string 289 props properties 290 ) 291 292 keyValue, properties, found := strings.Cut(member, propertyDelimiter) 293 if found { 294 // Parse the member properties. 295 for _, pStr := range strings.Split(properties, propertyDelimiter) { 296 p, err := parseProperty(pStr) 297 if err != nil { 298 return newInvalidMember(), err 299 } 300 props = append(props, p) 301 } 302 } 303 // Parse the member key/value pair. 304 305 // Take into account a value can contain equal signs (=). 306 k, v, found := strings.Cut(keyValue, keyValueDelimiter) 307 if !found { 308 return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member) 309 } 310 // "Leading and trailing whitespaces are allowed but MUST be trimmed 311 // when converting the header into a data structure." 312 key = strings.TrimSpace(k) 313 var err error 314 value, err = url.QueryUnescape(strings.TrimSpace(v)) 315 if err != nil { 316 return newInvalidMember(), fmt.Errorf("%w: %q", err, value) 317 } 318 if !keyRe.MatchString(key) { 319 return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key) 320 } 321 if !valueRe.MatchString(value) { 322 return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) 323 } 324 325 return Member{key: key, value: value, properties: props, hasData: true}, nil 326 } 327 328 // validate ensures m conforms to the W3C Baggage specification. 329 // A key is just an ASCII string, but a value must be URL encoded UTF-8, 330 // returning an error otherwise. 331 func (m Member) validate() error { 332 if !m.hasData { 333 return fmt.Errorf("%w: %q", errInvalidMember, m) 334 } 335 336 if !keyRe.MatchString(m.key) { 337 return fmt.Errorf("%w: %q", errInvalidKey, m.key) 338 } 339 if !valueRe.MatchString(m.value) { 340 return fmt.Errorf("%w: %q", errInvalidValue, m.value) 341 } 342 return m.properties.validate() 343 } 344 345 // Key returns the Member key. 346 func (m Member) Key() string { return m.key } 347 348 // Value returns the Member value. 349 func (m Member) Value() string { return m.value } 350 351 // Properties returns a copy of the Member properties. 352 func (m Member) Properties() []Property { return m.properties.Copy() } 353 354 // String encodes Member into a string compliant with the W3C Baggage 355 // specification. 356 func (m Member) String() string { 357 // A key is just an ASCII string, but a value is URL encoded UTF-8. 358 s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, url.QueryEscape(m.value)) 359 if len(m.properties) > 0 { 360 s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String()) 361 } 362 return s 363 } 364 365 // Baggage is a list of baggage members representing the baggage-string as 366 // defined by the W3C Baggage specification. 367 type Baggage struct { //nolint:golint 368 list baggage.List 369 } 370 371 // New returns a new valid Baggage. It returns an error if it results in a 372 // Baggage exceeding limits set in that specification. 373 // 374 // It expects all the provided members to have already been validated. 375 func New(members ...Member) (Baggage, error) { 376 if len(members) == 0 { 377 return Baggage{}, nil 378 } 379 380 b := make(baggage.List) 381 for _, m := range members { 382 if !m.hasData { 383 return Baggage{}, errInvalidMember 384 } 385 386 // OpenTelemetry resolves duplicates by last-one-wins. 387 b[m.key] = baggage.Item{ 388 Value: m.value, 389 Properties: m.properties.asInternal(), 390 } 391 } 392 393 // Check member numbers after deduplication. 394 if len(b) > maxMembers { 395 return Baggage{}, errMemberNumber 396 } 397 398 bag := Baggage{b} 399 if n := len(bag.String()); n > maxBytesPerBaggageString { 400 return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) 401 } 402 403 return bag, nil 404 } 405 406 // Parse attempts to decode a baggage-string from the passed string. It 407 // returns an error if the input is invalid according to the W3C Baggage 408 // specification. 409 // 410 // If there are duplicate list-members contained in baggage, the last one 411 // defined (reading left-to-right) will be the only one kept. This diverges 412 // from the W3C Baggage specification which allows duplicate list-members, but 413 // conforms to the OpenTelemetry Baggage specification. 414 func Parse(bStr string) (Baggage, error) { 415 if bStr == "" { 416 return Baggage{}, nil 417 } 418 419 if n := len(bStr); n > maxBytesPerBaggageString { 420 return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) 421 } 422 423 b := make(baggage.List) 424 for _, memberStr := range strings.Split(bStr, listDelimiter) { 425 m, err := parseMember(memberStr) 426 if err != nil { 427 return Baggage{}, err 428 } 429 // OpenTelemetry resolves duplicates by last-one-wins. 430 b[m.key] = baggage.Item{ 431 Value: m.value, 432 Properties: m.properties.asInternal(), 433 } 434 } 435 436 // OpenTelemetry does not allow for duplicate list-members, but the W3C 437 // specification does. Now that we have deduplicated, ensure the baggage 438 // does not exceed list-member limits. 439 if len(b) > maxMembers { 440 return Baggage{}, errMemberNumber 441 } 442 443 return Baggage{b}, nil 444 } 445 446 // Member returns the baggage list-member identified by key. 447 // 448 // If there is no list-member matching the passed key the returned Member will 449 // be a zero-value Member. 450 // The returned member is not validated, as we assume the validation happened 451 // when it was added to the Baggage. 452 func (b Baggage) Member(key string) Member { 453 v, ok := b.list[key] 454 if !ok { 455 // We do not need to worry about distinguishing between the situation 456 // where a zero-valued Member is included in the Baggage because a 457 // zero-valued Member is invalid according to the W3C Baggage 458 // specification (it has an empty key). 459 return newInvalidMember() 460 } 461 462 return Member{ 463 key: key, 464 value: v.Value, 465 properties: fromInternalProperties(v.Properties), 466 hasData: true, 467 } 468 } 469 470 // Members returns all the baggage list-members. 471 // The order of the returned list-members does not have significance. 472 // 473 // The returned members are not validated, as we assume the validation happened 474 // when they were added to the Baggage. 475 func (b Baggage) Members() []Member { 476 if len(b.list) == 0 { 477 return nil 478 } 479 480 members := make([]Member, 0, len(b.list)) 481 for k, v := range b.list { 482 members = append(members, Member{ 483 key: k, 484 value: v.Value, 485 properties: fromInternalProperties(v.Properties), 486 hasData: true, 487 }) 488 } 489 return members 490 } 491 492 // SetMember returns a copy the Baggage with the member included. If the 493 // baggage contains a Member with the same key the existing Member is 494 // replaced. 495 // 496 // If member is invalid according to the W3C Baggage specification, an error 497 // is returned with the original Baggage. 498 func (b Baggage) SetMember(member Member) (Baggage, error) { 499 if !member.hasData { 500 return b, errInvalidMember 501 } 502 503 n := len(b.list) 504 if _, ok := b.list[member.key]; !ok { 505 n++ 506 } 507 list := make(baggage.List, n) 508 509 for k, v := range b.list { 510 // Do not copy if we are just going to overwrite. 511 if k == member.key { 512 continue 513 } 514 list[k] = v 515 } 516 517 list[member.key] = baggage.Item{ 518 Value: member.value, 519 Properties: member.properties.asInternal(), 520 } 521 522 return Baggage{list: list}, nil 523 } 524 525 // DeleteMember returns a copy of the Baggage with the list-member identified 526 // by key removed. 527 func (b Baggage) DeleteMember(key string) Baggage { 528 n := len(b.list) 529 if _, ok := b.list[key]; ok { 530 n-- 531 } 532 list := make(baggage.List, n) 533 534 for k, v := range b.list { 535 if k == key { 536 continue 537 } 538 list[k] = v 539 } 540 541 return Baggage{list: list} 542 } 543 544 // Len returns the number of list-members in the Baggage. 545 func (b Baggage) Len() int { 546 return len(b.list) 547 } 548 549 // String encodes Baggage into a string compliant with the W3C Baggage 550 // specification. The returned string will be invalid if the Baggage contains 551 // any invalid list-members. 552 func (b Baggage) String() string { 553 members := make([]string, 0, len(b.list)) 554 for k, v := range b.list { 555 members = append(members, Member{ 556 key: k, 557 value: v.Value, 558 properties: fromInternalProperties(v.Properties), 559 }.String()) 560 } 561 return strings.Join(members, listDelimiter) 562 }