1 // Package v4 implements signing for AWS V4 signer
3 // Provides request signing for request that need to be signed with
8 // Generally using the signer outside of the SDK should not require any additional
9 // logic when using Go v1.5 or higher. The signer does this by taking advantage
10 // of the URL.EscapedPath method. If your request URI requires additional escaping
11 // you many need to use the URL.Opaque to define what the raw URI should be sent
14 // The signer will first check the URL.Opaque field, and use its value if set.
15 // The signer does require the URL.Opaque field to be set in the form of:
17 // "//<hostname>/<path>"
20 // "//example.com/some/path"
22 // The leading "//" and hostname are required or the URL.Opaque escaping will
23 // not work correctly.
25 // If URL.Opaque is not set the signer will fallback to the URL.EscapedPath()
26 // method and using the returned value. If you're using Go v1.4 you must set
27 // URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with
28 // Go v1.5 the signer will fallback to URL.Path.
30 // AWS v4 signature validation requires that the canonical string's URI path
31 // element must be the URI escaped form of the HTTP request's path.
32 // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
34 // The Go HTTP client will perform escaping automatically on the request. Some
35 // of these escaping may cause signature validation errors because the HTTP
36 // request differs from the URI path or query that the signature was generated.
37 // https://golang.org/pkg/net/url/#URL.EscapedPath
39 // Because of this, it is recommended that when using the signer outside of the
40 // SDK that explicitly escaping the request prior to being signed is preferable,
41 // and will help prevent signature validation errors. This can be done by setting
42 // the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then
43 // call URL.EscapedPath() if Opaque is not set.
45 // If signing a request intended for HTTP2 server, and you're using Go 1.6.2
46 // through 1.7.4 you should use the URL.RawPath as the pre-escaped form of the
47 // request URL. https://github.com/golang/go/issues/16847 points to a bug in
48 // Go pre 1.8 that fails to make HTTP2 requests using absolute URL in the HTTP
49 // message. URL.Opaque generally will force Go to make requests with absolute URL.
50 // URL.RawPath does not do this, but RawPath must be a valid escaping of Path
51 // or url.EscapedPath will ignore the RawPath escaping.
53 // Test `TestStandaloneSign` provides a complete example of using the signer
54 // outside of the SDK and pre-escaping the URI path.
71 "github.com/aws/aws-sdk-go/aws"
72 "github.com/aws/aws-sdk-go/aws/credentials"
73 "github.com/aws/aws-sdk-go/aws/request"
74 "github.com/aws/aws-sdk-go/internal/sdkio"
75 "github.com/aws/aws-sdk-go/private/protocol/rest"
79 authHeaderPrefix = "AWS4-HMAC-SHA256"
80 timeFormat = "20060102T150405Z"
81 shortTimeFormat = "20060102"
83 // emptyStringSHA256 is a SHA256 of an empty string
84 emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
87 var ignoredHeaders = rules{
90 "Authorization": struct{}{},
91 "User-Agent": struct{}{},
92 "X-Amzn-Trace-Id": struct{}{},
97 // requiredSignedHeaders is a whitelist for build canonical headers.
98 var requiredSignedHeaders = rules{
101 "Cache-Control": struct{}{},
102 "Content-Disposition": struct{}{},
103 "Content-Encoding": struct{}{},
104 "Content-Language": struct{}{},
105 "Content-Md5": struct{}{},
106 "Content-Type": struct{}{},
107 "Expires": struct{}{},
108 "If-Match": struct{}{},
109 "If-Modified-Since": struct{}{},
110 "If-None-Match": struct{}{},
111 "If-Unmodified-Since": struct{}{},
113 "X-Amz-Acl": struct{}{},
114 "X-Amz-Copy-Source": struct{}{},
115 "X-Amz-Copy-Source-If-Match": struct{}{},
116 "X-Amz-Copy-Source-If-Modified-Since": struct{}{},
117 "X-Amz-Copy-Source-If-None-Match": struct{}{},
118 "X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
119 "X-Amz-Copy-Source-Range": struct{}{},
120 "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
121 "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
122 "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
123 "X-Amz-Grant-Full-control": struct{}{},
124 "X-Amz-Grant-Read": struct{}{},
125 "X-Amz-Grant-Read-Acp": struct{}{},
126 "X-Amz-Grant-Write": struct{}{},
127 "X-Amz-Grant-Write-Acp": struct{}{},
128 "X-Amz-Metadata-Directive": struct{}{},
129 "X-Amz-Mfa": struct{}{},
130 "X-Amz-Request-Payer": struct{}{},
131 "X-Amz-Server-Side-Encryption": struct{}{},
132 "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
133 "X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
134 "X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
135 "X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
136 "X-Amz-Storage-Class": struct{}{},
137 "X-Amz-Tagging": struct{}{},
138 "X-Amz-Website-Redirect-Location": struct{}{},
139 "X-Amz-Content-Sha256": struct{}{},
142 patterns{"X-Amz-Meta-"},
145 // allowedHoisting is a whitelist for build query headers. The boolean value
146 // represents whether or not it is a pattern.
147 var allowedQueryHoisting = inclusiveRules{
148 blacklist{requiredSignedHeaders},
152 // Signer applies AWS v4 signing to given request. Use this to sign requests
153 // that need to be signed with AWS V4 Signatures.
155 // The authentication credentials the request will be signed against.
156 // This value must be set to sign requests.
157 Credentials *credentials.Credentials
159 // Sets the log level the signer should use when reporting information to
160 // the logger. If the logger is nil nothing will be logged. See
161 // aws.LogLevelType for more information on available logging levels
163 // By default nothing will be logged.
164 Debug aws.LogLevelType
166 // The logger loging information will be written to. If there the logger
167 // is nil, nothing will be logged.
170 // Disables the Signer's moving HTTP header key/value pairs from the HTTP
171 // request header to the request's query string. This is most commonly used
172 // with pre-signed requests preventing headers from being added to the
173 // request's query string.
174 DisableHeaderHoisting bool
176 // Disables the automatic escaping of the URI path of the request for the
177 // siganture's canonical string's path. For services that do not need additional
178 // escaping then use this to disable the signer escaping the path.
180 // S3 is an example of a service that does not need additional escaping.
182 // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
183 DisableURIPathEscaping bool
185 // Disables the automatical setting of the HTTP request's Body field with the
186 // io.ReadSeeker passed in to the signer. This is useful if you're using a
187 // custom wrapper around the body for the io.ReadSeeker and want to preserve
188 // the Body value on the Request.Body.
190 // This does run the risk of signing a request with a body that will not be
191 // sent in the request. Need to ensure that the underlying data of the Body
192 // values are the same.
193 DisableRequestBodyOverwrite bool
195 // currentTimeFn returns the time value which represents the current time.
196 // This value should only be used for testing. If it is nil the default
197 // time.Now will be used.
198 currentTimeFn func() time.Time
200 // UnsignedPayload will prevent signing of the payload. This will only
201 // work for services that have support for this.
205 // NewSigner returns a Signer pointer configured with the credentials and optional
206 // option values provided. If not options are provided the Signer will use its
207 // default configuration.
208 func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer {
210 Credentials: credentials,
213 for _, option := range options {
220 type signingCtx struct {
223 Request *http.Request
227 ExpireTime time.Duration
228 SignedHeaderVals http.Header
230 DisableURIPathEscaping bool
232 credValues credentials.Value
235 formattedShortTime string
240 canonicalHeaders string
241 canonicalString string
242 credentialString string
248 // Sign signs AWS v4 requests with the provided body, service name, region the
249 // request is made to, and time the request is signed at. The signTime allows
250 // you to specify that a request is signed for the future, and cannot be
253 // Returns a list of HTTP headers that were included in the signature or an
254 // error if signing the request failed. Generally for signed requests this value
255 // is not needed as the full request context will be captured by the http.Request
256 // value. It is included for reference though.
258 // Sign will set the request's Body to be the `body` parameter passed in. If
259 // the body is not already an io.ReadCloser, it will be wrapped within one. If
260 // a `nil` body parameter passed to Sign, the request's Body field will be
261 // also set to nil. Its important to note that this functionality will not
262 // change the request's ContentLength of the request.
264 // Sign differs from Presign in that it will sign the request using HTTP
265 // header values. This type of signing is intended for http.Request values that
266 // will not be shared, or are shared in a way the header values on the request
269 // The requests body is an io.ReadSeeker so the SHA256 of the body can be
270 // generated. To bypass the signer computing the hash you can set the
271 // "X-Amz-Content-Sha256" header with a precomputed value. The signer will
272 // only compute the hash if the request header value is empty.
273 func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
274 return v4.signWithBody(r, body, service, region, 0, false, signTime)
277 // Presign signs AWS v4 requests with the provided body, service name, region
278 // the request is made to, and time the request is signed at. The signTime
279 // allows you to specify that a request is signed for the future, and cannot
280 // be used until then.
282 // Returns a list of HTTP headers that were included in the signature or an
283 // error if signing the request failed. For presigned requests these headers
284 // and their values must be included on the HTTP request when it is made. This
285 // is helpful to know what header values need to be shared with the party the
286 // presigned request will be distributed to.
288 // Presign differs from Sign in that it will sign the request using query string
289 // instead of header values. This allows you to share the Presigned Request's
290 // URL with third parties, or distribute it throughout your system with minimal
293 // Presign also takes an exp value which is the duration the
294 // signed request will be valid after the signing time. This is allows you to
295 // set when the request will expire.
297 // The requests body is an io.ReadSeeker so the SHA256 of the body can be
298 // generated. To bypass the signer computing the hash you can set the
299 // "X-Amz-Content-Sha256" header with a precomputed value. The signer will
300 // only compute the hash if the request header value is empty.
302 // Presigning a S3 request will not compute the body's SHA256 hash by default.
303 // This is done due to the general use case for S3 presigned URLs is to share
304 // PUT/GET capabilities. If you would like to include the body's SHA256 in the
305 // presigned request's signature you can set the "X-Amz-Content-Sha256"
306 // HTTP header and that will be included in the request's signature.
307 func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) {
308 return v4.signWithBody(r, body, service, region, exp, true, signTime)
311 func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, isPresign bool, signTime time.Time) (http.Header, error) {
312 currentTimeFn := v4.currentTimeFn
313 if currentTimeFn == nil {
314 currentTimeFn = time.Now
320 Query: r.URL.Query(),
323 isPresign: isPresign,
324 ServiceName: service,
326 DisableURIPathEscaping: v4.DisableURIPathEscaping,
327 unsignedPayload: v4.UnsignedPayload,
330 for key := range ctx.Query {
331 sort.Strings(ctx.Query[key])
334 if ctx.isRequestSigned() {
335 ctx.Time = currentTimeFn()
336 ctx.handlePresignRemoval()
340 ctx.credValues, err = v4.Credentials.Get()
342 return http.Header{}, err
345 ctx.sanitizeHostForHeader()
346 ctx.assignAmzQueryValues()
347 if err := ctx.build(v4.DisableHeaderHoisting); err != nil {
351 // If the request is not presigned the body should be attached to it. This
352 // prevents the confusion of wanting to send a signed request without
353 // the body the request was signed for attached.
354 if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) {
355 var reader io.ReadCloser
358 if reader, ok = body.(io.ReadCloser); !ok {
359 reader = ioutil.NopCloser(body)
365 if v4.Debug.Matches(aws.LogDebugWithSigning) {
366 v4.logSigningInfo(ctx)
369 return ctx.SignedHeaderVals, nil
372 func (ctx *signingCtx) sanitizeHostForHeader() {
373 request.SanitizeHostForHeader(ctx.Request)
376 func (ctx *signingCtx) handlePresignRemoval() {
381 // The credentials have expired for this request. The current signing
382 // is invalid, and needs to be request because the request will fail.
385 // Update the request's query string to ensure the values stays in
386 // sync in the case retrieving the new credentials fails.
387 ctx.Request.URL.RawQuery = ctx.Query.Encode()
390 func (ctx *signingCtx) assignAmzQueryValues() {
392 ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
393 if ctx.credValues.SessionToken != "" {
394 ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
396 ctx.Query.Del("X-Amz-Security-Token")
402 if ctx.credValues.SessionToken != "" {
403 ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
407 // SignRequestHandler is a named request handler the SDK will use to sign
408 // service client request with using the V4 signature.
409 var SignRequestHandler = request.NamedHandler{
410 Name: "v4.SignRequestHandler", Fn: SignSDKRequest,
413 // SignSDKRequest signs an AWS request with the V4 signature. This
414 // request handler should only be used with the SDK's built in service client's
415 // API operation requests.
417 // This function should not be used on its on its own, but in conjunction with
418 // an AWS service client's API operation call. To sign a standalone request
419 // not created by a service client's API operation method use the "Sign" or
420 // "Presign" functions of the "Signer" type.
422 // If the credentials of the request's config are set to
423 // credentials.AnonymousCredentials the request will not be signed.
424 func SignSDKRequest(req *request.Request) {
425 SignSDKRequestWithCurrentTime(req, time.Now)
428 // BuildNamedHandler will build a generic handler for signing.
429 func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler {
430 return request.NamedHandler{
432 Fn: func(req *request.Request) {
433 SignSDKRequestWithCurrentTime(req, time.Now, opts...)
438 // SignSDKRequestWithCurrentTime will sign the SDK's request using the time
439 // function passed in. Behaves the same as SignSDKRequest with the exception
440 // the request is signed with the value returned by the current time function.
441 func SignSDKRequestWithCurrentTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) {
442 // If the request does not need to be signed ignore the signing of the
443 // request if the AnonymousCredentials object is used.
444 if req.Config.Credentials == credentials.AnonymousCredentials {
448 region := req.ClientInfo.SigningRegion
450 region = aws.StringValue(req.Config.Region)
453 name := req.ClientInfo.SigningName
455 name = req.ClientInfo.ServiceName
458 v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) {
459 v4.Debug = req.Config.LogLevel.Value()
460 v4.Logger = req.Config.Logger
461 v4.DisableHeaderHoisting = req.NotHoist
462 v4.currentTimeFn = curTimeFn
464 // S3 service should not have any escaping applied
465 v4.DisableURIPathEscaping = true
467 // Prevents setting the HTTPRequest's Body. Since the Body could be
468 // wrapped in a custom io.Closer that we do not want to be stompped
469 // on top of by the signer.
470 v4.DisableRequestBodyOverwrite = true
473 for _, opt := range opts {
477 curTime := curTimeFn()
478 signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(),
479 name, region, req.ExpireTime, req.ExpireTime > 0, curTime,
483 req.SignedHeaderVals = nil
487 req.SignedHeaderVals = signedHeaders
488 req.LastSignedAt = curTime
491 const logSignInfoMsg = `DEBUG: Request Signature:
492 ---[ CANONICAL STRING ]-----------------------------
494 ---[ STRING TO SIGN ]--------------------------------
496 -----------------------------------------------------`
497 const logSignedURLMsg = `
498 ---[ SIGNED URL ]------------------------------------
501 func (v4 *Signer) logSigningInfo(ctx *signingCtx) {
504 signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String())
506 msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg)
510 func (ctx *signingCtx) build(disableHeaderHoisting bool) error {
511 ctx.buildTime() // no depends
512 ctx.buildCredentialString() // no depends
514 if err := ctx.buildBodyDigest(); err != nil {
518 unsignedHeaders := ctx.Request.Header
520 if !disableHeaderHoisting {
521 urlValues := url.Values{}
522 urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends
523 for k := range urlValues {
524 ctx.Query[k] = urlValues[k]
529 ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
530 ctx.buildCanonicalString() // depends on canon headers / signed headers
531 ctx.buildStringToSign() // depends on canon string
532 ctx.buildSignature() // depends on string to sign
535 ctx.Request.URL.RawQuery += "&X-Amz-Signature=" + ctx.signature
538 authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString,
539 "SignedHeaders=" + ctx.signedHeaders,
540 "Signature=" + ctx.signature,
542 ctx.Request.Header.Set("Authorization", strings.Join(parts, ", "))
548 func (ctx *signingCtx) buildTime() {
549 ctx.formattedTime = ctx.Time.UTC().Format(timeFormat)
550 ctx.formattedShortTime = ctx.Time.UTC().Format(shortTimeFormat)
553 duration := int64(ctx.ExpireTime / time.Second)
554 ctx.Query.Set("X-Amz-Date", ctx.formattedTime)
555 ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
557 ctx.Request.Header.Set("X-Amz-Date", ctx.formattedTime)
561 func (ctx *signingCtx) buildCredentialString() {
562 ctx.credentialString = strings.Join([]string{
563 ctx.formattedShortTime,
570 ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString)
574 func buildQuery(r rule, header http.Header) (url.Values, http.Header) {
575 query := url.Values{}
576 unsignedHeaders := http.Header{}
577 for k, h := range header {
581 unsignedHeaders[k] = h
585 return query, unsignedHeaders
587 func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) {
589 headers = append(headers, "host")
590 for k, v := range header {
591 canonicalKey := http.CanonicalHeaderKey(k)
592 if !r.IsValid(canonicalKey) {
593 continue // ignored header
595 if ctx.SignedHeaderVals == nil {
596 ctx.SignedHeaderVals = make(http.Header)
599 lowerCaseKey := strings.ToLower(k)
600 if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
601 // include additional values
602 ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
606 headers = append(headers, lowerCaseKey)
607 ctx.SignedHeaderVals[lowerCaseKey] = v
609 sort.Strings(headers)
611 ctx.signedHeaders = strings.Join(headers, ";")
614 ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders)
617 headerValues := make([]string, len(headers))
618 for i, k := range headers {
620 if ctx.Request.Host != "" {
621 headerValues[i] = "host:" + ctx.Request.Host
623 headerValues[i] = "host:" + ctx.Request.URL.Host
626 headerValues[i] = k + ":" +
627 strings.Join(ctx.SignedHeaderVals[k], ",")
630 stripExcessSpaces(headerValues)
631 ctx.canonicalHeaders = strings.Join(headerValues, "\n")
634 func (ctx *signingCtx) buildCanonicalString() {
635 ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1)
637 uri := getURIPath(ctx.Request.URL)
639 if !ctx.DisableURIPathEscaping {
640 uri = rest.EscapePath(uri, false)
643 ctx.canonicalString = strings.Join([]string{
646 ctx.Request.URL.RawQuery,
647 ctx.canonicalHeaders + "\n",
653 func (ctx *signingCtx) buildStringToSign() {
654 ctx.stringToSign = strings.Join([]string{
657 ctx.credentialString,
658 hex.EncodeToString(makeSha256([]byte(ctx.canonicalString))),
662 func (ctx *signingCtx) buildSignature() {
663 secret := ctx.credValues.SecretAccessKey
664 date := makeHmac([]byte("AWS4"+secret), []byte(ctx.formattedShortTime))
665 region := makeHmac(date, []byte(ctx.Region))
666 service := makeHmac(region, []byte(ctx.ServiceName))
667 credentials := makeHmac(service, []byte("aws4_request"))
668 signature := makeHmac(credentials, []byte(ctx.stringToSign))
669 ctx.signature = hex.EncodeToString(signature)
672 func (ctx *signingCtx) buildBodyDigest() error {
673 hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
675 includeSHA256Header := ctx.unsignedPayload ||
676 ctx.ServiceName == "s3" ||
677 ctx.ServiceName == "glacier"
679 s3Presign := ctx.isPresign && ctx.ServiceName == "s3"
681 if ctx.unsignedPayload || s3Presign {
682 hash = "UNSIGNED-PAYLOAD"
683 includeSHA256Header = !s3Presign
684 } else if ctx.Body == nil {
685 hash = emptyStringSHA256
687 if !aws.IsReaderSeekable(ctx.Body) {
688 return fmt.Errorf("cannot use unseekable request body %T, for signed request with body", ctx.Body)
690 hashBytes, err := makeSha256Reader(ctx.Body)
694 hash = hex.EncodeToString(hashBytes)
697 if includeSHA256Header {
698 ctx.Request.Header.Set("X-Amz-Content-Sha256", hash)
701 ctx.bodyDigest = hash
706 // isRequestSigned returns if the request is currently signed or presigned
707 func (ctx *signingCtx) isRequestSigned() bool {
708 if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" {
711 if ctx.Request.Header.Get("Authorization") != "" {
718 // unsign removes signing flags for both signed and presigned requests.
719 func (ctx *signingCtx) removePresign() {
720 ctx.Query.Del("X-Amz-Algorithm")
721 ctx.Query.Del("X-Amz-Signature")
722 ctx.Query.Del("X-Amz-Security-Token")
723 ctx.Query.Del("X-Amz-Date")
724 ctx.Query.Del("X-Amz-Expires")
725 ctx.Query.Del("X-Amz-Credential")
726 ctx.Query.Del("X-Amz-SignedHeaders")
729 func makeHmac(key []byte, data []byte) []byte {
730 hash := hmac.New(sha256.New, key)
735 func makeSha256(data []byte) []byte {
741 func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) {
743 start, err := reader.Seek(0, sdkio.SeekCurrent)
748 // ensure error is return if unable to seek back to start of payload.
749 _, err = reader.Seek(start, sdkio.SeekStart)
752 // Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies
753 // smaller than 32KB. Fall back to io.Copy if we fail to determine the size.
754 size, err := aws.SeekerLen(reader)
756 io.Copy(hash, reader)
758 io.CopyN(hash, reader, size)
761 return hash.Sum(nil), nil
764 const doubleSpace = " "
766 // stripExcessSpaces will rewrite the passed in slice's string values to not
767 // contain multiple side-by-side spaces.
768 func stripExcessSpaces(vals []string) {
769 var j, k, l, m, spaces int
770 for i, str := range vals {
771 // Trim trailing spaces
772 for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
775 // Trim leading spaces
776 for k = 0; k < j && str[k] == ' '; k++ {
780 // Strip multiple spaces.
781 j = strings.Index(str, doubleSpace)
788 for k, m, l = j, j, len(buf); k < l; k++ {
797 // End of multiple spaces.
804 vals[i] = string(buf[:m])