diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index a00d36fa87..e7cd61d8d2 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -247,6 +247,13 @@ development postgresql user. `tools/provision` also invokes `tools/do-destroy-rebuild-database` to create the actual database with its schema. +### Thumbor and thumbnailing + +We use Thumbor, a popular open source thumbnailing server, to serve +images (both for inline URL previews and serving uploaded image +files). See [our thumbnailing docs](../subsystems/thumbnailing.html) +for more details on how this works. + ### Nagios Nagios is an optional component used for notifications to the system diff --git a/docs/subsystems/index.rst b/docs/subsystems/index.rst index 6a00ec3e1b..ff96980cdf 100644 --- a/docs/subsystems/index.rst +++ b/docs/subsystems/index.rst @@ -37,6 +37,7 @@ Subsystems Documentation documentation conversion input-pills + thumbnailing presence unread_messages billing diff --git a/docs/subsystems/thumbnailing.md b/docs/subsystems/thumbnailing.md new file mode 100644 index 0000000000..34cd59b9fe --- /dev/null +++ b/docs/subsystems/thumbnailing.md @@ -0,0 +1,80 @@ +# Thumbnailing + +There are two key places one would naturally want to thumbnail images +in a team chat application like Zulip: + +* On the server-side, when serving inline image and URL previews in + the bodies of messages. This is very important for Zulip's network + performance of low-bandwidth networks. +* In mobile apps, to avoid uploading full-size images on a mobile + network (which Zulip does not yet implement), + +Our server-side thumbnailing system is powered by [thumbor][], a +popular open source server for serving images and thumbnailing them. + +Thumbor is responsible for a few things in Zulip: + +* Serving all image content over HTTPS, even if the original/upstream + image was hosted on HTTP (this was previously done by `camo` in + older versions of Zulip). This is important to avoid mixed-content + warnings from browsers (which look very bad), and does have some + real security benefit in protecting our users from malicious + content. +* Minimizing potentially unnecessary bandwidth that might be used in + communication between the Zulip server and clients. Before we + introduced this feature, + +Thumbor handles a lot of details for us, varying from signing of +thumbnailing URLs, to caching for DoS prevention. + +It is configured via the `THUMBOR_URL` setting in +`/etc/zulip/settings.py`; you can host Thumbor on the same machine as +the Zulip server, or a remote server (which is better for isolation, +since security bugs in image-processing libraries have in the past +been a common attack vector). + +In order to avoid putting the raw Thumbor URLs (which have a complex +encoding that we might want to change over time) into Zulip messages, +we instead encoding in markdown URLs of the form +`/thumbnail/?url=https://example.com/image.png&size=thumbnail` as the +`src` in our image tags, and that URL serves a +(configuration-dependent) redirect to the actual image hosted on +thumbor. + +The thumbnailing system is used for any images that appear in the +bodies of Zulip messages (i.e. both images linked to by users, as well +as uploaded image files.). We exclude a few special image sources +(e.g. youtube stills) only because they are already thumbnailed. + +For uploaded image files, we enforce the same security policy on +thumbnail URLs that we do for the uploaded files themselves. + +A correct client implementation interacting with the thumbnailing +system should do the following: + +* For serving the thumbnailed to 100px height version of images, + nothing special is required; the client just needs to display the + `src=` value in the `` tag in the rendered message HTML. +* For displaying a "full-size" version of an image (e.g. to use in a + lightbox), the client can access the `data-fullsize-src` attribute + on the `` tag; this will contain the URL for a full-size + version. +* Ideally, when clicking on an image to switch from the thumbnail to + the full-size / lightbox size, the client should immediately display + the thumbnailed (low resolution) version and in parallel fetch the + full-size version in the background, transparently swapping it into + place once the full size version is available. This provides a + slick user experience where the user doesn't see a loading state, + and instead just sees the image focus a few hundred milliseconds + after clicking the image. + + +## Avatars, realm icons, and custom emoji + +Currently, these user-uploaded content are thumbnailed by Zulip's +internal file-upload code, in part because they change rarely and +don't have the same throughput/performance requirements as +user-uploaded files. We may later convert them to use thumbor as +well. + +[thumbor]: https://github.com/thumbor/thumbor diff --git a/zerver/lib/bugdown/__init__.py b/zerver/lib/bugdown/__init__.py index aa5bd6836a..47d39f7a68 100644 --- a/zerver/lib/bugdown/__init__.py +++ b/zerver/lib/bugdown/__init__.py @@ -227,6 +227,8 @@ def add_a( a.set("data-id", data_id) img = markdown.util.etree.SubElement(a, "img") if is_thumbor_enabled() and use_thumbnails: + # See docs/thumbnailing.md for some high-level documentation. + # # We strip leading '/' from relative URLs here to ensure # consistency in what gets passed to /thumbnail url = url.lstrip('/') diff --git a/zerver/lib/thumbnail.py b/zerver/lib/thumbnail.py index c83fc2919b..44d95ae629 100644 --- a/zerver/lib/thumbnail.py +++ b/zerver/lib/thumbnail.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html import base64 import os import sys diff --git a/zerver/views/thumbnail.py b/zerver/views/thumbnail.py index cc75e12401..2a676721ff 100644 --- a/zerver/views/thumbnail.py +++ b/zerver/views/thumbnail.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html from django.shortcuts import redirect from django.utils.translation import ugettext as _ from django.http import HttpRequest, HttpResponse, HttpResponseForbidden diff --git a/zthumbor/loaders/zloader.py b/zthumbor/loaders/zloader.py index afdfa43372..7387242333 100644 --- a/zthumbor/loaders/zloader.py +++ b/zthumbor/loaders/zloader.py @@ -1,3 +1,4 @@ +# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html from __future__ import absolute_import from six.moves import urllib