// Copyright (c) 2003-present, Jodd Team (http://jodd.org) // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package jodd.http; import jodd.exception.ExceptionUtil; import jodd.util.StringPool; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Emulates HTTP Browser and persist cookies between requests. */ public class HttpBrowser { protected HttpConnectionProvider httpConnectionProvider; protected HttpRequest httpRequest; protected HttpResponse httpResponse; protected HttpMultiMap<Cookie> cookies = HttpMultiMap.newCaseInsensitveMap(); protected HttpMultiMap<String> defaultHeaders = HttpMultiMap.newCaseInsensitveMap(); protected boolean keepAlive; protected long elapsedTime; protected boolean catchTransportExceptions = true; public HttpBrowser() { httpConnectionProvider = JoddHttp.httpConnectionProvider; } /** * Returns <code>true</code> if keep alive is used. */ public boolean isKeepAlive() { return keepAlive; } /** * Defines that persistent HTTP connection should be used. */ public HttpBrowser setKeepAlive(boolean keepAlive) { this.keepAlive = keepAlive; return this; } /** * Defines if transport exceptions should be thrown. */ public HttpBrowser setCatchTransportExceptions(boolean catchTransportExceptions) { this.catchTransportExceptions = catchTransportExceptions; return this; } /** * Defines proxy for a browser. */ public HttpBrowser setProxyInfo(ProxyInfo proxyInfo) { httpConnectionProvider.useProxy(proxyInfo); return this; } /** * Defines {@link jodd.http.HttpConnectionProvider} for this browser session. * Resets the previous proxy definition, if set. */ public HttpBrowser setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider) { this.httpConnectionProvider = httpConnectionProvider; return this; } /** * Adds default header to all requests. */ public HttpBrowser setDefaultHeader(String name, String value) { defaultHeaders.add(name, value); return this; } /** * Returns last used request. */ public HttpRequest getHttpRequest() { return httpRequest; } /** * Returns last received {@link HttpResponse HTTP response} object. */ public HttpResponse getHttpResponse() { return httpResponse; } /** * Returns last response HTML page. */ public String getPage() { if (httpResponse == null) { return null; } return httpResponse.bodyText(); } /** * Sends new request as a browser. Before sending, * all browser cookies are added to the request. * After sending, the cookies are read from the response. * Moreover, status codes 301 and 302 are automatically * handled. Returns very last response. */ public HttpResponse sendRequest(HttpRequest httpRequest) { elapsedTime = System.currentTimeMillis(); // send request while (true) { this.httpRequest = httpRequest; HttpResponse previouseResponse = this.httpResponse; this.httpResponse = null; addDefaultHeaders(httpRequest); addCookies(httpRequest); // send request if (catchTransportExceptions) { try { this.httpResponse = _sendRequest(httpRequest, previouseResponse); } catch (HttpException httpException) { httpResponse = new HttpResponse(); httpResponse.assignHttpRequest(httpRequest); httpResponse.statusCode(503); httpResponse.statusPhrase("Service unavailable. " + ExceptionUtil.message(httpException)); } } else { this.httpResponse =_sendRequest(httpRequest, previouseResponse); } readCookies(httpResponse); int statusCode = httpResponse.statusCode(); // 301: moved permanently if (statusCode == 301) { String newPath = location(httpResponse); httpRequest = HttpRequest.get(newPath); continue; } // 302: redirect, 303: see other if (statusCode == 302 || statusCode == 303) { String newPath = location(httpResponse); httpRequest = HttpRequest.get(newPath); continue; } // 307: temporary redirect if (statusCode == 307) { String newPath = location(httpResponse); String originalMethod = httpRequest.method(); httpRequest = new HttpRequest() .method(originalMethod) .set(newPath); continue; } break; } elapsedTime = System.currentTimeMillis() - elapsedTime; return this.httpResponse; } /** * Opens connection and sends a response. */ protected HttpResponse _sendRequest(HttpRequest httpRequest, HttpResponse previouseResponse) { if (!keepAlive) { httpRequest.open(httpConnectionProvider); } else { // keeping alive if (previouseResponse == null) { httpRequest.open(httpConnectionProvider).connectionKeepAlive(true); } else { httpRequest.keepAlive(previouseResponse, true); } } return httpRequest.send(); } /** * Add default headers to the request. If request already has a header set, * default header will be ignored. */ protected void addDefaultHeaders(HttpRequest httpRequest) { List<Map.Entry<String, String>> entries = defaultHeaders.entries(); for (Map.Entry<String, String> entry : entries) { String name = entry.getKey(); if (!httpRequest.headers.contains(name)) { httpRequest.headers.add(name, entry.getValue()); } } } /** * Parse 'location' header to return the next location. * Specification (<a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30">rfc2616</a>) * says that only absolute path must be provided, however, this does not * happens in the real world. There a <a href="https://tools.ietf.org/html/rfc7231#section-7.1.2">proposal</a> * that allows server name etc to be omitted. */ protected String location(HttpResponse httpResponse) { String location = httpResponse.header("location"); if (location.startsWith(StringPool.SLASH)) { HttpRequest httpRequest = httpResponse.getHttpRequest(); location = httpRequest.hostUrl() + location; } return location; } /** * Returns elapsed time of last {@link #sendRequest(HttpRequest)} in milliseconds. */ public long getElapsedTime() { return elapsedTime; } // ---------------------------------------------------------------- close /** * Closes browser explicitly, needed when keep-alive connection is used. */ public void close() { if (httpResponse != null) { httpResponse.close(); } } // ---------------------------------------------------------------- cookies /** * Deletes all cookies. */ public void clearCookies() { cookies.clear(); } /** * Reads cookies from response and adds to cookies list. */ protected void readCookies(HttpResponse httpResponse) { Cookie[] newCookies = httpResponse.cookies(); for (Cookie cookie : newCookies) { cookies.add(cookie.getName(), cookie); } } /** * Add cookies to the request. */ protected void addCookies(HttpRequest httpRequest) { // prepare all cookies List<Cookie> cookiesList = new ArrayList<>(); if (!cookies.isEmpty()) { for (Map.Entry<String, Cookie> cookieEntry : cookies) { cookiesList.add(cookieEntry.getValue()); } httpRequest.cookies(cookiesList.toArray(new Cookie[cookiesList.size()])); } } }