- attachment:imgproxy.py of EfficientImageServing
Attachment 'imgproxy.py'
Download 1 """A module to redirect image file loading to imgproxy for more
2 efficient and smaller image formats.
3
4 This module defines a util.imgproxy_url class that can be
5 used in place of download_url in img tags.
6
7 E.G. replace
8
9 <img tal:attributes="src file/download_url; alt file/name"
10 loading="lazy">
11
12 with:
13
14 <img tal:attributes="src
15 python:utils.imgproxy_url(file, ['rs:fit:400:300',
16 'preset:mark' ]);
17 alt file/name" loading="lazy">
18
19 to load the image at a size of 400x300 and have a watermark
20 placed on it.
21
22 It is controlled by the following settings in
23 extensions/config.ini:
24
25 -----
26 [imgproxy]
27
28 # URL for imgproxy serving up your tracker's images.
29 # Must have trailing /.
30 # If not set, regular download_url for the attached file is generated.
31 url = https://example.net/tracker/imgproxy/
32
33 # Hex encoded random key used for signature hash.
34 # Can be stored in a file. Use file://filename relative to
35 # extensions directory.
36 # Shell command to generate suitable string is:
37 # echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')
38 #
39 # If not set, unsigned requests are generated for imgproxy.
40 # if Accepted by imgproxy, this allows attached files to
41 # be accessed by unauthorized clients.
42 key = 736563726574
43
44 # Hex encoded random salt used for signature hash.
45 # See key documentation for details.
46 salt = file://../secrets/imgproxy.salt
47
48 # Integer number of seconds that the image url is cached/valid
49 # (default 5 seconds). Must be > 0. If you are lazy loading
50 # the images on a page with a lot of images, you need to
51 # increase this time otherwise images will fail to load.
52 #ttl = 10
53
54 # Add a watermark to images. Assumes IMGPROXY_WATERMARK_DATA
55 # or other watermark source is defined in imgproxy environment
56 # (default False)
57 #watermark = on
58 ------
59
60 See: https://imgproxy.net/
61 https://github.com/imgproxy/imgproxy
62 """
63
64 import binascii
65 import base64
66 import hashlib
67 import hmac
68 import logging
69 import time
70
71 from roundup.anypy.strings import b2s, bs2b
72 from roundup.anypy.urllib_ import quote
73 from roundup.configuration import BooleanOption, \
74 IntegerNumberOption, \
75 InvalidOptionError, \
76 OptionValueError, \
77 SecretOption, \
78 WebUrlOption
79
80 try:
81 fromhex = bytes.fromhex
82 except AttributeError:
83 fromhex = binascii.unhexlify
84
85 logger = logging.getLogger('extension')
86
87
88 def make_signature(processing_url, key_hex, salt_hex):
89 """Generate the signature for the processing_url with key/salt
90
91 Used to limit access to the image url passed to
92 imgproxy by signing the processing_url.
93 """
94 key = fromhex(key_hex)
95 salt = fromhex(salt_hex)
96
97 path = bs2b(processing_url)
98 digest = hmac.new(key, msg=salt+path,
99 digestmod=hashlib.sha256).digest()
100 return base64.urlsafe_b64encode(digest).rstrip(b"=")
101
102
103 def get_setting(config, setting, default=""):
104 """Get a setting from a detector or extension url with
105 default value if not defined.
106 """
107 try:
108 return config[setting]
109 except InvalidOptionError:
110 return default
111
112
113 def imgproxy_url(file_obj, render_options=None):
114 """Take a file object (with a download_url method) and an
115 optional list of imgproxy processing
116 options. Specified options take precedence over the
117 ones added by this function (filename, watermark, expires).
118
119 Return a url to the imgproxy image server that has
120 access to the tracker's db/files/file directory
121 tree. Processing directives and other doc at:
122 https://docs.imgproxy.net/.
123 """
124
125 ext_config = file_obj._db.config.ext
126
127 url_prefix = get_setting(ext_config, 'IMGPROXY_URL', None)
128
129 if url_prefix is None:
130 # can't use imgproxy without the url.
131 # this is a safety since it should never happen
132 # as this function is not called if url not defined.
133 return file_obj.download_url()
134
135 key = get_setting(ext_config, 'IMGPROXY_KEY', None)
136 salt = get_setting(ext_config, 'IMGPROXY_SALT', None)
137 ttl = get_setting(ext_config, 'IMGPROXY_TTL', 5)
138 watermark = get_setting(ext_config, 'IMGPROXY_WATERMARK', False)
139
140 if not render_options:
141 # basic resize 400x300 no shrink or expand
142 render_options = ["rs:fill:400:300:0:0"]
143
144 # Insert default/configured options at start of
145 # render_options. Options later in the URL take
146 # precedence. So watermark can be overridden by
147 # 'watermark:0' set in render_options passed to
148 # imgproxy_url().
149
150 # Set filename to basename of image file.
151 # Suffix is added by imgproxy to match image format.
152 name = file_obj._klass.get(file_obj._nodeid, 'name')
153 render_options.insert(0, "filename:%s" % quote(name[:name.rindex('.')]))
154
155 # Add the watermark. Assumes IMGPROXY_WATERMARK_DATA or
156 # other watermark definition is defined in imgproxy
157 # environment.
158 if watermark:
159 render_options.insert(0, "watermark:0.3:sowe")
160
161 if key and salt:
162 # Set URL expiration time.
163 # If there is no signature hash, any user can change the
164 # expire time and resend the URL. So don't bother
165 # setting it.
166 render_options.insert(0, "expires:%i" % (time.time() + ttl))
167
168 rendering = "/".join(render_options)
169 local_url = "local:///%s" % file_obj._db.subdirFilename(
170 file_obj._klass.classname,
171 file_obj._nodeid)
172
173 processing_url = "/%(rendering)s/plain/%(local_url)s" % {
174 "rendering": rendering, "local_url": local_url, }
175
176 if key and salt:
177 signature = b2s(make_signature(processing_url, key, salt))
178 else:
179 signature = "not_signed"
180
181 imgproxy_url = "%(url_prefix)s%(signature)s%(processing_url)s" % {
182 "url_prefix": url_prefix, "signature": signature,
183 "processing_url": processing_url}
184
185 return imgproxy_url
186
187
188 def init(instance):
189 # verify options values meet standards.
190 try:
191 instance.config.ext.update_option(
192 'IMGPROXY_URL', WebUrlOption,
193 description="Url for imgproxy server.")
194 except InvalidOptionError:
195 # It's not set, that's ok. Just call download_url()
196 # and skip everything else in this module.
197 instance.registerUtil(
198 'imgproxy_url',
199 lambda file_obj, _ignore=None: file_obj.download_url())
200 return
201
202 try:
203 option_name = 'IMGPROXY_KEY'
204 instance.config.ext.update_option(
205 option_name, SecretOption,
206 description="Random HMAC key, hex encoded.")
207
208 try:
209 fromhex(instance.config.ext[option_name])
210 except ValueError:
211 raise OptionValueError(
212 instance.config.ext.options[option_name],
213 instance.config.ext[option_name],
214 "in extensions/config.ini. Is not a valid hex encoded string."
215 )
216 except InvalidOptionError:
217 # It's not set, that's ok.
218 pass
219
220 try:
221 option_name = 'IMGPROXY_SALT'
222 instance.config.ext.update_option(
223 option_name, SecretOption,
224 description="Random salt used with HMAC key, hex encoded.")
225
226 try:
227 fromhex(instance.config.ext[option_name])
228 except ValueError:
229 raise OptionValueError(
230 instance.config.ext.options[option_name],
231 instance.config.ext[option_name],
232 "in extensions/config.ini. Is not a valid hex encoded string."
233 )
234 except InvalidOptionError:
235 # It's not set, that's ok.
236 pass
237
238 try:
239 instance.config.ext.update_option(
240 'IMGPROXY_TTL', IntegerNumberOption, default=5,
241 description="Lifetime in integer seconds of image URL.")
242 except InvalidOptionError:
243 # It's not set, that's ok. Default is 5 seconds.
244 pass
245
246 try:
247 instance.config.ext.update_option(
248 'IMGPROXY_WATERMARK', BooleanOption, default=False,
249 description="Print watermark on images.")
250 except InvalidOptionError:
251 # It's not set, no watermark.
252 pass
253
254 instance.registerUtil('imgproxy_url', imgproxy_url)
255
256
257 if __name__ == '__main__':
258 """ Test make_signature() using imgproxy example signature
259 settings.
260 """
261 url = ('/rs:fill:300:400:0/g:sm/aHR0cDovL2V4YW1w/bGUuY29tL2l'
262 'tYWdl/cy9jdXJpb3NpdHku/anBn.png')
263 print(url)
264
265 s = make_signature(url, '736563726574', '68656C6C6F')
266
267 print(s)
268
269 assert b2s(s) == "oKfUtW34Dvo2BGQehJFR4Nr0_rIjOtdtzJ3QFsUcXH8"
270
271 print("Test passed, no errors")
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.