Search code examples
nginxhttp-status-code-301

How to redirect if from reddit with nginx?


I post images on reddit by hotlinking images from my site. By allowing reddit to hotlink my images it will display the image on their site but I also want it to redirect if a user clicks on the image link from reddit. How can I do this while allowing hotlinking for reddit only?

Basically I'm trying to mimic the same way tumblr does with their images. Just try posting an image on reddit by hotlinking a tumblr image and it will display the image on their site and if you try to direct access the image it will redirect.

Example: https://www.reddit.com/user/HeavenlyTasty/comments/vqgn9r/demo_pic/

So far I thought of using nginx http referrer module but that doesn't achieve what I want it to do since it doesn't redirect if coming from reddit.

Anyone know another method to do this?

location ~* ^/(?<filenum>.*)\.(jpeg|jpg|png|webp)$ {
    proxy_pass http://195.xxx.xxx.xxx:7492;
    valid_referers none blocked server_names
               ~\.reddit\.;
    if ($invalid_referer) {
    add_header Cache-Control "no-cache";
    return 301 https://example.com/$filenum; 
}

Solution

  • Here is a reverse engineered tumblr behavior. Decision to serve an image file or an HTML page is made according to the Accept HTTP request header. The trick is that this header value will vary depending on how is it requested - being entered at the browser address bar (or following the <a> tag hyperlink) or being referred via the src attribute of the <img> tag. Here is a (somewhat inaccurate) sample list for various browsers from MDN and here are the actual Accept header values for several browsers (checked at the time of writing):

    • Chrome 102

      <a> tag:

      text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
      

      <img> tag:

      image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
      
    • Firefox 102

      <a> tag:

      text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
      

      <img> tag:

      image/avif,image/webp,*/*
      
    • Safari 14.1 (MacOS Big Sur)

      <a> tag:

      text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
      

      <img> tag:

      image/webp,image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5
      
    • Internet Explorer 11

      <a> tag:

      text/html, application/xhtml+xml, */*
      

      <img> tag:

      image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5
      

    As you can see, a certain pattern can be observed for all browsers Accept header value. When an image is requested from the address bar (or via the <a> HTML tag), a text/html item is present in the accepted MIME types list; when an image is referred via src attibute of the <img> HTML tag, accepted MIME types list consists of image/<MIME_subtype> elements and the */* element (except the Safari, where an aditional video/* item is present). If accepted MIME types list from the Accept header contains a text/html item, tumblr will serve such a request with an HTML page (except if the User-Agent header contains one of the well-known values for download managers, e.g. curl, wget, etc. - in this case, the server will still return the image). Note that tumblr won't give you a redirect to the HTML file. Instead it will serve the request with HTML file. The same request (with another Accept header) will be served later with the actual image file. Behaving like that, a properly configured server should warn the client browser (or intermediate proxy servers that may be present along the request/response route) that the content of the response may vary depending on certain request headers. The tumblr behaves exactly like that, adding the Vary: Accept header to the response. Here is an example requesting the same URL using two different Accept headers (User-Agent header removed from the request due to the aforementioned reasons):

    > curl -I --http1.1 -H "Accept: */*" -H "User-Agent:" https://64.media.tumblr.com/f34905b055dc7fe1d7370bbea305662a/b3dcc022e27e2c22-f0/s1280x1920/84a7c043ea887caa2febb83ccd73c3c173a6beae.jpg
    HTTP/1.1 200 OK
    Server: nginx
    Date: Tue, 05 Jul 2022 01:37:31 GMT
    Content-Type: image/jpeg
    Content-Length: 56077
    Last-Modified: Fri, 01 Jul 2022 12:15:02 GMT
    Etag: "9a02a2354a0fc8847dbddc91ff869735-1498089600-d32ddc9"
    Content-Disposition: inline; filename="tumblr_f34905b055dc7fe1d7370bbea305662a_84a7c043_1280.jpg"
    Cache-Control: max-age=315360000
    ...
    
    > curl -I --http1.1 -H "Accept: text/html,*/*" -H "User-Agent:" https://64.media.tumblr.com/f34905b055dc7fe1d7370bbea305662a/b3dcc022e27e2c22-f0/s1280x1920/84a7c043ea887caa2febb83ccd73c3c173a6beae.jpg
    HTTP/1.1 200 OK
    Server: nginx
    Date: Tue, 05 Jul 2022 01:37:52 GMT
    Content-Type: text/html; charset=utf-8
    Content-Length: 17267
    Vary: Accept-Encoding
    Vary: Accept
    ETag: W/"4373-SQYv0QHYV7hXuB2cQlEhOw0Czx4"
    Cache-Control: private, max-age=0
    ...
    

    Issuing a 301 redirect with the Cache-Control: no-cache header can be an alternative; however, I'd rather use 302 temporary redirect instead:

    map $invalid_referer $redirect {
        ''              $check_accept;
        default         1;
    }
    map $http_accept $check_accept {
        ~\btext/html\b  1;
    }
    
    server {
        ...
        location ~* ^/(?<filenum>.*)\.(jpe?g|png|webp)$ {
            proxy_pass http://195.xxx.xxx.xxx:7492;
            valid_referers none blocked server_names ~\.reddit\.;
            if ($redirect) {
                return 302 https://example.com/$filenum;
            }
        }
    

    To completely mimic tumblr behavior you can try internal rewrite ... last instead of redirect (the map blocks will be the same as from the previous example):

    server {
        ...
        location ~* ^/(?<filenum>.*)\.(jpe?g|png|webp)$ {
            proxy_pass http://195.xxx.xxx.xxx:7492;
            valid_referers none blocked server_names ~\.reddit\.;
            if ($redirect) {
                set $vary Accept;
                rewrite ^ /$filenum last;
            }
        }
        location ... {
            # location where rewritten request will be actually processed
            add_header Vary $vary;
            ...