integrity.go (3367B)
1 // Copyright 2018 The Hugo Authors. All rights reserved.
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 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package integrity
15
16 import (
17 "crypto/md5"
18 "crypto/sha256"
19 "crypto/sha512"
20 "encoding/base64"
21 "encoding/hex"
22 "fmt"
23 "hash"
24 "html/template"
25 "io"
26
27 "github.com/gohugoio/hugo/resources/internal"
28
29 "github.com/gohugoio/hugo/resources"
30 "github.com/gohugoio/hugo/resources/resource"
31 )
32
33 const defaultHashAlgo = "sha256"
34
35 // Client contains methods to fingerprint (cachebusting) and other integrity-related
36 // methods.
37 type Client struct {
38 rs *resources.Spec
39 }
40
41 // New creates a new Client with the given specification.
42 func New(rs *resources.Spec) *Client {
43 return &Client{rs: rs}
44 }
45
46 type fingerprintTransformation struct {
47 algo string
48 }
49
50 func (t *fingerprintTransformation) Key() internal.ResourceTransformationKey {
51 return internal.NewResourceTransformationKey("fingerprint", t.algo)
52 }
53
54 // Transform creates a MD5 hash of the Resource content and inserts that hash before
55 // the extension in the filename.
56 func (t *fingerprintTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
57 h, err := newHash(t.algo)
58 if err != nil {
59 return err
60 }
61
62 var w io.Writer
63 if rc, ok := ctx.From.(io.ReadSeeker); ok {
64 // This transformation does not change the content, so try to
65 // avoid writing to To if we can.
66 defer rc.Seek(0, 0)
67 w = h
68 } else {
69 w = io.MultiWriter(h, ctx.To)
70 }
71
72 io.Copy(w, ctx.From)
73 d, err := digest(h)
74 if err != nil {
75 return err
76 }
77
78 ctx.Data["Integrity"] = integrity(t.algo, d)
79 ctx.AddOutPathIdentifier("." + hex.EncodeToString(d[:]))
80 return nil
81 }
82
83 func newHash(algo string) (hash.Hash, error) {
84 switch algo {
85 case "md5":
86 return md5.New(), nil
87 case "sha256":
88 return sha256.New(), nil
89 case "sha384":
90 return sha512.New384(), nil
91 case "sha512":
92 return sha512.New(), nil
93 default:
94 return nil, fmt.Errorf("unsupported crypto algo: %q, use either md5, sha256, sha384 or sha512", algo)
95 }
96 }
97
98 // Fingerprint applies fingerprinting of the given resource and hash algorithm.
99 // It defaults to sha256 if none given, and the options are md5, sha256 or sha512.
100 // The same algo is used for both the fingerprinting part (aka cache busting) and
101 // the base64-encoded Subresource Integrity hash, so you will have to stay away from
102 // md5 if you plan to use both.
103 // See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
104 func (c *Client) Fingerprint(res resources.ResourceTransformer, algo string) (resource.Resource, error) {
105 if algo == "" {
106 algo = defaultHashAlgo
107 }
108
109 return res.Transform(&fingerprintTransformation{algo: algo})
110 }
111
112 func integrity(algo string, sum []byte) template.HTMLAttr {
113 encoded := base64.StdEncoding.EncodeToString(sum)
114 return template.HTMLAttr(algo + "-" + encoded)
115 }
116
117 func digest(h hash.Hash) ([]byte, error) {
118 sum := h.Sum(nil)
119 return sum, nil
120 }