001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020
021package org.shredzone.commons.gravatar;
022
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035import org.springframework.beans.BeansException;
036import org.springframework.util.FileCopyUtils;
037import org.springframework.web.servlet.FrameworkServlet;
038
039/**
040 * A servlet that proxies requests to the Gravatar server, and caches the image results.
041 * <p>
042 * By using this servlet instead of immediate requests to Gravatar, page loading speed is
043 * kept high even if the Gravatar servers are currently under high load or unreachable.
044 * Additionally, privacy is kept because the visitor's IP and browser headers are kept
045 * hidden from the Gravatar servers.
046 *
047 * @author Richard "Shred" Körber
048 */
049public class GravatarCacheServlet extends FrameworkServlet {
050    private static final long serialVersionUID = 5962372398781412921L;
051
052    private static final Logger LOG = LoggerFactory.getLogger(GravatarCacheServlet.class);
053    private static final Pattern HASH_PATTERN = Pattern.compile("/([0-9a-f]{32})");
054
055    @Override
056    protected void doService(HttpServletRequest req, HttpServletResponse resp)
057    throws Exception {
058        try {
059            Matcher m = HASH_PATTERN.matcher(req.getPathInfo());
060            if (! m.matches()) {
061                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
062                return;
063            }
064
065            String hash = m.group(1);
066
067            GravatarService gs = getWebApplicationContext().getBean("gravatarService", GravatarService.class);
068            File gravatarFile = gs.fetchGravatar(hash);
069
070            long modifiedSinceTs = getIfModifiedSince(req);
071            if (modifiedSinceTs >= 0
072                    && (modifiedSinceTs / 1000L) == (gravatarFile.lastModified() / 1000L)) {
073                // The image has not been modified since last request
074                resp.sendError(HttpServletResponse.SC_NOT_MODIFIED);
075                return;
076            }
077
078            long size = gravatarFile.length();
079            if (size > 0 && size <= Integer.MAX_VALUE) {
080                // Cast to int is safe
081                resp.setContentLength((int) size);
082            }
083
084            resp.setContentType("image/png");
085            resp.setDateHeader("Date", System.currentTimeMillis());
086            resp.setDateHeader("Last-Modified", gravatarFile.lastModified());
087
088            try (InputStream in = new FileInputStream(gravatarFile)) {
089                FileCopyUtils.copy(in, resp.getOutputStream());
090            }
091        } catch (IOException | BeansException ex) {
092            LOG.error("Failed to send Gravatar icon", ex);
093            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); //NOSONAR
094        }
095    }
096
097    /**
098     * Reads the If-Modified-Since header.
099     *
100     * @return date returned in the If-Modified-Since header, or -1 if not set or not
101     *         readable
102     */
103    private long getIfModifiedSince(HttpServletRequest req) {
104        try {
105            return req.getDateHeader("If-Modified-Since");
106        } catch (IllegalArgumentException ex) {
107            // As stated in RFC2616 Sec. 14.25, an invalid date will just be ignored.
108            LOG.debug("Ignored a bad If-Modified-Since header", ex);
109            return -1;
110        }
111    }
112
113}