diff --git a/.gitignore b/.gitignore index 1508b92..112f2f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ .idea/ +.project +.classpath +.settings/ target/ **/target/ -**/.idea/ \ No newline at end of file +**/.idea/ +*.iml + +### OSX ### +.DS_Store diff --git a/.travis.yml b/.travis.yml index c2d9b47..e8f05af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: java script: "mvn clean install -U" jdk: - - oraclejdk6 - #- oraclejdk7 - - openjdk6 - - openjdk7 \ No newline at end of file + - openjdk7 + - openjdk8 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..43db8ab --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present, GreenGerong(https://github.com/greengerong), Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6ec66ca..4d3f00f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Prerender Java [![Build Status](https://travis-ci.org/greengerong/prerender-java.png)](https://travis-ci.org/greengerong/prerender-java) +Prerender Java =========================== Are you using backbone, angular, emberjs, etc, but you're unsure about the SEO implications? @@ -15,12 +15,8 @@ Use this java filter that prerenders a javascript-rendered page using an externa prerender com.github.greengerong.PreRenderSEOFilter - prerenderServiceUrl - http://localhost:3000 - - - crawlerUserAgents - me + prerenderToken + [get from prerender: https://prerender.io/] @@ -33,7 +29,7 @@ Use this java filter that prerenders a javascript-rendered page using an externa com.github.greengerong prerender-java - 1.0 + 1.6.4 ## How it works @@ -42,7 +38,7 @@ Use this java filter that prerenders a javascript-rendered page using an externa 2. Check to make sure we aren't requesting a resource (js, css, etc...) 3. (optional) Check to make sure the url is in the whitelist 4. (optional) Check to make sure the url isn't in the blacklist -2. Make a `GET` request to the [prerender service](https://github.com/collectiveip/prerender)(phantomjs server) for the page's prerendered HTML +2. Make a `GET` request to the [prerender service](https://github.com/prerender/prerender)(phantomjs server) for the page's prerendered HTML 3. Return that HTML to the crawler ## Customization @@ -54,10 +50,16 @@ example: someproxy,someproxy1 ### blacklist +### forwardedURLHeader +Important for servers behind reverse proxy that need the public url to be used for pre-rendering. +We usually set the original url in an http header which is added by the reverse proxy (similar to the more standard `x-forwarded-proto` and `x-forwarded-for`) + +### protocol +If you specifically want to make sure that the Prerender service queries using https or http protocol, you can set the init-param `protocol` to `https` or `http` respectively. ### Using your own prerender service -If you've deployed the prerender service on your own, set the `PRERENDER_SERVICE_URL` environment variable so that this package points there instead. Otherwise, it will default to the service already deployed at `http://prerender.herokuapp.com` +If you've deployed the prerender service on your own, set the `PRERENDER_SERVICE_URL` environment variable so that this package points there instead. Otherwise, it will default to the service already deployed at `http://service.prerender.io/` $ export PRERENDER_SERVICE_URL= @@ -71,18 +73,43 @@ As an alternative, you can pass `prerender_service_url` in the options object du config filter init param with "prerenderServiceUrl"; ``` +### prerender service token + +If you want to use token with the prerender service, you can config it. + + +``` xml + config filter init param with "prerenderToken"; +``` + + +### prerender event handler + +If you want to cache the caching, analytics, log or others, you can config it. It should be instance of "com.github.greengerong.PreRenderEventHandler" + + +``` xml + config filter init param with "preRenderEventHandler"; +``` + + ## Testing -If you want to make sure your pages are rendering correctly: +If your URLs use a hash-bang: + + If you want to see `http://localhost:3000/#!/profiles/1234` + Then go to `http://localhost:3000/?_escaped_fragment_=/profiles/1234` + +If your URLs use push-state: + + If you want to see `http://localhost:3000/profiles/1234` + Then go to `http://localhost:3000/profiles/1234?_escaped_fragment_=` + -1. Open the Developer Tools in Chrome (Cmd + Atl + J) -2. Click the Settings gear in the bottom right corner. -3. Click "Overrides" on the left side of the settings panel. -4. Check the "User Agent" checkbox. -6. Choose "Other..." from the User Agent dropdown. -7. Type `googlebot` into the input box. -8. Refresh the page (make sure to keep the developer tools open). +project demo test url: + http://localhost:8080/test/?_escaped_fragment_= + ## License The MIT License (MIT) diff --git a/demo/demo.iml b/demo/demo.iml index c89231a..5215d4d 100644 --- a/demo/demo.iml +++ b/demo/demo.iml @@ -16,17 +16,18 @@ - - - + + - - - + + + + + diff --git a/demo/pom.xml b/demo/pom.xml index 6864939..5b1c40c 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -4,17 +4,75 @@ com.github.greengerong demo war - 1.0 + 1.0-SNAPSHOT demo Maven Webapp http://maven.apache.org com.github.greengerong prerender-java - 1.0-SNAPSHOT + 1.6.4 + + + + + + + + commons-io + commons-io + 2.4 + + + org.slf4j + slf4j-log4j12 + 1.7.5 + + + log4j + log4j + 1.2.17 demo + + + + org.mortbay.jetty + maven-jetty-plugin + 6.1.10 + + + + /test + ${project.build.directory}/demo/ + + 10 + foo + 8080 + + + + start-jetty + pre-integration-test + + run + + + 0 + true + + + + stop-jetty + post-integration-test + + stop + + + + + diff --git a/demo/src/main/resources/log4j.properties b/demo/src/main/resources/log4j.properties new file mode 100644 index 0000000..3074ab0 --- /dev/null +++ b/demo/src/main/resources/log4j.properties @@ -0,0 +1,63 @@ +#log4j.rootLogger=CONSOLE,FILE +log4j.rootLogger=DEBUG,CONSOLE +log4j.addivity.org.apache=true + + +# 应用于控制台 +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.Threshold=INFO +log4j.appender.CONSOLE.Target=System.out +log4j.appender.CONSOLE.Encoding=GBK +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n + +# 每天新建日志 +log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender +log4j.appender.A1.File=C:/log4j/log +log4j.appender.A1.Encoding=GBK +log4j.appender.A1.Threshold=DEBUG +log4j.appender.A1.DatePattern='.'yyyy-MM-dd +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L : %m%n + +#应用于文件 +log4j.appender.FILE=org.apache.log4j.FileAppender +log4j.appender.FILE.File=C:/log4j/file.log +log4j.appender.FILE.Append=false +log4j.appender.FILE.Encoding=GBK +log4j.appender.FILE.layout=org.apache.log4j.PatternLayout +log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n + +# 应用于文件回滚 +log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender +log4j.appender.ROLLING_FILE.Threshold=ERROR +log4j.appender.ROLLING_FILE.File=rolling.log +log4j.appender.ROLLING_FILE.Append=true +log4j.appender.CONSOLE_FILE.Encoding=GBK +log4j.appender.ROLLING_FILE.MaxFileSize=10KB +log4j.appender.ROLLING_FILE.MaxBackupIndex=1 +log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout +log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n + +#应用于socket +log4j.appender.SOCKET=org.apache.log4j.RollingFileAppender +log4j.appender.SOCKET.RemoteHost=localhost +log4j.appender.SOCKET.Port=5001 +log4j.appender.SOCKET.LocationInfo=true +# Set up for Log Facter 5 +log4j.appender.SOCKET.layout=org.apache.log4j.PatternLayout +log4j.appender.SOCET.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[THREAD]%n%c[CATEGORY]%n%m[MESSAGE]%n%n +# Log Factor 5 Appender +log4j.appender.LF5_APPENDER=org.apache.log4j.lf5.LF5Appender +log4j.appender.LF5_APPENDER.MaxNumberOfRecords=2000 + +# 发送日志给邮件 +log4j.appender.MAIL=org.apache.log4j.net.SMTPAppender +log4j.appender.MAIL.Threshold=FATAL +log4j.appender.MAIL.BufferSize=10 +log4j.appender.MAIL.From= +log4j.appender.MAIL.SMTPHost=www.wusetu.com +log4j.appender.MAIL.Subject=Log4J Message +log4j.appender.MAIL.To= +log4j.appender.MAIL.layout=org.apache.log4j.PatternLayout +log4j.appender.MAIL.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n diff --git a/demo/src/main/webapp/WEB-INF/web.xml b/demo/src/main/webapp/WEB-INF/web.xml index 89ebe63..2677fca 100644 --- a/demo/src/main/webapp/WEB-INF/web.xml +++ b/demo/src/main/webapp/WEB-INF/web.xml @@ -13,8 +13,12 @@ crawlerUserAgents - me + YahooSeeker + + + + prerender diff --git a/demo/src/main/webapp/index.jsp b/demo/src/main/webapp/index.jsp index b1e3b13..4a09353 100644 --- a/demo/src/main/webapp/index.jsp +++ b/demo/src/main/webapp/index.jsp @@ -1,176 +1,18 @@ - - - - -

Prerender java demo page

- -

use angularjs ng-repeat

-
    -
  • {{item}}
  • -
- - + + + + + +

Prerender java demo page

+

use angularjs ng-repeat

+
    +
  • {{item}}
  • +
+ + diff --git a/pom.xml b/pom.xml index f506089..01017d3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.github.greengerong prerender-java - 1.1-SNAPSHOT + 1.6.5-SNAPSHOT jar prerender-java @@ -15,6 +15,7 @@ http://www.apache.org/licenses/LICENSE-2.0.txt repo + scm:git:git@github.com:greengerong/prerender-java.git @@ -32,23 +33,19 @@ 4.11 1.9.5 15.0 - 2.6 + 2.6 + 2.4 2.5 - 3.1 - 1.2.9 + 4.3.1 + 1.7.5 - - junit - junit - 4.11 - test - com.google.guava guava ${guava.version} + cdi1.0 junit @@ -65,7 +62,12 @@ commons-lang commons-lang - ${commons-lange.version} + ${commons-lang.version} + + + commons-io + commons-io + ${commons-io.version} javax.servlet @@ -74,27 +76,27 @@ provided - commons-httpclient - commons-httpclient - ${commons-httpclient.version} + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + org.slf4j + slf4j-api + ${slf4j.version} - - - - - sonatype-nexus-snapshots Sonatype Nexus Snapshots - http://oss.sonatype.org/content/repositories/snapshots + https://oss.sonatype.org/content/repositories/snapshots sonatype-nexus-staging Nexus Release Repository - http://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://oss.sonatype.org/service/local/staging/deploy/maven2/ @@ -113,9 +115,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.7 - - + 2.8.1 attach-javadoc diff --git a/prerender-java.iml b/prerender-java.iml index 2dc2562..f74aa01 100644 --- a/prerender-java.iml +++ b/prerender-java.iml @@ -10,15 +10,17 @@ + - + - - - + + + + + - - + \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..e66c6b5 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/java/com/github/greengerong/PreRenderEventHandler.java b/src/main/java/com/github/greengerong/PreRenderEventHandler.java new file mode 100644 index 0000000..bd71634 --- /dev/null +++ b/src/main/java/com/github/greengerong/PreRenderEventHandler.java @@ -0,0 +1,16 @@ +package com.github.greengerong; + + +import org.apache.http.HttpResponse; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface PreRenderEventHandler { + + String beforeRender(HttpServletRequest clientRequest); + + String afterRender(HttpServletRequest clientRequest, HttpServletResponse clientResponse, HttpResponse prerenderResponse, String responseHtml); + + void destroy(); +} diff --git a/src/main/java/com/github/greengerong/PreRenderSEOFilter.java b/src/main/java/com/github/greengerong/PreRenderSEOFilter.java index 5e9defd..5704025 100644 --- a/src/main/java/com/github/greengerong/PreRenderSEOFilter.java +++ b/src/main/java/com/github/greengerong/PreRenderSEOFilter.java @@ -1,237 +1,55 @@ package com.github.greengerong; - -import com.google.common.base.Predicate; -import com.google.common.collect.Lists; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.lang.StringUtils; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; import java.io.IOException; -import java.io.PrintWriter; -import java.net.URISyntaxException; -import java.util.Arrays; import java.util.List; -import java.util.regex.Pattern; - -import static com.google.common.collect.FluentIterable.from; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; public class PreRenderSEOFilter implements Filter { - - private FilterConfig filterConfig; + public static final List PARAMETER_NAMES = Lists.newArrayList("preRenderEventHandler", "proxy", "proxyPort", + "prerenderToken", "forwardedURLHeader", "crawlerUserAgents", "extensionsToIgnore", "whitelist", + "blacklist", "prerenderServiceUrl", "protocol","socketTimeout"); + private PrerenderSeoService prerenderSeoService; @Override public void init(FilterConfig filterConfig) throws ServletException { - this.filterConfig = filterConfig; + this.prerenderSeoService = new PrerenderSeoService(toMap(filterConfig)); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - try { - final HttpServletRequest request = (HttpServletRequest) servletRequest; - if (shouldShowPrerenderedPage(request)) { - final ResponseResult result = getPrerenderedPageResponse(request); - if (result.getStatusCode() == 200) { - final PrintWriter writer = servletResponse.getWriter(); - writer.write(result.getResponseBody()); - writer.flush(); - return; - } - } - } catch (Exception e) { - } - filterChain.doFilter(servletRequest, servletResponse); - } - - private ResponseResult getPrerenderedPageResponse(HttpServletRequest request) throws IOException { - final String apiUrl = getApiUrl(getFullUrl(request)); - final HttpClient httpClient = new HttpClient(); - final GetMethod getMethod = new GetMethod(apiUrl); - setConfig(httpClient); - setHttpHeader(getMethod); - final int code = httpClient.executeMethod(getMethod); - final String responseBody = getMethod.getResponseBodyAsString(); - String body = new String(responseBody.getBytes("utf-8")); - return new ResponseResult(code, body); - } - - private String getFullUrl(HttpServletRequest request) { - final StringBuffer url = request.getRequestURL(); - final String queryString = request.getQueryString(); - if (queryString != null) { - url.append('?'); - url.append(queryString); - } - return url.toString(); - } - - private void setHttpHeader(HttpMethod httpMethod) { - httpMethod.setRequestHeader("Cache-Control", "no-cache"); - httpMethod.setRequestHeader("Content-Type", "text/html"); - } - - private void setConfig(HttpClient httpClient) { - final String proxy = filterConfig.getInitParameter("proxy"); - if (StringUtils.isNotBlank(proxy)) { - final int proxyPort = Integer.parseInt(filterConfig.getInitParameter("proxyPort")); - httpClient.getHostConfiguration().setProxy(proxy, proxyPort); + boolean isPreRendered = prerenderSeoService.prerenderIfEligible( + (HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); + if (!isPreRendered) { + filterChain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { - filterConfig = null; + prerenderSeoService.destroy(); } - private List getCrawlerUserAgents() { - List crawlerUserAgents = Lists.newArrayList("googlebot", "yahoo", "bingbot", "baiduspider", - "facebookexternalhit", "twitterbot"); - final String crawlerUserAgentsFromConfig = filterConfig.getInitParameter("crawlerUserAgents"); - if (StringUtils.isNotBlank(crawlerUserAgentsFromConfig)) { - crawlerUserAgents.addAll(Arrays.asList(crawlerUserAgentsFromConfig.trim().split(","))); - } - - return crawlerUserAgents; + protected void setPrerenderSeoService(PrerenderSeoService prerenderSeoService) { + this.prerenderSeoService = prerenderSeoService; } - private List getExtensionsToIgnore() { - List extensionsToIgnore = Lists.newArrayList(".js", ".css", ".less", ".png", ".jpg", ".jpeg", - ".gif", ".pdf", ".doc", ".txt", ".zip", ".mp3", ".rar", ".exe", ".wmv", ".doc", ".avi", ".ppt", ".mpg", - ".mpeg", ".tif", ".wav", ".mov", ".psd", ".ai", ".xls", ".mp4", ".m4a", ".swf", ".dat", ".dmg", - ".iso", ".flv", ".m4v", ".torrent"); - final String extensionsToIgnoreFromConfig = filterConfig.getInitParameter("extensionsToIgnore"); - if (StringUtils.isNotBlank(extensionsToIgnoreFromConfig)) { - extensionsToIgnore.addAll(Arrays.asList(extensionsToIgnoreFromConfig.trim().split(","))); - } - - return extensionsToIgnore; - } - - private List getWhitelist() { - final String whitelist = filterConfig.getInitParameter("whitelist"); - if (StringUtils.isNotBlank(whitelist)) { - return Arrays.asList(whitelist.trim().split(",")); - } - return null; - } - - private List getBlacklist() { - final String blacklist = filterConfig.getInitParameter("blacklist"); - if (StringUtils.isNotBlank(blacklist)) { - return Arrays.asList(blacklist.trim().split(",")); - } - return null; - } - - private boolean shouldShowPrerenderedPage(HttpServletRequest request) throws URISyntaxException { - final String useAgent = request.getHeader("User-Agent"); - final String url = request.getRequestURL().toString(); - final String referer = request.getHeader("Referer"); - - if (hasEscapedFragment(request)) { - return true; - } - - if (StringUtils.isBlank(useAgent)) { - return false; - } - - if (!isInSearchUserAgent(useAgent)) { - return false; - } - - - if (isInResources(url)) { - return false; - } - - final List whiteList = getWhitelist(); - if (whiteList != null && !isInWhiteList(url, whiteList)) { - return false; - } - - final List blacklist = getBlacklist(); - if (blacklist != null && isInBlackList(url, referer, blacklist)) { - return false; - } - - return true; - } - - private boolean hasEscapedFragment(HttpServletRequest request) { - return request.getParameterMap().containsKey("_escaped_fragment_"); - } - - private String getApiUrl(String url) { - String prerenderServiceUrl = getPrerenderServiceUrl(); - if (!prerenderServiceUrl.endsWith("/")) { - prerenderServiceUrl += "/"; - } - return prerenderServiceUrl + url; - } - - private String getPrerenderServiceUrl() { - final String prerenderServiceUrl = filterConfig.getInitParameter("prerenderServiceUrl"); - return StringUtils.isNotBlank(prerenderServiceUrl) ? prerenderServiceUrl : "http://prerender.herokuapp.com/"; - } - - private boolean isInBlackList(final String url, final String referer, List blacklist) { - return from(blacklist).anyMatch(new Predicate() { - @Override - public boolean apply(String regex) { - final Pattern pattern = Pattern.compile(regex); - return pattern.matcher(url).matches() || - (!StringUtils.isBlank(referer) && pattern.matcher(referer).matches()); - } - }); - } - - private boolean isInWhiteList(final String url, List whitelist) { - return from(whitelist).anyMatch(new Predicate() { - @Override - public boolean apply(String regex) { - return Pattern.compile(regex).matcher(url).matches(); - } - }); - } - - private boolean isInResources(final String url) { - return from(getExtensionsToIgnore()).anyMatch(new Predicate() { - @Override - public boolean apply(String item) { - return url.contains(item.toLowerCase()); - } - }); - } - - private boolean isInSearchUserAgent(final String useAgent) { - return from(getCrawlerUserAgents()).anyMatch(new Predicate() { - @Override - public boolean apply(String item) { - return item.equalsIgnoreCase(useAgent); - } - }); - } - - private class ResponseResult { - private int statusCode; - private String responseBody; - - public ResponseResult(int code, String body) { - statusCode = code; - responseBody = body; - } - - private int getStatusCode() { - return statusCode; - } - - private String getResponseBody() { - return responseBody; + protected Map toMap(FilterConfig filterConfig) { + Map config = Maps.newHashMap(); + for (String parameterName : PARAMETER_NAMES) { + config.put(parameterName, filterConfig.getInitParameter(parameterName)); } + return config; } } + diff --git a/src/main/java/com/github/greengerong/PrerenderConfig.java b/src/main/java/com/github/greengerong/PrerenderConfig.java new file mode 100644 index 0000000..6b4e894 --- /dev/null +++ b/src/main/java/com/github/greengerong/PrerenderConfig.java @@ -0,0 +1,140 @@ +package com.github.greengerong; + + +import com.google.common.collect.Lists; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +public class PrerenderConfig { + private final static Logger log = LoggerFactory.getLogger(PrerenderConfig.class); + public static final String PRERENDER_IO_SERVICE_URL = "http://service.prerender.io/"; + private Map config; + + public PrerenderConfig(Map config) { + this.config = config; + } + + public PreRenderEventHandler getEventHandler() { + final String preRenderEventHandler = config.get("preRenderEventHandler"); + if (isNotBlank(preRenderEventHandler)) { + try { + return (PreRenderEventHandler) Class.forName(preRenderEventHandler).newInstance(); + } catch (Exception e) { + log.error("PreRenderEventHandler class not find or can not new a instance", e); + } + } + return null; + } + + public CloseableHttpClient getHttpClient() { + HttpClientBuilder builder = HttpClients.custom() + .setConnectionManager(new PoolingHttpClientConnectionManager()) + .disableRedirectHandling(); + + configureProxy(builder); + configureTimeout(builder); + return builder.build(); + } + + private HttpClientBuilder configureProxy(HttpClientBuilder builder) { + final String proxy = config.get("proxy"); + if (isNotBlank(proxy)) { + final int proxyPort = Integer.parseInt(config.get("proxyPort")); + DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(new HttpHost(proxy, proxyPort)); + builder.setRoutePlanner(routePlanner); + } + return builder; + } + + private HttpClientBuilder configureTimeout(HttpClientBuilder builder) { + final String socketTimeout = getSocketTimeout(); + if (socketTimeout != null) { + RequestConfig config = RequestConfig.custom().setSocketTimeout(Integer.parseInt(socketTimeout)).build(); + builder.setDefaultRequestConfig(config); + } + return builder; + } + + public String getSocketTimeout() { + return config.get("socketTimeout"); + } + + public String getPrerenderToken() { + return config.get("prerenderToken"); + } + + public String getForwardedURLHeader() { + return config.get("forwardedURLHeader"); + } + + public String getProtocol() { + return config.get("protocol"); + } + + public List getCrawlerUserAgents() { + List crawlerUserAgents = Lists.newArrayList("baiduspider", + "facebookexternalhit", "twitterbot", "rogerbot", "linkedinbot", "embedly", "quora link preview" + , "showyoubo", "outbrain", "pinterest", "developers.google.com/+/web/snippet", "slackbot", "vkShare", + "W3C_Validator", "redditbot", "Applebot", "whatsapp", "flipboard", "tumblr", "bitlybot", + "skypeuripreview", "nuzzel", "discordbot", "google page speed", "qwantify", "pinterestbot", + "bitrix link preview", "xing-contenttabreceiver", "chrome-lighthouse", "telegrambot"); + final String crawlerUserAgentsFromConfig = config.get("crawlerUserAgents"); + if (isNotBlank(crawlerUserAgentsFromConfig)) { + crawlerUserAgents.addAll(Arrays.asList(crawlerUserAgentsFromConfig.trim().split(","))); + } + + return crawlerUserAgents; + } + + public List getExtensionsToIgnore() { + List extensionsToIgnore = Lists.newArrayList(".js", ".json", ".css", ".xml", ".less", ".png", ".jpg", + ".jpeg", ".gif", ".pdf", ".doc", ".txt", ".ico", ".rss", ".zip", ".mp3", ".rar", ".exe", ".wmv", + ".doc", ".avi", ".ppt", ".mpg", ".mpeg", ".tif", ".wav", ".mov", ".psd", ".ai", ".xls", ".mp4", + ".m4a", ".swf", ".dat", ".dmg", ".iso", ".flv", ".m4v", ".torrent", ".woff", ".ttf"); + final String extensionsToIgnoreFromConfig = config.get("extensionsToIgnore"); + if (isNotBlank(extensionsToIgnoreFromConfig)) { + extensionsToIgnore.addAll(Arrays.asList(extensionsToIgnoreFromConfig.trim().split(","))); + } + + return extensionsToIgnore; + } + + public List getWhitelist() { + final String whitelist = config.get("whitelist"); + if (isNotBlank(whitelist)) { + return Arrays.asList(whitelist.trim().split(",")); + } + return null; + } + + public List getBlacklist() { + final String blacklist = config.get("blacklist"); + if (isNotBlank(blacklist)) { + return Arrays.asList(blacklist.trim().split(",")); + } + return null; + } + + public String getPrerenderServiceUrl() { + final String prerenderServiceUrl = config.get("prerenderServiceUrl"); + return isNotBlank(prerenderServiceUrl) ? prerenderServiceUrl : getDefaultPrerenderIoServiceUrl(); + } + + private String getDefaultPrerenderIoServiceUrl() { + final String prerenderServiceUrlInEnv = System.getProperty("PRERENDER_SERVICE_URL"); + return isNotBlank(prerenderServiceUrlInEnv) ? prerenderServiceUrlInEnv : PRERENDER_IO_SERVICE_URL; + } +} diff --git a/src/main/java/com/github/greengerong/PrerenderSeoService.java b/src/main/java/com/github/greengerong/PrerenderSeoService.java new file mode 100644 index 0000000..63d3352 --- /dev/null +++ b/src/main/java/com/github/greengerong/PrerenderSeoService.java @@ -0,0 +1,378 @@ +package com.github.greengerong; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import org.apache.commons.lang.StringUtils; +import org.apache.http.*; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.HeaderGroup; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.google.common.collect.FluentIterable.from; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.apache.http.HttpHeaders.CONTENT_LENGTH; +import static org.apache.http.HttpHeaders.HOST; + +public class PrerenderSeoService { + private final static Logger log = LoggerFactory.getLogger(PrerenderSeoService.class); + /** + * These are the "hop-by-hop" headers that should not be copied. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + * I use an HttpClient HeaderGroup class instead of Set because this + * approach does case insensitive lookup faster. + */ + private static final HeaderGroup hopByHopHeaders; + public static final String ESCAPED_FRAGMENT_KEY = "_escaped_fragment_"; + private CloseableHttpClient httpClient; + private PrerenderConfig prerenderConfig; + private PreRenderEventHandler preRenderEventHandler; + + public PrerenderSeoService(Map config) { + this.prerenderConfig = new PrerenderConfig(config); + this.httpClient = getHttpClient(); + } + + static { + hopByHopHeaders = new HeaderGroup(); + String[] headers = new String[]{ + "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", + "TE", "Trailers", "Transfer-Encoding", "Upgrade"}; + for (String header : headers) { + hopByHopHeaders.addHeader(new BasicHeader(header, null)); + } + } + + public void destroy() { + if (preRenderEventHandler != null) { + preRenderEventHandler.destroy(); + } + closeQuietly(httpClient); + } + + public boolean prerenderIfEligible(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { + try { + if (handlePrerender(servletRequest, servletResponse)) { + return true; + } + } catch (Exception e) { + log.error("Prerender service error", e); + } + return false; + } + + private boolean handlePrerender(HttpServletRequest servletRequest, HttpServletResponse servletResponse) + throws URISyntaxException, IOException { + if (shouldShowPrerenderedPage(servletRequest)) { + this.preRenderEventHandler = prerenderConfig.getEventHandler(); + if (beforeRender(servletRequest, servletResponse) || proxyPrerenderedPageResponse(servletRequest, servletResponse)) { + return true; + } + } + return false; + } + + private boolean shouldShowPrerenderedPage(HttpServletRequest request) throws URISyntaxException { + final String userAgent = request.getHeader("User-Agent"); + final String url = getRequestURL(request); + final String referer = request.getHeader("Referer"); + + log.trace(String.format("checking request for %s from User-Agent %s and referer %s", url, userAgent, referer)); + + if (!HttpGet.METHOD_NAME.equals(request.getMethod())) { + log.trace("Request is not HTTP GET; intercept: no"); + return false; + } + + if (isInResources(url)) { + log.trace("request is for a (static) resource; intercept: no"); + return false; + } + + String prerenderHeader = request.getHeader("X-Prerender"); + if (StringUtils.isNotEmpty(prerenderHeader)) { + return false; + } + + final List whiteList = prerenderConfig.getWhitelist(); + if (whiteList != null && !isInWhiteList(url, whiteList)) { + log.trace("Whitelist is enabled, but this request is not listed; intercept: no"); + return false; + } + + final List blacklist = prerenderConfig.getBlacklist(); + if (blacklist != null && isInBlackList(url, referer, blacklist)) { + log.trace("Blacklist is enabled, and this request is listed; intercept: no"); + return false; + } + + if (hasEscapedFragment(request)) { + log.trace("Request Has _escaped_fragment_; intercept: yes"); + return true; + } + + if (StringUtils.isBlank(userAgent)) { + log.trace("Request has blank userAgent; intercept: no"); + return false; + } + + if (!isInSearchUserAgent(userAgent)) { + log.trace("Request User-Agent is not a search bot; intercept: no"); + return false; + } + + log.trace(String.format("Defaulting to request intercept(user-agent=%s): yes", userAgent)); + return true; + } + + protected HttpGet getHttpGet(String apiUrl) { + return new HttpGet(apiUrl); + } + + protected CloseableHttpClient getHttpClient() { + return prerenderConfig.getHttpClient(); + } + + /** + * Copy request headers from the servlet client to the proxy request. + * + * @throws java.net.URISyntaxException + */ + private void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest) + throws URISyntaxException { + // Get an Enumeration of all of the header names sent by the client + Enumeration enumerationOfHeaderNames = servletRequest.getHeaderNames(); + while (enumerationOfHeaderNames.hasMoreElements()) { + String headerName = (String) enumerationOfHeaderNames.nextElement(); + //Instead the content-length is effectively set via InputStreamEntity + if (!headerName.equalsIgnoreCase(CONTENT_LENGTH) && !hopByHopHeaders.containsHeader(headerName)) { + Enumeration headers = servletRequest.getHeaders(headerName); + while (headers.hasMoreElements()) {//sometimes more than one value + String headerValue = (String) headers.nextElement(); + // In case the proxy host is running multiple virtual servers, + // rewrite the Host header to ensure that we get content from + // the correct virtual server + if (headerName.equalsIgnoreCase(HOST)) { + HttpHost host = URIUtils.extractHost(new URI(prerenderConfig.getPrerenderServiceUrl())); + headerValue = host.getHostName(); + if (host.getPort() != -1) { + headerValue += ":" + host.getPort(); + } + } + proxyRequest.addHeader(headerName, headerValue); + } + } + } + } + + private String getRequestURL(HttpServletRequest request) { + if (StringUtils.isNotEmpty(prerenderConfig.getForwardedURLHeader())) { + String url = request.getHeader(prerenderConfig.getForwardedURLHeader()); + if (StringUtils.isNotEmpty(url)) { + return url; + } + } + if (StringUtils.isNotEmpty(prerenderConfig.getProtocol())) { + String url = request.getRequestURL().toString(); + return url.replace(request.getScheme(), prerenderConfig.getProtocol()); + } + return request.getRequestURL().toString(); + } + + private String getApiUrl(String url) { + String prerenderServiceUrl = prerenderConfig.getPrerenderServiceUrl(); + if (!prerenderServiceUrl.endsWith("/")) { + prerenderServiceUrl += "/"; + } + return prerenderServiceUrl + url; + } + + /** + * Copy proxied response headers back to the servlet client. + */ + private void copyResponseHeaders(HttpResponse proxyResponse, final HttpServletResponse servletResponse) { + servletResponse.setCharacterEncoding(getContentCharSet(proxyResponse.getEntity())); + from(Arrays.asList(proxyResponse.getAllHeaders())).filter(new Predicate
() { + @Override + public boolean apply(Header header) { + return !hopByHopHeaders.containsHeader(header.getName()); + } + }).transform(new Function() { + @Override + public Boolean apply(Header header) { + servletResponse.addHeader(header.getName(), header.getValue()); + return true; + } + }).toList(); + } + + /** + * Get the charset used to encode the http entity. + */ + private String getContentCharSet(final HttpEntity entity) throws ParseException { + if (entity == null) { + return null; + } + String charset = null; + if (entity.getContentType() != null) { + HeaderElement values[] = entity.getContentType().getElements(); + if (values.length > 0) { + NameValuePair param = values[0].getParameterByName("charset"); + if (param != null) { + charset = param.getValue(); + } + } + } + return charset; + } + + private String getResponseHtml(HttpResponse proxyResponse) + throws IOException { + HttpEntity entity = proxyResponse.getEntity(); + return entity != null ? EntityUtils.toString(entity) : ""; + } + + /** + * Copy response body data (the entity) from the proxy to the servlet client. + */ + private void responseEntity(String html, HttpServletResponse servletResponse) + throws IOException { + PrintWriter printWriter = servletResponse.getWriter(); + try { + printWriter.write(html); + printWriter.flush(); + } finally { + closeQuietly(printWriter); + } + } + + + protected void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + log.error("Close proxy error", e); + } + } + + private boolean hasEscapedFragment(HttpServletRequest request) { + return request.getParameterMap().containsKey(ESCAPED_FRAGMENT_KEY); + } + + private boolean isInBlackList(final String url, final String referer, List blacklist) { + return from(blacklist).anyMatch(new Predicate() { + @Override + public boolean apply(String regex) { + final Pattern pattern = Pattern.compile(regex); + return pattern.matcher(url).matches() || + (!StringUtils.isBlank(referer) && pattern.matcher(referer).matches()); + } + }); + } + + private boolean isInSearchUserAgent(final String userAgent) { + return from(prerenderConfig.getCrawlerUserAgents()).anyMatch(new Predicate() { + @Override + public boolean apply(String item) { + return userAgent.toLowerCase().contains(item.toLowerCase()); + } + }); + } + + + private boolean isInResources(final String url) { + return from(prerenderConfig.getExtensionsToIgnore()).anyMatch(new Predicate() { + @Override + public boolean apply(String item) { + return (url.indexOf('?') >= 0 ? url.substring(0, url.indexOf('?')) : url) + .toLowerCase().endsWith(item); + } + }); + } + + private boolean isInWhiteList(final String url, List whitelist) { + return from(whitelist).anyMatch(new Predicate() { + @Override + public boolean apply(String regex) { + return Pattern.compile(regex).matcher(url).matches(); + } + }); + } + + private boolean beforeRender(HttpServletRequest request, HttpServletResponse response) throws IOException { + if (preRenderEventHandler != null) { + final String html = preRenderEventHandler.beforeRender(request); + if (isNotBlank(html)) { + final PrintWriter writer = response.getWriter(); + writer.write(html); + writer.flush(); + closeQuietly(writer); + return true; + } + } + return false; + } + + private boolean proxyPrerenderedPageResponse(HttpServletRequest request, HttpServletResponse response) + throws IOException, URISyntaxException { + final String apiUrl = getApiUrl(getFullUrl(request)); + log.info(String.format("Prerender proxy will send request to:%s", apiUrl)); + final HttpGet getMethod = getHttpGet(apiUrl); + copyRequestHeaders(request, getMethod); + withPrerenderToken(getMethod); + CloseableHttpResponse prerenderServerResponse = null; + + try { + prerenderServerResponse = httpClient.execute(getMethod); + response.setStatus(prerenderServerResponse.getStatusLine().getStatusCode()); + copyResponseHeaders(prerenderServerResponse, response); + String html = getResponseHtml(prerenderServerResponse); + html = afterRender(request, response, prerenderServerResponse, html); + responseEntity(html, response); + return true; + } finally { + closeQuietly(prerenderServerResponse); + } + } + + private String afterRender(HttpServletRequest clientRequest, HttpServletResponse clientResponse, CloseableHttpResponse prerenderServerResponse, String responseHtml) { + if (preRenderEventHandler != null) { + return preRenderEventHandler.afterRender(clientRequest, clientResponse, prerenderServerResponse, responseHtml); + } + return responseHtml; + } + + private void withPrerenderToken(HttpRequest proxyRequest) { + final String token = prerenderConfig.getPrerenderToken(); + //for new version prerender with token. + if (isNotBlank(token)) { + proxyRequest.addHeader("X-Prerender-Token", token); + } + } + + private String getFullUrl(HttpServletRequest request) { + final String url = getRequestURL(request); + final String queryString = request.getQueryString(); + return isNotBlank(queryString) ? String.format("%s?%s", url, queryString) : url; + } +} diff --git a/src/test/java/com/github/greengerong/PreRenderSEOFilterTest.java b/src/test/java/com/github/greengerong/PreRenderSEOFilterTest.java index 20beb9b..ba81d67 100644 --- a/src/test/java/com/github/greengerong/PreRenderSEOFilterTest.java +++ b/src/test/java/com/github/greengerong/PreRenderSEOFilterTest.java @@ -1,4 +1,341 @@ package com.github.greengerong; +import com.google.common.collect.Maps; +import org.apache.http.Header; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.util.Enumeration; +import java.util.HashMap; + +import static com.github.greengerong.PrerenderSeoService.ESCAPED_FRAGMENT_KEY; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.client.methods.HttpGet.METHOD_NAME; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) public class PreRenderSEOFilterTest { + + private PreRenderSEOFilter preRenderSEOFilter; + + @Mock + private CloseableHttpClient httpClient; + + @Mock + private FilterConfig filterConfig; + + @Mock + private HttpServletRequest servletRequest; + + @Mock + private HttpServletResponse servletResponse; + + @Mock + private FilterChain filterChain; + + @Mock + private HttpGet httpGet; + @Mock + private PrintWriter printWriter; + + @Before + public void setUp() throws Exception { + preRenderSEOFilter = new PreRenderSEOFilter() { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + setPrerenderSeoService(new PrerenderSeoService(toMap(filterConfig)) { + @Override + protected CloseableHttpClient getHttpClient() { + return httpClient; + } + + @Override + protected HttpGet getHttpGet(String apiUrl) { + return httpGet; + } + }); + } + }; + } + + @Test + public void should_not_handle_when_non_get_request() throws Exception { + //given + preRenderSEOFilter.init(filterConfig); + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer()); + when(servletRequest.getMethod()).thenReturn(HttpPost.METHOD_NAME); + + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_handle_when_url_with_escaped_fragment_() throws Exception { + //given + preRenderSEOFilter.init(filterConfig); + final CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(httpClient.execute(httpGet)).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + final HashMap map = Maps.newHashMap(); + map.put(ESCAPED_FRAGMENT_KEY, ""); + when(servletRequest.getParameterMap()).thenReturn(map); + when(statusLine.getStatusCode()).thenReturn(SC_OK); + when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(servletResponse.getWriter()).thenReturn(printWriter); + + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient).execute(httpGet); + verify(filterChain, never()).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_not_handle_when_user_agent_is_not_crawler() throws Exception { + //given + preRenderSEOFilter.init(filterConfig); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getParameterMap()).thenReturn(Maps.newHashMap()); + when(servletRequest.getHeader("User-Agent")).thenReturn("no"); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_not_handle_when_x_prerender_header_is_present() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + preRenderSEOFilter.init(filterConfig); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getParameterMap()).thenReturn(Maps.newHashMap()); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + when(servletRequest.getHeader("X-Prerender")).thenReturn("1"); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_not_handle_when_url_is_a_resource() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + preRenderSEOFilter.init(filterConfig); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test.js")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getParameterMap()).thenReturn(Maps.newHashMap()); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_not_handle_when_white_list_is_not_empty_and_url_is_not_in_white_list() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + when(filterConfig.getInitParameter("whitelist")).thenReturn("whitelist1,whitelist2"); + preRenderSEOFilter.init(filterConfig); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getParameterMap()).thenReturn(Maps.newHashMap()); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_not_handle_when_black_list_is_not_empty_and_url_is_in_black_list() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + when(filterConfig.getInitParameter("blacklist")).thenReturn("blacklist1,http://localhost/test"); + preRenderSEOFilter.init(filterConfig); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getParameterMap()).thenReturn(Maps.newHashMap()); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient, never()).execute(httpGet); + verify(filterChain).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_handle_when_user_agent_is_crawler_and_url_is_not_resource_and_white_list_is_empty_and_black_list_is_empty() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + preRenderSEOFilter.init(filterConfig); + + final CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + + when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(httpClient.execute(httpGet)).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + final HashMap map = Maps.newHashMap(); + map.put(ESCAPED_FRAGMENT_KEY, ""); + when(servletRequest.getParameterMap()).thenReturn(map); + when(statusLine.getStatusCode()).thenReturn(SC_OK); + when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(servletResponse.getWriter()).thenReturn(printWriter); + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient).execute(httpGet); + verify(filterChain, never()).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_handle_when_every_thing_is_ok_but_prerender_server_response_is_not_200() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + preRenderSEOFilter.init(filterConfig); + + final CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + + when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(httpClient.execute(httpGet)).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + final HashMap map = Maps.newHashMap(); + map.put(ESCAPED_FRAGMENT_KEY, ""); + when(servletRequest.getParameterMap()).thenReturn(map); + when(statusLine.getStatusCode()).thenReturn(SC_NOT_FOUND); + when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(servletResponse.getWriter()).thenReturn(printWriter); + + + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient).execute(httpGet); + verify(filterChain, never()).doFilter(servletRequest, servletResponse); + verify(servletResponse).setStatus(SC_NOT_FOUND); + } + + + @Test + public void should_handle_when_user_agent_is_crawler_and_url_is_not_resource_and_in_white_list_and_not_in_black_list() throws Exception { + //given + when(filterConfig.getInitParameter("crawlerUserAgents")).thenReturn("crawler1,crawler2"); + when(filterConfig.getInitParameter("whitelist")).thenReturn("whitelist1,http://localhost/test"); + when(filterConfig.getInitParameter("blacklist")).thenReturn("blacklist1,blacklist2"); + + preRenderSEOFilter.init(filterConfig); + + final CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getHeader("User-Agent")).thenReturn("crawler1"); + + when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(httpClient.execute(httpGet)).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + final HashMap map = Maps.newHashMap(); + map.put(ESCAPED_FRAGMENT_KEY, ""); + when(servletRequest.getParameterMap()).thenReturn(map); + when(statusLine.getStatusCode()).thenReturn(SC_OK); + when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(servletResponse.getWriter()).thenReturn(printWriter); + + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient).execute(httpGet); + verify(filterChain, never()).doFilter(servletRequest, servletResponse); + } + + @Test + public void should_use_request_url_from_custom_header_if_available() throws Exception { + //given + when(filterConfig.getInitParameter("forwardedURLHeader")).thenReturn("X-Forwarded-URL"); + when(filterConfig.getInitParameter("whitelist")).thenReturn("http://my.public.domain.com/"); + when(filterConfig.getInitParameter("blacklist")).thenReturn("http://localhost/test"); + + preRenderSEOFilter.init(filterConfig); + + final CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + when(servletRequest.getMethod()).thenReturn(METHOD_NAME); + when(servletRequest.getHeader("X-Forwarded-URL")).thenReturn("http://my.public.domain.com/"); + + when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(httpClient.execute(httpGet)).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + final HashMap map = Maps.newHashMap(); + map.put(ESCAPED_FRAGMENT_KEY, ""); + when(servletRequest.getParameterMap()).thenReturn(map); + when(statusLine.getStatusCode()).thenReturn(SC_OK); + when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(servletResponse.getWriter()).thenReturn(printWriter); + + //when + preRenderSEOFilter.doFilter(servletRequest, servletResponse, filterChain); + + //then + verify(httpClient).execute(httpGet); + verify(filterChain, never()).doFilter(servletRequest, servletResponse); + } } diff --git a/src/test/java/com/github/greengerong/PrerenderConfigTest.java b/src/test/java/com/github/greengerong/PrerenderConfigTest.java new file mode 100644 index 0000000..531c021 --- /dev/null +++ b/src/test/java/com/github/greengerong/PrerenderConfigTest.java @@ -0,0 +1,35 @@ +package com.github.greengerong; + +import com.google.common.collect.Maps; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; + +public class PrerenderConfigTest { + @Test(expected = Exception.class) + public void should_throw_exception_if_invalid_timeout_value_specified() throws Exception { + //given + Map configuration = Maps.newHashMap(); + configuration.put("socketTimeout", "not_an_int"); + PrerenderConfig config = new PrerenderConfig(configuration); + //when + config.getHttpClient(); + } + + @Test + public void should_pass_if_correct_timeout_value_specified() throws Exception { + //given + Map configuration = Maps.newHashMap(); + configuration.put("socketTimeout", "1000"); + PrerenderConfig config = new PrerenderConfig(configuration); + //when + final CloseableHttpClient httpClient = config.getHttpClient(); + + assertThat(httpClient, is(notNullValue())); + } +}