github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/header.go (about) 1 // Copyright 2020 Google LLC 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 // https://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 safehttp 16 17 import ( 18 "errors" 19 "fmt" 20 "net/http" 21 "net/textproto" 22 ) 23 24 // Header represents the key-value pairs in an HTTP header. 25 // The keys will be in canonical form, as returned by 26 // textproto.CanonicalMIMEHeaderKey. 27 type Header struct { 28 wrapped http.Header 29 claimed map[string]bool 30 } 31 32 // NewHeader creates a new Header. 33 func NewHeader(h http.Header) Header { 34 if h == nil { 35 h = http.Header{} 36 } 37 return Header{ 38 wrapped: h, 39 claimed: map[string]bool{}, 40 } 41 } 42 43 // Claim claims the header with the given name and returns a function 44 // which can be used to set the header. The name is first canonicalized 45 // using textproto.CanonicalMIMEHeaderKey. Other methods in 46 // the struct can't write to, change or delete the header with this 47 // name. These methods will instead panic when applied on a claimed 48 // header. The only way to modify the header is to use the returned 49 // function. The Set-Cookie header can't be claimed. 50 func (h Header) Claim(name string) (set func([]string)) { 51 name = textproto.CanonicalMIMEHeaderKey(name) 52 if err := h.writableHeader(name); err != nil { 53 panic(err) 54 } 55 h.claimed[name] = true 56 return func(v []string) { 57 if v == nil { 58 return 59 } 60 h.wrapped[name] = v 61 } 62 } 63 64 // IsClaimed reports whether the provided header is already claimed. The name is 65 // first canonicalized using textproto.CanonicalMIMEHeaderKey. The Set-Cookie header 66 // is treated as claimed. 67 func (h Header) IsClaimed(name string) bool { 68 name = textproto.CanonicalMIMEHeaderKey(name) 69 err := h.writableHeader(name) 70 return err != nil 71 } 72 73 // Set sets the header with the given name to the given value. 74 // The name is first canonicalized using textproto.CanonicalMIMEHeaderKey. 75 // This method first removes all other values associated with this 76 // header before setting the new value. It panics when applied on claimed headers 77 // or on the Set-Cookie header. 78 func (h Header) Set(name, value string) { 79 name = textproto.CanonicalMIMEHeaderKey(name) 80 if err := h.writableHeader(name); err != nil { 81 panic(err) 82 } 83 h.wrapped.Set(name, value) 84 } 85 86 // Add adds a new header with the given name and the given value to 87 // the collection of headers. The name is first canonicalized using 88 // textproto.CanonicalMIMEHeaderKey. It panics when applied 89 // on claimed headers or on the Set-Cookie header. 90 func (h Header) Add(name, value string) { 91 name = textproto.CanonicalMIMEHeaderKey(name) 92 if err := h.writableHeader(name); err != nil { 93 panic(err) 94 } 95 h.wrapped.Add(name, value) 96 } 97 98 // Del deletes all headers with the given name. The name is first canonicalized 99 // using textproto.CanonicalMIMEHeaderKey. It panics when applied on claimed headers 100 // or on the Set-Cookie header. 101 func (h Header) Del(name string) { 102 name = textproto.CanonicalMIMEHeaderKey(name) 103 if err := h.writableHeader(name); err != nil { 104 panic(err) 105 } 106 h.wrapped.Del(name) 107 } 108 109 // Get returns the value of the first header with the given name. 110 // The name is first canonicalized using textproto.CanonicalMIMEHeaderKey. 111 // If no header exists with the given name then "" is returned. 112 func (h Header) Get(name string) string { 113 return h.wrapped.Get(name) 114 } 115 116 // Values returns all the values of all the headers with the given name. 117 // The name is first canonicalized using textproto.CanonicalMIMEHeaderKey. 118 // The values are returned in the same order as they were sent in the request. 119 // The values are returned as a copy of the original slice of strings in 120 // the internal header map. This is to prevent modification of the original 121 // slice. If no header exists with the given name then an empty slice is 122 // returned. 123 func (h Header) Values(name string) []string { 124 v := h.wrapped.Values(name) 125 clone := make([]string, len(v)) 126 copy(clone, v) 127 return clone 128 } 129 130 // addCookie adds the cookie provided as a Set-Cookie header in the header 131 // collection. If the cookie is nil or cookie.Name() is invalid, no header is 132 // added and an error is returned. This is the only method that can modify the 133 // Set-Cookie header. If other methods try to modify the header they will return 134 // errors. 135 func (h Header) addCookie(c *Cookie) error { 136 v := c.String() 137 if v == "" { 138 return errors.New("invalid cookie name") 139 } 140 h.wrapped.Add("Set-Cookie", v) 141 return nil 142 } 143 144 // TODO: Add Write, WriteSubset and Clone when needed. 145 146 // writableHeader assumes that the given name already has been canonicalized 147 // using textproto.CanonicalMIMEHeaderKey. 148 func (h Header) writableHeader(name string) error { 149 // TODO(@mattiasgrenfeldt, @kele, @empijei): Think about how this should 150 // work during legacy conversions. 151 if name == "Set-Cookie" { 152 return errors.New("can't write to Set-Cookie header") 153 } 154 if h.claimed[name] { 155 return fmt.Errorf("claimed header: %s", name) 156 } 157 return nil 158 }