|
5 | 5 |
|
6 | 6 | defmodule Scenic.Cache.File do
|
7 | 7 | @moduledoc """
|
8 |
| - Simple functions to load a file, following the hashing rules |
| 8 | + Helpers for loading files directly into the cache. |
| 9 | +
|
| 10 | + Static assets such as fonts and images are usually stored as files on the local |
| 11 | + storage device. These files need to be loaded into the cache in order to be used |
| 12 | + by the various parts of Scenic. |
| 13 | +
|
| 14 | + ## Where to store your static file assets |
| 15 | +
|
| 16 | + You can store your assets anywhere in your app's `priv/` directory. This directory is |
| 17 | + special in the sense that the Elixir build system knows to copy its contents into the |
| 18 | + correct final build location. How you organize your assets inside of `priv/` is up to you. |
| 19 | +
|
| 20 | + my_app/ |
| 21 | + priv/ |
| 22 | + static/ |
| 23 | + images/ |
| 24 | + asset.jpg |
| 25 | +
|
| 26 | + |
| 27 | + At compile time you need to build the actual path of your asset by combining |
| 28 | + the build directory with the partial path inside of `priv/` |
| 29 | +
|
| 30 | + Example |
| 31 | +
|
| 32 | + path = :code.priv_dir(:my_app) |
| 33 | + |> Path.join("/static/images/asset.jpg") |
| 34 | +
|
| 35 | + You can do this at either compile time or runtime. |
| 36 | +
|
| 37 | + ## Security |
| 38 | +
|
| 39 | + A lesson learned the hard way is that static assets (fonts, images, etc) that your app |
| 40 | + loads out of storage can easily become attack vectors. |
| 41 | +
|
| 42 | + These formats are complicated! There is no guarantee (on any system) that a malformed |
| 43 | + asset will not cause an error in the C code that interprets it. Again - these are complicated |
| 44 | + and the renderers need to be fast... |
| 45 | +
|
| 46 | + The solution is to compute a SHA hash of these files during build-time of your |
| 47 | + and to store the result in your applications code itself. Then during run time, you |
| 48 | + compare then pre-computed hash against the run-time of the asset being loaded. |
| 49 | +
|
| 50 | + Please take advantage of the helper modules [`Cache.File`](Scenic.Cache.File.html), |
| 51 | + [`Cache.Term`](Scenic.Cache.Term.html), and [`Cache.Hash`](Scenic.Cache.Hash.html) to |
| 52 | + do this for you. These modules load files and insert them into the cache while checking |
| 53 | + a precomputed hash. |
| 54 | +
|
| 55 | + These scheme is much stronger when the application code itself is also signed and |
| 56 | + verified, but that is an exercise for the packaging tools. |
| 57 | +
|
| 58 | + When assets are loaded this way, the `@asset_hash` term is also used as the key in |
| 59 | + the cache. This has the additional benefit of allowing you to pre-compute |
| 60 | + the graph itself, using the correct keys for the correct assets. |
| 61 | +
|
| 62 | + ## Full example |
| 63 | +
|
| 64 | + defmodule MyApp.MyScene do |
| 65 | + use Scenic.Scene |
| 66 | +
|
| 67 | + # build the path to the static asset file (compile time) |
| 68 | + @asset_path :code.priv_dir(:my_app) |> Path.join("/static/images/asset.jpg") |
| 69 | +
|
| 70 | + # pre-compute the hash (compile time) |
| 71 | + @asset_hash Scenic.Cache.Hash.file!( @asset_path, :sha ) |
| 72 | +
|
| 73 | + # build a graph that uses the asset (compile time) |
| 74 | + @graph Scenic.Graph.build() |
| 75 | + |> rect( {100, 100}, fill: {:image, @asset_hash} ) |
| 76 | +
|
| 77 | + def init( _, _ ) { |
| 78 | + # load the asset into the cache (run time) |
| 79 | + Scenic.Cache.File.load(@asset_path, @asset_hash) |
| 80 | +
|
| 81 | + # push the graph. (run time) |
| 82 | + push_graph(@graph) |
| 83 | +
|
| 84 | + {:ok, @graph} |
| 85 | + end |
| 86 | +
|
| 87 | + end |
9 | 88 | """
|
| 89 | + |
10 | 90 | alias Scenic.Cache
|
11 | 91 | alias Scenic.Cache.Hash
|
12 | 92 |
|
13 |
| - # import IEx |
14 |
| - |
15 | 93 | # --------------------------------------------------------
|
| 94 | + @doc """ |
| 95 | + Load a file directly into the cache. |
| 96 | +
|
| 97 | + Parameters: |
| 98 | + * `path` - the path to the asset file |
| 99 | + * `hash` - the pre-computed hash of the file |
| 100 | + * `opts` - a list of options. See below. |
| 101 | +
|
| 102 | + Options: |
| 103 | + * `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160` |
| 104 | + * `scope` - Explicitly set the scope of the asset in the cache. |
| 105 | +
|
| 106 | + On success, returns |
| 107 | + `{:ok, cache_key}` |
| 108 | +
|
| 109 | + The key in the cache will be the hash of the file. |
| 110 | + """ |
16 | 111 | def load(path, hash, opts \\ [])
|
17 | 112 |
|
18 | 113 | # insecure loading. Loads file blindly even it is altered
|
19 | 114 | # don't recommend doing this in production. Better to embed the expected
|
20 | 115 | # hashes. Is also slower because it has to load the file and compute the hash
|
21 | 116 | # to use as a key even it is is already loaded into the cache.
|
22 | 117 | def load(path, :insecure, opts) do
|
| 118 | + IO.puts "WARNING: Cache asset loaded as :insecure \"#{path}\"" |
23 | 119 | with {:ok, data} <- read(path, :insecure, opts) do
|
24 | 120 | hash = Hash.binary(data, opts[:hash] || :sha)
|
25 | 121 |
|
@@ -53,13 +149,33 @@ defmodule Scenic.Cache.File do
|
53 | 149 | end
|
54 | 150 |
|
55 | 151 | # --------------------------------------------------------
|
| 152 | + @doc """ |
| 153 | + Read a file into the memory. |
| 154 | +
|
| 155 | + The reason you would use this instead of File.read is to verify the file against |
| 156 | + a pre-computed hash. |
| 157 | +
|
| 158 | + Parameters: |
| 159 | + * `path` - the path to the asset file |
| 160 | + * `hash` - the pre-computed hash of the file |
| 161 | + * `opts` - a list of options. See below. |
| 162 | +
|
| 163 | + Options: |
| 164 | + * `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160` |
| 165 | + * `decompress` - if true - decompress the data (zlib) after reading and verifying the hash. |
| 166 | +
|
| 167 | + On success, returns |
| 168 | + `{:ok, data}` |
| 169 | + """ |
| 170 | + |
56 | 171 | def read(path, hash, opts \\ [])
|
57 | 172 |
|
58 | 173 | # insecure read
|
59 | 174 | # don't recommend doing this in production. Better to embed the expected
|
60 | 175 | # hashes. Is also slower because it has to load the file and compute the hash
|
61 | 176 | # to use as a key even it is is already loaded into the cache.
|
62 | 177 | def read(path, :insecure, opts) do
|
| 178 | + IO.puts "WARNING: Cache asset read as :insecure \"#{path}\"" |
63 | 179 | with {:ok, data} <- File.read(path) do
|
64 | 180 | do_unzip(data, opts)
|
65 | 181 | else
|
|
0 commit comments