-
Notifications
You must be signed in to change notification settings - Fork 25
/
scopedConfig.go
228 lines (199 loc) · 7.6 KB
/
scopedConfig.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
// Copyright 2015-present, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package signed
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"io"
"io/ioutil"
"net/http"
"time"
"github.com/corestoreio/errors"
"github.com/corestoreio/pkg/net/responseproxy"
"github.com/corestoreio/pkg/store/scope"
"github.com/corestoreio/pkg/util/bufferpool"
"github.com/corestoreio/pkg/util/hashpool"
)
// DefaultHashName identifies the default hash when creating a new scoped
// configuration. You must register this name before using this package via:
// hashpool.Register(`sha256`, sha256.New)
// If you would like to use different hashes you must registered them also in
// the hashpool package.
const DefaultHashName = `sha256`
// ScopedConfig scoped based configuration and should not be embedded into your
// own types. Call ScopedConfig.ScopeHash to know to which scope this
// configuration has been bound to.
type ScopedConfig struct {
scopedConfigGeneric
// start of package specific config values
hashPool hashpool.Tank
// Disabled set to true to disable content signing.
Disabled bool
// InTrailer set to true and the signature will be added to the HTTP Trailer for
// responses.
InTrailer bool
// HeaderParseWriter see description of interface HeaderParseWriter
HeaderParseWriter
// AllowedMethods list of allowed HTTP methods. Must be upper case.
AllowedMethods []string
// TransparentCacher stores the calculated hashes in memory with a TTL. The hash
// won't get written into the HTTP response. If enable you must set the
// Cacher field in the Service struct.
TransparentCacher Cacher
// TransparentTTL defines the time to live for a hash within the Cacher
// interface.
TransparentTTL time.Duration
}
// newScopedConfig creates a new object with the minimum needed configuration.
// Acts also as WithDefaultConfig()
// Default settings: InTrailer activated, Content-HMAC header with sha256,
// allowed HTTP methods set to POST, PUT, PATCH and password for the HMAC SHA
// 256 from a cryptographically random source with a length of 64 bytes.
func newScopedConfig(target, parent scope.TypeID) *ScopedConfig {
sc := &ScopedConfig{
scopedConfigGeneric: newScopedConfigGeneric(target, parent),
InTrailer: true,
HeaderParseWriter: NewContentHMAC(DefaultHashName),
AllowedMethods: []string{"POST", "PUT", "PATCH"},
}
key := make([]byte, 64) // 64 character password
if _, err := rand.Read(key); err != nil {
sc.lastErr = errors.Wrap(err, "[signed] newScopedConfig: Failed to cread from crypto/rand.Read")
// don't init hashpool and let app panic
} else {
sc.hashPoolInit(DefaultHashName, key)
}
return sc
}
func (sc *ScopedConfig) hashPoolInit(name string, key []byte) {
sc.hashPool, sc.lastErr = hashpool.FromRegistryHMAC(name, key)
sc.lastErr = errors.Wrapf(sc.lastErr, "[signed] The hash %q has not yet been registered via hashpool.Register() function.", name)
}
// isValid a configuration for a scope is only then valid when several fields
// are not empty: HeaderParseWriter and AllowedMethods OR disabled for current
// scope.
func (sc *ScopedConfig) isValid() error {
if err := sc.isValidPreCheck(); err != nil {
return errors.Wrap(err, "[signed] scopedConfig.isValid has an lastErr")
}
if sc.Disabled {
return nil
}
if sc.HeaderParseWriter == nil || len(sc.AllowedMethods) == 0 {
return errors.NewNotValidf(errScopedConfigNotValid, sc.ScopeID, sc.HeaderParseWriter == nil, sc.AllowedMethods)
}
return nil
}
// direct output to the client and the signature will be inserted after the body
// has been written. ideal for streaming but not all clients can process a
// trailer.
func (sc *ScopedConfig) writeTrailer(next http.Handler, w http.ResponseWriter, r *http.Request) {
h := sc.hashPool.Get()
defer sc.hashPool.Put(h)
wt := responseproxy.WrapTee(w)
wt.Tee(h) // write also to hash
if k := sc.HeaderParseWriter.HeaderKey(); k != "" {
wt.Header().Add("Trailer", k)
}
next.ServeHTTP(wt, r)
buf := bufferpool.Get()
sc.HeaderParseWriter.Write(w, h.Sum(buf.Bytes()))
bufferpool.Put(buf)
}
// the write to w gets buffered and we calculate the checksum of the buffer and
// then flush the buffer to the client.
func (sc *ScopedConfig) writeBuffered(next http.Handler, w http.ResponseWriter, r *http.Request) {
h := sc.hashPool.Get()
defer sc.hashPool.Put(h)
rwBuf := bufferpool.Get()
hBuf := bufferpool.Get()
defer bufferpool.Put(hBuf)
defer bufferpool.Put(rwBuf)
next.ServeHTTP(responseproxy.WrapBuffered(rwBuf, w), r)
// calculate the hash based on the buffered response body
if _, err := h.Write(rwBuf.Bytes()); err != nil {
sc.ErrorHandler(errors.Wrap(err, "[signed] ScopedConfig.writeBuffered failed to io.Copy")).ServeHTTP(w, r)
return
}
sc.HeaderParseWriter.Write(w, h.Sum(hBuf.Bytes()))
if _, err := io.Copy(w, rwBuf); err != nil {
sc.ErrorHandler(errors.Wrap(err, "[signed] ScopedConfig.writeBuffered failed to io.Copy")).ServeHTTP(w, r)
}
}
// CalculateHash calculates the hash sum from the request body. The full body
// gets read into a buffer. This buffer gets assigned to the r.Body to make a
// read possible for the next consumer.
func (sc *ScopedConfig) CalculateHash(r *http.Request) ([]byte, error) {
h := sc.hashPool.Get()
defer sc.hashPool.Put(h)
defer r.Body.Close()
// copy the body so that the next consumer can read it.
body := new(bytes.Buffer)
buf := make([]byte, 4096) // maybe make it configurable ...
for {
n, err := r.Body.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return nil, errors.Wrap(err, "[signed] ValidateBody HTTP.Body.Read")
}
if _, err := h.Write(buf[:n]); err != nil {
return nil, errors.Wrap(err, "[signed] ValidateBody Hash.Write")
}
_, _ = body.Write(buf[:n])
}
r.Body = ioutil.NopCloser(body)
buf = buf[:0]
return h.Sum(buf), nil
}
func (sc *ScopedConfig) isMethodAllowed(reqMethod string) bool {
for _, m := range sc.AllowedMethods {
if reqMethod == m {
return true
}
}
return false
}
// ValidateBody uses the HTTPParser to extract the hash signature. It then
// hashes the body and compares the hash of the body with the hash value found
// in the HTTP header. Hash comparison via constant time.
func (sc *ScopedConfig) ValidateBody(r *http.Request) error {
if !sc.isMethodAllowed(r.Method) {
return errors.NewNotValidf(errScopedConfigMethodNotAllowed, r.Method, sc.AllowedMethods)
}
hashSum, err := sc.CalculateHash(r)
if err != nil {
return errors.Wrap(err, "[signed] ScopedConfig.ValidateBody.calculateHash")
}
// check if we're using transparent hashing and store the hash values in a
// cache. constant time not implement and responsibility of the Cacher
// implementation.
if sc.TransparentCacher != nil {
if sc.TransparentCacher.Has(hashSum) {
return nil
}
return errors.NewNotValidf(errScopedConfigCacheNotFound, hashSum)
}
// parse the header to find the signature
reqHashSum, err := sc.HeaderParseWriter.Parse(r)
if err != nil {
return errors.Wrap(err, "[signed] ValidateBody HTTPParser.Parse")
}
if !hmac.Equal(reqHashSum, hashSum) {
return errors.NewNotValidf(errScopedConfigSignatureNoMatch, reqHashSum, hashSum)
}
return nil
}