Roundup Tracker

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.
  • [get | view] (2023-03-14 02:29:34, 8.4 KB) [[attachment:imgproxy.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.