|
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 | + These scheme is much stronger when the application code itself is also signed and |
| 51 | + verified, but that is an exercise for the packaging tools. |
| 52 | +
|
| 53 | + When assets are loaded this way, the `@asset_hash` term is also used as the key in |
| 54 | + the cache. This has the additional benefit of allowing you to pre-compute |
| 55 | + the graph itself, using the correct keys for the correct assets. |
| 56 | +
|
| 57 | + ## Full example |
| 58 | +
|
| 59 | + defmodule MyApp.MyScene do |
| 60 | + use Scenic.Scene |
| 61 | +
|
| 62 | + # build the path to the static asset file (compile time) |
| 63 | + @asset_path :code.priv_dir(:my_app) |> Path.join("/static/images/asset.jpg") |
| 64 | +
|
| 65 | + # pre-compute the hash (compile time) |
| 66 | + @asset_hash Scenic.Cache.Hash.file!( @asset_path, :sha ) |
| 67 | +
|
| 68 | + # build a graph that uses the asset (compile time) |
| 69 | + @graph Scenic.Graph.build() |
| 70 | + |> rect( {100, 100}, fill: {:image, @asset_hash} ) |
| 71 | +
|
| 72 | + def init( _, _ ) { |
| 73 | + # load the asset into the cache (run time) |
| 74 | + Scenic.Cache.File.load(@asset_path, @asset_hash) |
| 75 | +
|
| 76 | + # push the graph. (run time) |
| 77 | + push_graph(@graph) |
| 78 | +
|
| 79 | + {:ok, @graph} |
| 80 | + end |
| 81 | +
|
| 82 | + end |
9 | 83 | """
|
10 | 84 | alias Scenic.Cache
|
11 | 85 | alias Scenic.Cache.Hash
|
12 | 86 |
|
13 |
| - # import IEx |
14 |
| - |
15 | 87 | # --------------------------------------------------------
|
| 88 | + @doc """ |
| 89 | + Load a file directly into the cache. |
| 90 | +
|
| 91 | + Parameters: |
| 92 | + * `path` - the path to the asset file |
| 93 | + * `hash` - the pre-computed hash of the file |
| 94 | + * `opts` - a list of options. See below. |
| 95 | +
|
| 96 | + Options: |
| 97 | + * `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160`. If the hash option is not set, it will use `:sha` by default. |
| 98 | + * `scope` - Explicitly set the scope of the asset in the cache. |
| 99 | + * `decompress` - if `true` - decompress the data (zlib) after reading and verifying the hash. |
| 100 | +
|
| 101 | + On success, returns |
| 102 | + `{:ok, cache_key}` |
| 103 | +
|
| 104 | + The key in the cache will be the hash of the file. |
| 105 | + """ |
16 | 106 | def load(path, hash, opts \\ [])
|
17 | 107 |
|
18 | 108 | # insecure loading. Loads file blindly even it is altered
|
19 | 109 | # don't recommend doing this in production. Better to embed the expected
|
20 | 110 | # hashes. Is also slower because it has to load the file and compute the hash
|
21 | 111 | # to use as a key even it is is already loaded into the cache.
|
22 | 112 | def load(path, :insecure, opts) do
|
| 113 | + if Mix.env() != :test do |
| 114 | + IO.puts("WARNING: Cache asset loaded as :insecure \"#{path}\"") |
| 115 | + end |
| 116 | + |
23 | 117 | with {:ok, data} <- read(path, :insecure, opts) do
|
24 | 118 | hash = Hash.binary(data, opts[:hash] || :sha)
|
25 | 119 |
|
@@ -53,13 +147,36 @@ defmodule Scenic.Cache.File do
|
53 | 147 | end
|
54 | 148 |
|
55 | 149 | # --------------------------------------------------------
|
| 150 | + @doc """ |
| 151 | + Read a file into memory. |
| 152 | +
|
| 153 | + The reason you would use this instead of File.read is to verify the data against |
| 154 | + a pre-computed hash. |
| 155 | +
|
| 156 | + Parameters: |
| 157 | + * `path` - the path to the asset file |
| 158 | + * `hash` - the pre-computed hash of the file |
| 159 | + * `opts` - a list of options. See below. |
| 160 | +
|
| 161 | + Options: |
| 162 | + * `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160`. If the hash option is not set, it will use `:sha` by default. |
| 163 | + * `decompress` - if `true` - decompress the data (zlib) after reading and verifying the hash. |
| 164 | +
|
| 165 | + On success, returns |
| 166 | + `{:ok, data}` |
| 167 | + """ |
| 168 | + |
56 | 169 | def read(path, hash, opts \\ [])
|
57 | 170 |
|
58 | 171 | # insecure read
|
59 | 172 | # don't recommend doing this in production. Better to embed the expected
|
60 | 173 | # hashes. Is also slower because it has to load the file and compute the hash
|
61 | 174 | # to use as a key even it is is already loaded into the cache.
|
62 | 175 | def read(path, :insecure, opts) do
|
| 176 | + if Mix.env() != :test do |
| 177 | + IO.puts("WARNING: Cache asset read as :insecure \"#{path}\"") |
| 178 | + end |
| 179 | + |
63 | 180 | with {:ok, data} <- File.read(path) do
|
64 | 181 | do_unzip(data, opts)
|
65 | 182 | else
|
|
0 commit comments