Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2013, 2014, 2015, 2016 Synacor, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License along with this program. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.common.util.ngxlookup; import static java.util.Arrays.asList; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.TreeSet; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpVersion; import org.apache.commons.httpclient.methods.GetMethod; import com.zimbra.common.httpclient.HttpClientUtil; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.ZimbraHttpConnectionManager; import com.zimbra.common.util.ZimbraLog; public class ZimbraNginxLookUpClient { private List<Route> ngxLookUpServers; // Nginx Upstream LookUp Handlers private List<Route> upstreamMailServers; // Upstream Mail service servers private int ngxConnectTimeout; private int ngxRetryTimeout; private static int nginxRRindex; private static int upstreamMailServerRRindex; private static final String urlExtension = "/service/extension/nginx-lookup"; private static final int DEFAULT_NGINX_HANDLER_PORT = 7072; private static final int DEFAULT_UPSTREAM_MAIL_SERVER_PORT = 7070; private static final String ngxPassword = "_password_"; private static final String[] ngxSchemes = new String[] { "https", "http" }; public ZimbraNginxLookUpClient() { ngxLookUpServers = null; upstreamMailServers = null; ngxConnectTimeout = 15000; ngxRetryTimeout = 600000; } public void setAttributes(String[] lookUpServers, String[] upstreamMailServers, int connectTimeout, int retryTimeout) { this.ngxLookUpServers = parseServerList(lookUpServers, DEFAULT_NGINX_HANDLER_PORT); ZimbraLog.misc.debug("got %s lookup servers", this.ngxLookUpServers == null ? "null" : this.ngxLookUpServers.size()); this.upstreamMailServers = parseServerList(upstreamMailServers, DEFAULT_UPSTREAM_MAIL_SERVER_PORT); ZimbraLog.misc.debug("got %s mailstore servers", this.upstreamMailServers == null ? "null" : this.upstreamMailServers.size()); this.ngxConnectTimeout = connectTimeout; this.ngxRetryTimeout = retryTimeout; } public String getUpstreamMailServer(String protocol) throws ServiceException { int count = 0; int currentIndex = 0; Route upstreamMailServer = null; if (upstreamMailServers != null && upstreamMailServers.size() > 0) { currentIndex = upstreamMailServerRRindex - 1; upstreamMailServerRRindex = (upstreamMailServerRRindex + 1) % upstreamMailServers.size(); do { if (count >= upstreamMailServers.size()) { throw ServiceException.FAILURE("All Upstream Mail Servers are unavailable", null); } else { currentIndex = (currentIndex + 1) % upstreamMailServers.size(); count++; upstreamMailServer = upstreamMailServers.get(currentIndex); String url = (new StringBuilder(protocol).append("://") .append(upstreamMailServer.ngxServerAddress.getHostName()).append(":") .append(upstreamMailServer.ngxServerAddress.getPort())).toString(); if (!ping(url, 60000)) { continue; } break; } } while (true); return new StringBuilder(upstreamMailServer.ngxServerAddress.getHostName()).append(":") .append(upstreamMailServer.ngxServerAddress.getPort()).toString(); } else { throw ServiceException.FAILURE("Upstream mail servers are not configured or set", null); } } private Route getNginxRouteHandler() throws ServiceException { // Return nginx handlers using RR algorithm int count = 0; int currentIndex = 0; Route ngxHandler = null; if (ngxLookUpServers != null && ngxLookUpServers.size() > 0) { currentIndex = nginxRRindex - 1; nginxRRindex = (nginxRRindex + 1) % ngxLookUpServers.size(); do { if (count >= ngxLookUpServers.size()) { throw ServiceException.FAILURE("All Nginx LookUp Handlers are unavailable", null); } else { currentIndex = (currentIndex + 1) % ngxLookUpServers.size(); count++; ngxHandler = ngxLookUpServers.get(currentIndex); if (ngxHandler.failureTime != 0) { if (System.nanoTime() - ngxHandler.failureTime < this.ngxRetryTimeout / 1000) { continue; } else { ngxHandler.failureTime = 0; } } // Ping the Upstream handler to check whether its up // if the handler is not reachable, mark is down String hostPath = (new StringBuilder().append(ngxHandler.ngxServerAddress.getHostName()) .append(":").append(ngxHandler.ngxServerAddress.getPort()).append(urlExtension)) .toString(); if (!ping(ngxSchemes, hostPath, ngxConnectTimeout)) { ngxHandler.failureTime = System.nanoTime(); continue; } break; } } while (true); return ngxHandler; } else { throw ServiceException.FAILURE("Nginx LookUp Handlers are not configured or set", null); } } /** * Pings a HTTP(S) URL. This effectively sends a GET request and returns <code>true</code> if the response code is 200 * @param url The HTTP(S) URL to be pinged. * @param timeout The timeout in millis for both the connection timeout. * @return <code>true</code> if the given HTTP(S) URL has returned response code 200 within the * given timeout, otherwise <code>false</code>. */ public static boolean ping(String url, int timeout) { ZimbraLog.misc.debug("attempting to ping \"%s\" with timeout %d", url, timeout); try { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setConnectTimeout(timeout); return (connection.getResponseCode() == 200) ? true : false; } catch (IOException exception) { return false; } } /** * Pings a series of URLs constructed by combining the supplied URL schemes with the given host/path. * This effectively sends a GET request and returns <code>true</code> if the response code is 200. * Will return <code>true</code> with the first successful request, else <code>false</code> if * all fail. * @param schemes The list of URL schemes to try. * @param hostPath The host and path portions of the URL to be pinged. * @param timeout The timeout in millis for both the connection timeout. * @return <code>true</code> if any of the constructed URLs return a response code 200 within the * given timeout, otherwise <code>false</code>. */ public static boolean ping(String[] schemes, String hostPath, int timeout) { for (String scheme : asList(schemes)) { if (ping(scheme + "://" + hostPath, timeout)) { return true; } } return false; } public NginxAuthServer getRouteforAccount(String userName, String authMethod, String authProtocol, String clientIP, String proxyIP, String virtualHost) throws ServiceException { Route nginxLookUpHandler = getNginxRouteHandler(); ZimbraLog.misc.debug("getting route for account %s with handler %s", userName, nginxLookUpHandler); if (nginxLookUpHandler != null) { for (String scheme : asList(ngxSchemes)) { GetMethod method = new GetMethod((new StringBuilder(scheme + "://") .append(nginxLookUpHandler.ngxServerAddress.getHostName()).append(":") .append(nginxLookUpHandler.ngxServerAddress.getPort()).append(urlExtension)).toString()); method.setRequestHeader("Auth-Method", authMethod); method.setRequestHeader("Auth-User", userName); method.setRequestHeader("Auth-Pass", ngxPassword); method.setRequestHeader("Auth-Protocol", authProtocol); // for web requests, login attempts is always 0 method.setRequestHeader("Auth-Login-Attempt", "0"); method.setRequestHeader("X-Proxy-IP", proxyIP); method.setRequestHeader("Client-IP", clientIP); method.setRequestHeader("X-Proxy-Host", virtualHost); HttpClient client = ZimbraHttpConnectionManager.getInternalHttpConnMgr().newHttpClient(); // currently we use default httpclient_internal_connmgr_connection_timeout instead of ngxConnectTimeout client.getParams().setParameter("http.protocol.version", HttpVersion.HTTP_1_0); try { int statusCode = HttpClientUtil.executeMethod(client, method); if (statusCode == 200 && method.getResponseHeader("Auth-Status").getValue().equals("OK")) { return new NginxAuthServer(method.getResponseHeader("Auth-Server").getValue(), method.getResponseHeader("Auth-Port").getValue(), method.getResponseHeader("Auth-User").getValue()); } else { ZimbraLog.misc.debug("unexpected return %d\r\n%s", statusCode, method.getResponseBodyAsString()); } } catch (IOException e) { nginxLookUpHandler.failureTime = System.nanoTime(); ZimbraLog.misc.debug("IOException getting route", e); } finally { method.releaseConnection(); } } } return null; } /** * Parse a server list. * Each server value is hostname:port or just hostname. * @param serverList * @return */ private List<Route> parseServerList(String[] servers, int defaultPort) { // Eliminate duplicates and sort case-insensitively. This negates operator error // configuring server list with inconsistent order on different Nginx Route Handler clients. // TreeSet provides deduping and sorting. TreeSet<String> tset = new TreeSet<String>(); for (int i = 0; i < servers.length; ++i) { tset.add(servers[i].toLowerCase()); } servers = tset.toArray(new String[0]); if (servers != null) { List<Route> addrs = new ArrayList<Route>(servers.length); for (String server : servers) { if (server.length() == 0) continue; // In case of nginx lookup handlers, there might be additional '/service/extension/nginx-lookup' at the end. // Remove it as the parser expects a server value with hostname:port or just hostname if (defaultPort == DEFAULT_NGINX_HANDLER_PORT) { server = server.replace(urlExtension, ""); ZimbraLog.misc.debug("Lookup server after removing urlExtension " + server); } ZimbraLog.misc.debug("Server before parsing " + server); String[] parts = server.split(":"); if (parts != null) { String host; int port = defaultPort; if (parts.length == 1) { host = parts[0]; } else if (parts.length == 2) { host = parts[0]; try { port = Integer.parseInt(parts[1]); } catch (NumberFormatException e) { ZimbraLog.misc.warn("Invalid server parsing ports " + server); continue; } } else { ZimbraLog.misc.warn("Invalid server " + server + "has %d parts" + parts.length); continue; } Route rt = this.new Route(new InetSocketAddress(host, port), 0); addrs.add(rt); } else { ZimbraLog.misc.warn("Invalid server has null parts" + server); continue; } } return addrs; } else { return new ArrayList<Route>(0); } } private class Route { private InetSocketAddress ngxServerAddress; private long failureTime; private Route(InetSocketAddress server, long failureTime) { this.ngxServerAddress = server; this.failureTime = failureTime; } } }