Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,496 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Date; +import java.util.Iterator; +import java.util.UUID; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.Header; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.SocketConfig; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.discovery.ClusterView; +import org.apache.sling.discovery.InstanceDescription; +import org.apache.sling.discovery.base.commons.ClusterViewService; +import org.apache.sling.discovery.base.commons.UndefinedClusterViewException; +import org.apache.sling.discovery.base.connectors.BaseConfig; +import org.apache.sling.discovery.base.connectors.announcement.Announcement; +import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter; +import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A topology connector client is used for sending (pinging) a remote topology + * connector servlet and exchanging announcements with it + */ +public class TopologyConnectorClient implements + TopologyConnectorClientInformation { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** the endpoint url **/ + private final URL connectorUrl; + + /** the cluster view service **/ + private final ClusterViewService clusterViewService; + + /** the config service to user **/ + private final BaseConfig config; + + /** the id of this connection **/ + private final UUID id; + + /** the announcement registry **/ + private final AnnouncementRegistry announcementRegistry; + + /** the last inherited announcement **/ + private Announcement lastInheritedAnnouncement; + + /** the time when the last announcement was inherited - for webconsole use only **/ + private long lastPingedAt; + + /** the information about this server **/ + private final String serverInfo; + + /** the status code of the last post **/ + private int lastStatusCode = -1; + + /** SLING-3316: whether or not this connector was auto-stopped **/ + private boolean autoStopped = false; + + /** more details about connection failures **/ + private String statusDetails = null; + + /** SLING-2882: whether or not to suppress ping warnings **/ + private boolean suppressPingWarnings_ = false; + + private TopologyRequestValidator requestValidator; + + /** value of Content-Encoding of the last request **/ + private String lastRequestEncoding; + + /** value of Content-Encoding of the last repsonse **/ + private String lastResponseEncoding; + + /** SLING-3382: unix-time at which point the backoff-period ends and pings can be sent again **/ + private long backoffPeriodEnd = -1; + + TopologyConnectorClient(final ClusterViewService clusterViewService, + final AnnouncementRegistry announcementRegistry, final BaseConfig config, + final URL connectorUrl, final String serverInfo) { + if (clusterViewService == null) { + throw new IllegalArgumentException( + "clusterViewService must not be null"); + } + if (announcementRegistry == null) { + throw new IllegalArgumentException( + "announcementRegistry must not be null"); + } + if (config == null) { + throw new IllegalArgumentException("config must not be null"); + } + if (connectorUrl == null) { + throw new IllegalArgumentException("connectorUrl must not be null"); + } + this.requestValidator = new TopologyRequestValidator(config); + this.clusterViewService = clusterViewService; + this.announcementRegistry = announcementRegistry; + this.config = config; + this.connectorUrl = connectorUrl; + this.serverInfo = serverInfo; + this.id = UUID.randomUUID(); + } + + /** ping the server and pass the announcements between the two **/ + void ping(final boolean force) { + if (autoStopped) { + // then we suppress any further pings! + logger.debug("ping: autoStopped=true, hence suppressing any further pings."); + return; + } + if (force) { + backoffPeriodEnd = -1; + } else if (backoffPeriodEnd>0) { + if (System.currentTimeMillis()<backoffPeriodEnd) { + logger.debug("ping: not issueing a heartbeat due to backoff instruction from peer."); + return; + } else { + logger.debug("ping: backoff period ended, issuing another ping now."); + } + } + final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json"; + if (logger.isDebugEnabled()) { + logger.debug("ping: connectorUrl=" + connectorUrl + ", complete uri=" + uri); + } + final HttpClientContext clientContext = HttpClientContext.create(); + final CloseableHttpClient httpClient = createHttpClient(); + final HttpPut putRequest = new HttpPut(uri); + + // setting the connection timeout (idle connection, configured in seconds) + putRequest.setConfig(RequestConfig. + custom(). + setConnectTimeout(1000*config.getSocketConnectionTimeout()). + build()); + + Announcement resultingAnnouncement = null; + try { + String userInfo = connectorUrl.getUserInfo(); + if (userInfo != null) { + Credentials c = new UsernamePasswordCredentials(userInfo); + clientContext.getCredentialsProvider().setCredentials( + new AuthScope(putRequest.getURI().getHost(), putRequest + .getURI().getPort()), c); + } + + Announcement topologyAnnouncement = new Announcement( + clusterViewService.getSlingId()); + topologyAnnouncement.setServerInfo(serverInfo); + final ClusterView clusterView; + try { + clusterView = clusterViewService + .getLocalClusterView(); + } catch (UndefinedClusterViewException e) { + // SLING-5030 : then we cannot ping + logger.warn("ping: no clusterView available at the moment, cannot ping others now: "+e); + return; + } + topologyAnnouncement.setLocalCluster(clusterView); + if (force) { + logger.debug("ping: sending a resetBackoff"); + topologyAnnouncement.setResetBackoff(true); + } + announcementRegistry.addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() { + + public boolean accept(final String receivingSlingId, final Announcement announcement) { + // filter out announcements that are of old cluster instances + // which I dont really have in my cluster view at the moment + final Iterator<InstanceDescription> it = + clusterView.getInstances().iterator(); + while(it.hasNext()) { + final InstanceDescription instance = it.next(); + if (instance.getSlingId().equals(receivingSlingId)) { + // then I have the receiving instance in my cluster view + // all fine then + return true; + } + } + // looks like I dont have the receiving instance in my cluster view + // then I should also not propagate that announcement anywhere + return false; + } + }); + final String p = requestValidator.encodeMessage(topologyAnnouncement.asJSON()); + + if (logger.isDebugEnabled()) { + logger.debug("ping: topologyAnnouncement json is: " + p); + } + requestValidator.trustMessage(putRequest, p); + if (config.isGzipConnectorRequestsEnabled()) { + // tell the server that the content is gzipped: + putRequest.addHeader("Content-Encoding", "gzip"); + // and gzip the body: + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final GZIPOutputStream gzipOut = new GZIPOutputStream(baos); + gzipOut.write(p.getBytes("UTF-8")); + gzipOut.close(); + final byte[] gzippedEncodedJson = baos.toByteArray(); + putRequest.setEntity(new ByteArrayEntity(gzippedEncodedJson, ContentType.APPLICATION_JSON)); + lastRequestEncoding = "gzip"; + } else { + // otherwise plaintext: + final StringEntity plaintext = new StringEntity(p, "UTF-8"); + plaintext.setContentType(ContentType.APPLICATION_JSON.getMimeType()); + putRequest.setEntity(plaintext); + lastRequestEncoding = "plaintext"; + } + // independent of request-gzipping, we do accept the response to be gzipped, + // so indicate this to the server: + putRequest.addHeader("Accept-Encoding", "gzip"); + final CloseableHttpResponse response = httpClient.execute(putRequest, clientContext); + if (logger.isDebugEnabled()) { + logger.debug("ping: done. code=" + response.getStatusLine().getStatusCode() + " - " + + response.getStatusLine().getReasonPhrase()); + } + lastStatusCode = response.getStatusLine().getStatusCode(); + lastResponseEncoding = null; + if (response.getStatusLine().getStatusCode()==HttpServletResponse.SC_OK) { + final Header contentEncoding = response.getFirstHeader("Content-Encoding"); + if (contentEncoding!=null && contentEncoding.getValue()!=null && + contentEncoding.getValue().contains("gzip")) { + lastResponseEncoding = "gzip"; + } else { + lastResponseEncoding = "plaintext"; + } + final String responseBody = requestValidator.decodeMessage(putRequest.getURI().getPath(), response); // limiting to 16MB, should be way enough + if (logger.isDebugEnabled()) { + logger.debug("ping: response body=" + responseBody); + } + if (responseBody!=null && responseBody.length()>0) { + Announcement inheritedAnnouncement = Announcement + .fromJSON(responseBody); + final long backoffInterval = inheritedAnnouncement.getBackoffInterval(); + if (backoffInterval>0) { + // then reset the backoffPeriodEnd: + + /* minus 1 sec to avoid slipping the interval by a few millis */ + this.backoffPeriodEnd = System.currentTimeMillis() + (1000 * backoffInterval) - 1000; + logger.debug("ping: servlet instructed to backoff: backoffInterval="+backoffInterval+", resulting in period end of "+new Date(backoffPeriodEnd)); + } else { + logger.debug("ping: servlet did not instruct any backoff-ing at this stage"); + this.backoffPeriodEnd = -1; + } + if (inheritedAnnouncement.isLoop()) { + if (logger.isDebugEnabled()) { + logger.debug("ping: connector response indicated a loop detected. not registering this announcement from "+ + inheritedAnnouncement.getOwnerId()); + } + if (inheritedAnnouncement.getOwnerId().equals(clusterViewService.getSlingId())) { + // SLING-3316 : local-loop detected. Check config to see if we should stop this connector + + if (config.isAutoStopLocalLoopEnabled()) { + inheritedAnnouncement = null; // results in connected -> false and representsloop -> true + autoStopped = true; // results in isAutoStopped -> true + } + } + } else { + inheritedAnnouncement.setInherited(true); + if (announcementRegistry + .registerAnnouncement(inheritedAnnouncement)==-1) { + if (logger.isDebugEnabled()) { + logger.debug("ping: connector response is from an instance which I already see in my topology" + + inheritedAnnouncement); + } + statusDetails = "receiving side is seeing me via another path (connector or cluster) already (loop)"; + return; + } + } + resultingAnnouncement = inheritedAnnouncement; + statusDetails = null; + } else { + statusDetails = "no response body received"; + } + } else { + statusDetails = "got HTTP Status-Code: "+lastStatusCode; + } + // SLING-2882 : reset suppressPingWarnings_ flag in success case + suppressPingWarnings_ = false; + } catch (IOException e) { + // SLING-2882 : set/check the suppressPingWarnings_ flag + if (suppressPingWarnings_) { + if (logger.isDebugEnabled()) { + logger.debug("ping: got IOException: " + e + ", uri=" + uri); + } + } else { + suppressPingWarnings_ = true; + logger.warn("ping: got IOException [suppressing further warns]: " + e + ", uri=" + uri); + } + statusDetails = e.toString(); + } catch (JSONException e) { + logger.warn("ping: got JSONException: " + e); + statusDetails = e.toString(); + } catch (RuntimeException re) { + logger.warn("ping: got RuntimeException: " + re, re); + statusDetails = re.toString(); + } finally { + putRequest.releaseConnection(); + lastInheritedAnnouncement = resultingAnnouncement; + lastPingedAt = System.currentTimeMillis(); + try { + httpClient.close(); + } catch (IOException e) { + logger.error("disconnect: could not close httpClient: "+e, e); + } + } + } + + private CloseableHttpClient createHttpClient() { + final HttpClientBuilder builder = HttpClientBuilder.create(); + // setting the SoTimeout (which is configured in seconds) + builder.setDefaultSocketConfig(SocketConfig. + custom(). + setSoTimeout(1000*config.getSoTimeout()). + build()); + builder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)); + + return builder.build(); + } + + public int getStatusCode() { + return lastStatusCode; + } + + public URL getConnectorUrl() { + return connectorUrl; + } + + public boolean representsLoop() { + if (autoStopped) { + return true; + } + if (lastInheritedAnnouncement == null) { + return false; + } else { + return lastInheritedAnnouncement.isLoop(); + } + } + + public boolean isConnected() { + if (autoStopped) { + return false; + } + if (lastInheritedAnnouncement == null) { + return false; + } else { + return announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId()); + } + } + + public String getStatusDetails() { + if (autoStopped) { + return "auto-stopped"; + } + if (lastInheritedAnnouncement == null) { + return statusDetails; + } else { + if (announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId())) { + // still active - so no status details + return null; + } else { + return "received announcement has expired (it was last renewed "+new Date(lastPingedAt)+") - consider increasing heartbeat timeout"; + } + } + } + + public long getLastPingSent() { + return lastPingedAt; + } + + public int getNextPingDue() { + final long absDue; + if (backoffPeriodEnd>0) { + absDue = backoffPeriodEnd; + } else { + absDue = lastPingedAt + 1000*config.getConnectorPingInterval(); + } + final int relDue = (int) ((absDue - System.currentTimeMillis()) / 1000); + if (relDue<0) { + return -1; + } else { + return relDue; + } + } + + public boolean isAutoStopped() { + return autoStopped; + } + + public String getLastRequestEncoding() { + return lastRequestEncoding==null ? "" : lastRequestEncoding; + } + + public String getLastResponseEncoding() { + return lastResponseEncoding==null ? "" : lastResponseEncoding; + } + + public String getRemoteSlingId() { + if (lastInheritedAnnouncement == null) { + return null; + } else { + return lastInheritedAnnouncement.getOwnerId(); + } + } + + public String getId() { + return id.toString(); + } + + /** Disconnect this connector **/ + public void disconnect() { + final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json"; + if (logger.isDebugEnabled()) { + logger.debug("disconnect: connectorUrl=" + connectorUrl + ", complete uri="+uri); + } + + if (lastInheritedAnnouncement != null) { + announcementRegistry + .unregisterAnnouncement(lastInheritedAnnouncement + .getOwnerId()); + } + + final HttpClientContext clientContext = HttpClientContext.create(); + final CloseableHttpClient httpClient = createHttpClient(); + final HttpDelete deleteRequest = new HttpDelete(uri); + // setting the connection timeout (idle connection, configured in seconds) + deleteRequest.setConfig(RequestConfig. + custom(). + setConnectTimeout(1000*config.getSocketConnectionTimeout()). + build()); + + try { + String userInfo = connectorUrl.getUserInfo(); + if (userInfo != null) { + Credentials c = new UsernamePasswordCredentials(userInfo); + clientContext.getCredentialsProvider().setCredentials( + new AuthScope(deleteRequest.getURI().getHost(), deleteRequest + .getURI().getPort()), c); + } + + requestValidator.trustMessage(deleteRequest, null); + final CloseableHttpResponse response = httpClient.execute(deleteRequest, clientContext); + if (logger.isDebugEnabled()) { + logger.debug("disconnect: done. code=" + response.getStatusLine().getStatusCode() + + " - " + response.getStatusLine().getReasonPhrase()); + } + // ignoring the actual statuscode though as there's little we can + // do about it after this point + } catch (IOException e) { + logger.warn("disconnect: got IOException: " + e); + } catch (RuntimeException re) { + logger.error("disconnect: got RuntimeException: " + re, re); + } finally { + deleteRequest.releaseConnection(); + try { + httpClient.close(); + } catch (IOException e) { + logger.error("disconnect: could not close httpClient: "+e, e); + } + } + } +}
Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping; + +import java.net.URL; + +/** + * provides information about a topology connector client + */ +public interface TopologyConnectorClientInformation { + + /** the endpoint url where this connector is connecting to **/ + URL getConnectorUrl(); + + /** return the http status code of the last post to the servlet, -1 if no post was ever done **/ + int getStatusCode(); + + /** SLING-3316 : whether or not this connector was auto-stopped **/ + boolean isAutoStopped(); + + /** whether or not this connector was able to successfully connect **/ + boolean isConnected(); + + /** provides more details about connection failures **/ + String getStatusDetails(); + + /** whether or not the counterpart of this connector has detected a loop in the topology connectors **/ + boolean representsLoop(); + + /** the sling id of the remote end **/ + String getRemoteSlingId(); + + /** the unique id of this connector **/ + String getId(); + + /** the Content-Encoding of the last request **/ + String getLastRequestEncoding(); + + /** the Content-Encoding of the last response **/ + String getLastResponseEncoding(); + + /** the unix-millis when the last heartbeat was sent **/ + long getLastPingSent(); + + /** the seconds until the next heartbeat is due **/ + int getNextPingDue(); +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.discovery.ClusterView; +import org.apache.sling.discovery.base.commons.ClusterViewHelper; +import org.apache.sling.discovery.base.commons.ClusterViewService; +import org.apache.sling.discovery.base.commons.UndefinedClusterViewException; +import org.apache.sling.discovery.base.connectors.BaseConfig; +import org.apache.sling.discovery.base.connectors.announcement.Announcement; +import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter; +import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry; +import org.apache.sling.discovery.base.connectors.ping.wl.SubnetWhitelistEntry; +import org.apache.sling.discovery.base.connectors.ping.wl.WhitelistEntry; +import org.apache.sling.discovery.base.connectors.ping.wl.WildcardWhitelistEntry; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet which receives topology announcements at + * /libs/sling/topology/connector* + * without authorization (authorization is handled either via + * hmac-signature with a shared key or via a flexible whitelist) + */ +@SuppressWarnings("serial") +@Component(immediate = true) +@Service(value=TopologyConnectorServlet.class) +public class TopologyConnectorServlet extends HttpServlet { + + /** + * prefix under which the topology connector servlet is registered - + * the URL will consist of this prefix + "connector.slingId.json" + */ + private static final String TOPOLOGY_CONNECTOR_PREFIX = "/libs/sling/topology"; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Reference + private AnnouncementRegistry announcementRegistry; + + @Reference + private ClusterViewService clusterViewService; + + @Reference + private HttpService httpService; + + @Reference + private BaseConfig config; + + /** + * This list contains WhitelistEntry (ips/hostnames, cidr, wildcards), + * each filtering some hostname/addresses that are allowed to connect to this servlet. + **/ + private final List<WhitelistEntry> whitelist = new ArrayList<WhitelistEntry>(); + + /** Set of plaintext whitelist entries - for faster lookups **/ + private final Set<String> plaintextWhitelist = new HashSet<String>(); + + private TopologyRequestValidator requestValidator; + + @Activate + protected void activate(final ComponentContext context) { + whitelist.clear(); + if (!config.isHmacEnabled()) { + String[] whitelistConfig = config.getTopologyConnectorWhitelist(); + initWhitelist(whitelistConfig); + } + requestValidator = new TopologyRequestValidator(config); + + try { + httpService.registerServlet(TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX, + this, null, null); + logger.info("activate: connector servlet registered at "+ + TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX); + } catch (ServletException e) { + logger.error("activate: ServletException while registering topology connector servlet: "+e, e); + } catch (NamespaceException e) { + logger.error("activate: NamespaceException while registering topology connector servlet: "+e, e); + } + } + + @Deactivate + protected void deactivate() { + httpService.unregister(TOPOLOGY_CONNECTOR_PREFIX); + } + + void initWhitelist(String[] whitelistConfig) { + if (whitelistConfig==null) { + return; + } + for (int i = 0; i < whitelistConfig.length; i++) { + String aWhitelistEntry = whitelistConfig[i]; + + WhitelistEntry whitelistEntry = null; + if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains("/")) { + // then this is a CIDR notation + try{ + whitelistEntry = new SubnetWhitelistEntry(aWhitelistEntry); + } catch(Exception e) { + logger.error("activate: wrongly formatted CIDR subnet definition. Expected eg '1.2.3.4/24'. ignoring: "+aWhitelistEntry); + continue; + } + } else if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains(" ")) { + // then this is a IP/subnet-mask notation + try{ + final StringTokenizer st = new StringTokenizer(aWhitelistEntry, " "); + final String ip = st.nextToken(); + if (st.hasMoreTokens()) { + final String mask = st.nextToken(); + if (st.hasMoreTokens()) { + logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry); + continue; + } + whitelistEntry = new SubnetWhitelistEntry(ip, mask); + } + } catch(Exception e) { + logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry); + continue; + } + } + if (whitelistEntry==null) { + if (aWhitelistEntry.contains("*") || aWhitelistEntry.contains("?")) { + whitelistEntry = new WildcardWhitelistEntry(aWhitelistEntry); + } else { + plaintextWhitelist.add(aWhitelistEntry); + } + } + logger.info("activate: adding whitelist entry: " + aWhitelistEntry); + if (whitelistEntry!=null) { + whitelist.add(whitelistEntry); + } + } + } + + @Override + protected void doDelete(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + if (!isWhitelisted(request)) { + // in theory it would be 403==forbidden, but that would reveal that + // a resource would exist there in the first place + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + final String[] pathInfo = request.getPathInfo().split("\\."); + final String extension = pathInfo.length==3 ? pathInfo[2] : ""; + if (!"json".equals(extension)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + final String selector = pathInfo.length==3 ? pathInfo[1] : ""; + + announcementRegistry.unregisterAnnouncement(selector); + } + + @Override + protected void doPut(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + if (!isWhitelisted(request)) { + // in theory it would be 403==forbidden, but that would reveal that + // a resource would exist there in the first place + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + final String[] pathInfo = request.getPathInfo().split("\\."); + final String extension = pathInfo.length==3 ? pathInfo[2] : ""; + if (!"json".equals(extension)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + final String selector = pathInfo.length==3 ? pathInfo[1] : ""; + + String topologyAnnouncementJSON = requestValidator.decodeMessage(request); + if (logger.isDebugEnabled()) { + logger.debug("doPost: incoming topology announcement is: " + + topologyAnnouncementJSON); + } + final Announcement incomingTopologyAnnouncement; + try { + incomingTopologyAnnouncement = Announcement + .fromJSON(topologyAnnouncementJSON); + + if (!incomingTopologyAnnouncement.getOwnerId().equals(selector)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + String slingId = clusterViewService.getSlingId(); + if (slingId==null) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + logger.info("doPut: no slingId available. Service not ready as expected at the moment."); + return; + } + incomingTopologyAnnouncement.removeInherited(slingId); + + final Announcement replyAnnouncement = new Announcement( + slingId); + + long backoffInterval = -1; + ClusterView clusterView = clusterViewService.getLocalClusterView(); + if (!incomingTopologyAnnouncement.isCorrectVersion()) { + logger.warn("doPost: rejecting an announcement from an incompatible connector protocol version: " + + incomingTopologyAnnouncement); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } else if (ClusterViewHelper.contains(clusterView, incomingTopologyAnnouncement + .getOwnerId())) { + if (logger.isDebugEnabled()) { + logger.debug("doPost: rejecting an announcement from an instance that is part of my cluster: " + + incomingTopologyAnnouncement); + } + // marking as 'loop' + replyAnnouncement.setLoop(true); + backoffInterval = config.getBackoffStandbyInterval(); + } else if (ClusterViewHelper.containsAny(clusterView, incomingTopologyAnnouncement + .listInstances())) { + if (logger.isDebugEnabled()) { + logger.debug("doPost: rejecting an announcement as it contains instance(s) that is/are part of my cluster: " + + incomingTopologyAnnouncement); + } + // marking as 'loop' + replyAnnouncement.setLoop(true); + backoffInterval = config.getBackoffStandbyInterval(); + } else { + backoffInterval = announcementRegistry + .registerAnnouncement(incomingTopologyAnnouncement); + if (logger.isDebugEnabled()) { + logger.debug("doPost: backoffInterval after registration: "+backoffInterval); + } + if (backoffInterval==-1) { + if (logger.isDebugEnabled()) { + logger.debug("doPost: rejecting an announcement from an instance that I already see in my topology: " + + incomingTopologyAnnouncement); + } + // marking as 'loop' + replyAnnouncement.setLoop(true); + backoffInterval = config.getBackoffStandbyInterval(); + } else { + // normal, successful case: replying with the part of the topology which this instance sees + replyAnnouncement.setLocalCluster(clusterView); + announcementRegistry.addAllExcept(replyAnnouncement, clusterView, + new AnnouncementFilter() { + + public boolean accept(final String receivingSlingId, Announcement announcement) { + if (announcement.getPrimaryKey().equals( + incomingTopologyAnnouncement + .getPrimaryKey())) { + return false; + } + return true; + } + }); + } + } + if (backoffInterval>0) { + replyAnnouncement.setBackoffInterval(backoffInterval); + if (logger.isDebugEnabled()) { + logger.debug("doPost: backoffInterval for client set to "+replyAnnouncement.getBackoffInterval()); + } + } + final String p = requestValidator.encodeMessage(replyAnnouncement.asJSON()); + requestValidator.trustMessage(response, request, p); + // gzip the response if the client accepts this + final String acceptEncodingHeader = request.getHeader("Accept-Encoding"); + if (acceptEncodingHeader!=null && acceptEncodingHeader.contains("gzip")) { + // tell the client that the content is gzipped: + response.setHeader("Content-Encoding", "gzip"); + + // then gzip the body + final GZIPOutputStream gzipOut = new GZIPOutputStream(response.getOutputStream()); + gzipOut.write(p.getBytes("UTF-8")); + gzipOut.close(); + } else { + // otherwise plaintext + final PrintWriter pw = response.getWriter(); + pw.print(p); + pw.flush(); + } + } catch (JSONException e) { + logger.error("doPost: Got a JSONException: " + e, e); + response.sendError(500); + } catch (UndefinedClusterViewException e) { + logger.warn("doPost: no clusterView available at the moment - cannot handle connectors now: "+e); + response.sendError(503); // "please retry, but atm I can't help since I'm isolated" + } + + } + + /** Checks if the provided request's remote server is whitelisted **/ + boolean isWhitelisted(final HttpServletRequest request) { + if (config.isHmacEnabled()) { + final boolean isTrusted = requestValidator.isTrusted(request); + if (!isTrusted) { + logger.info("isWhitelisted: rejecting distrusted " + request.getRemoteAddr() + + ", " + request.getRemoteHost()); + } + return isTrusted; + } + + if (plaintextWhitelist.contains(request.getRemoteHost()) || + plaintextWhitelist.contains(request.getRemoteAddr())) { + return true; + } + + for (Iterator<WhitelistEntry> it = whitelist.iterator(); it.hasNext();) { + WhitelistEntry whitelistEntry = it.next(); + if (whitelistEntry.accepts(request)) { + return true; + } + } + logger.info("isWhitelisted: rejecting " + request.getRemoteAddr() + + ", " + request.getRemoteHost()); + return false; + } + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,585 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The SF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.sling.discovery.base.connectors.ping; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.GZIPInputStream; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.sling.commons.json.JSONArray; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; +import org.apache.sling.discovery.base.connectors.BaseConfig; + +/** + * Request Validator helper. + */ +public class TopologyRequestValidator { + + public static final String SIG_HEADER = "X-SlingTopologyTrust"; + + public static final String HASH_HEADER = "X-SlingTopologyHash"; + + /** + * Maximum number of keys to keep in memory. + */ + private static final int MAXKEYS = 5; + + /** + * Minimum number of keys to keep in memory. + */ + private static final int MINKEYS = 3; + + /** + * true if trust information should be in request headers. + */ + private boolean trustEnabled; + + /** + * true if encryption of the message payload should be encrypted. + */ + private boolean encryptionEnabled; + + /** + * map of hmac keys, keyed by key number. + */ + private Map<Integer, Key> keys = new ConcurrentHashMap<Integer, Key>(); + + /** + * The shared key. + */ + private String sharedKey; + + /** + * TTL of each shared key generation. + */ + private long interval; + + /** + * If true, everything is deactivated. + */ + private boolean deactivated; + + private SecureRandom random = new SecureRandom(); + + /** + * Create a TopologyRequestValidator. + * + * @param config the configuation object + */ + public TopologyRequestValidator(BaseConfig config) { + trustEnabled = false; + encryptionEnabled = false; + if (config.isHmacEnabled()) { + trustEnabled = true; + sharedKey = config.getSharedKey(); + interval = config.getKeyInterval(); + encryptionEnabled = config.isEncryptionEnabled(); + } + deactivated = false; + } + + /** + * Encodes a request returning the encoded body + * + * @param body + * @return the encoded body. + * @throws IOException + */ + public String encodeMessage(String body) throws IOException { + checkActive(); + if (encryptionEnabled) { + try { + JSONObject json = new JSONObject(); + json.put("payload", new JSONArray(encrypt(body))); + return json.toString(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (IllegalBlockSizeException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (BadPaddingException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (UnsupportedEncodingException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (NoSuchPaddingException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (JSONException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (InvalidKeySpecException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } catch (InvalidParameterSpecException e) { + throw new IOException("Unable to Encrypt Message " + e.getMessage()); + } + + } + return body; + } + + /** + * Decode a message sent from the client. + * + * @param request the request object for the message. + * @return the message in clear text. + * @throws IOException if there is a problem decoding the message or the + * message is invalid. + */ + public String decodeMessage(HttpServletRequest request) throws IOException { + checkActive(); + return decodeMessage("request:", request.getRequestURI(), getRequestBody(request), + request.getHeader(HASH_HEADER)); + } + + /** + * Decode a response from the server. + * + * @param response the response. + * @return the message in clear text. + * @throws IOException if there was a problem decoding the message. + */ + public String decodeMessage(String uri, HttpResponse response) throws IOException { + checkActive(); + return decodeMessage("response:", uri, getResponseBody(response), + getResponseHeader(response, HASH_HEADER)); + } + + /** + * Decode a message + * + * @param prefix the prefix to indicate if the message is a request or + * response message. + * @param url the url associated with the message. + * @param body the body of the message. + * @param requestHash a hash of the message. + * @return the message in clear text + * @throws IOException if the message can't be decrypted. + */ + private String decodeMessage(String prefix, String url, String body, String requestHash) + throws IOException { + if (trustEnabled) { + String bodyHash = hash(prefix + url + ":" + body); + if (bodyHash.equals(requestHash)) { + if (encryptionEnabled) { + try { + JSONObject json = new JSONObject(body); + if (json.has("payload")) { + return decrypt(json.getJSONArray("payload")); + } + } catch (JSONException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (InvalidKeyException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (IllegalBlockSizeException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (BadPaddingException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (NoSuchAlgorithmException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (NoSuchPaddingException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (InvalidAlgorithmParameterException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } catch (InvalidKeySpecException e) { + throw new IOException("Encrypted Message is in the correct json format"); + } + + } + } + throw new IOException("Message is not valid, hash does not match message"); + } + return body; + } + + /** + * Is the request from the client trusted, based on the signature headers. + * + * @param request the request. + * @return true if trusted, or true if this component is disabled. + */ + public boolean isTrusted(HttpServletRequest request) { + checkActive(); + if (trustEnabled) { + return checkTrustHeader(request.getHeader(HASH_HEADER), + request.getHeader(SIG_HEADER)); + } + return false; + } + + /** + * Is the response from the server to be trusted by the client. + * + * @param response the response + * @return true if trusted, or true if this component is disabled. + */ + public boolean isTrusted(HttpResponse response) { + checkActive(); + if (trustEnabled) { + return checkTrustHeader(getResponseHeader(response, HASH_HEADER), + getResponseHeader(response, SIG_HEADER)); + } + return false; + } + + /** + * Trust a message on the client before sending, only if trust is enabled. + * + * @param method the method which will have headers set after the call. + * @param body the body. + */ + public void trustMessage(HttpUriRequest method, String body) { + checkActive(); + if (trustEnabled) { + String bodyHash = hash("request:" + method.getURI().getPath() + ":" + body); + method.setHeader(HASH_HEADER, bodyHash); + method.setHeader(SIG_HEADER, createTrustHeader(bodyHash)); + } + } + + /** + * Trust a response message sent from the server to the client. + * + * @param response the response. + * @param request the request, + * @param body body of the response. + */ + public void trustMessage(HttpServletResponse response, HttpServletRequest request, String body) { + checkActive(); + if (trustEnabled) { + String bodyHash = hash("response:" + request.getRequestURI() + ":" + body); + response.setHeader(HASH_HEADER, bodyHash); + response.setHeader(SIG_HEADER, createTrustHeader(bodyHash)); + } + } + + /** + * @param body + * @return a hash of body base64 encoded. + */ + private String hash(String toHash) { + try { + MessageDigest m = MessageDigest.getInstance("SHA-256"); + return new String(Base64.encodeBase64(m.digest(toHash.getBytes("UTF-8"))), "UTF-8"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Generate a signature of the bodyHash and encode it so that it contains + * the key number used to generate the signature. + * + * @param bodyHash a hash + * @return the signature. + */ + private String createTrustHeader(String bodyHash) { + try { + int keyNo = getCurrentKey(); + return keyNo + "/" + hmac(keyNo, bodyHash); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (InvalidKeyException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (IllegalStateException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Check that the signature is a signature of the body hash. + * + * @param bodyHash the body hash. + * @param signature the signature. + * @return true if the signature can be trusted. + */ + private boolean checkTrustHeader(String bodyHash, String signature) { + try { + if (bodyHash == null || signature == null ) { + return false; + } + String[] parts = signature.split("/", 2); + int keyNo = Integer.parseInt(parts[0]); + return hmac(keyNo, bodyHash).equals(parts[1]); + } catch (ArrayIndexOutOfBoundsException e) { + return false; + } catch (IllegalArgumentException e) { + return false; + } catch (InvalidKeyException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (IllegalStateException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Get a Mac instance for the key number. + * + * @param keyNo the key number. + * @return the mac instance. + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + */ + private Mac getMac(int keyNo) throws NoSuchAlgorithmException, InvalidKeyException, + UnsupportedEncodingException { + Mac m = Mac.getInstance("HmacSHA256"); + m.init(getKey(keyNo)); + return m; + } + + /** + * Perform a HMAC on the body using the key specified. + * + * @param keyNo the key number. + * @param bodyHash a hash of the body. + * @return the hmac signature. + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws IllegalStateException + * @throws NoSuchAlgorithmException + */ + private String hmac(int keyNo, String bodyHash) throws InvalidKeyException, + UnsupportedEncodingException, IllegalStateException, NoSuchAlgorithmException { + return new String(Base64.encodeBase64(getMac(keyNo).doFinal(bodyHash.getBytes("UTF-8"))), + "UTF-8"); + } + + /** + * Decrypt the body. + * + * @param jsonArray the encrypted payload + * @return the decrypted payload. + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws UnsupportedEncodingException + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidKeySpecException + * @throws InvalidAlgorithmParameterException + * @throws JSONException + */ + private String decrypt(JSONArray jsonArray) throws IllegalBlockSizeException, + BadPaddingException, UnsupportedEncodingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, JSONException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, getCiperKey(Base64.decodeBase64(jsonArray.getString(0).getBytes("UTF-8"))), new IvParameterSpec(Base64.decodeBase64(jsonArray.getString(1).getBytes("UTF-8")))); + return new String(cipher.doFinal(Base64.decodeBase64(jsonArray.getString(2).getBytes("UTF-8")))); + } + + /** + * Encrypt a payload with the numbed key/ + * + * @param payload the payload. + * @param keyNo the key number. + * @return an encrypted version. + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws UnsupportedEncodingException + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidKeySpecException + * @throws InvalidParameterSpecException + */ + private List<String> encrypt(String payload) throws IllegalBlockSizeException, + BadPaddingException, UnsupportedEncodingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidParameterSpecException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + byte[] salt = new byte[9]; + random.nextBytes(salt); + cipher.init(Cipher.ENCRYPT_MODE, getCiperKey(salt)); + AlgorithmParameters params = cipher.getParameters(); + List<String> encrypted = new ArrayList<String>(); + encrypted.add(new String(Base64.encodeBase64(salt))); + encrypted.add(new String(Base64.encodeBase64(params.getParameterSpec(IvParameterSpec.class).getIV()))); + encrypted.add(new String(Base64.encodeBase64(cipher.doFinal(payload.getBytes("UTF-8"))))); + return encrypted; + } + + /** + * @param salt number of the key. + * @return the CupherKey. + * @throws UnsupportedEncodingException + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + private Key getCiperKey(byte[] salt) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + // hashing the password 65K times takes 151ms, hashing 256 times takes 2ms. + // Since the salt has 2^^72 values, 256 times is probably good enough. + KeySpec spec = new PBEKeySpec(sharedKey.toCharArray(), salt, 256, 128); + SecretKey tmp = factory.generateSecret(spec); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + return key; + } + + /** + * @param keyNo number of the key. + * @return the HMac key. + * @throws UnsupportedEncodingException + */ + private Key getKey(int keyNo) throws UnsupportedEncodingException { + if(Math.abs(keyNo - getCurrentKey()) > 1 ) { + throw new IllegalArgumentException("Key has expired"); + } + if (keys.containsKey(keyNo)) { + return keys.get(keyNo); + } + trimKeys(); + SecretKeySpec key = new SecretKeySpec(hash(sharedKey + keyNo).getBytes("UTF-8"), + "HmacSHA256"); + keys.put(keyNo, key); + return key; + } + + private int getCurrentKey() { + return (int) (System.currentTimeMillis() / interval); + } + + /** + * dump olf keys. + */ + private void trimKeys() { + if (keys.size() > MAXKEYS) { + List<Integer> keysKeys = new ArrayList<Integer>(keys.keySet()); + Collections.sort(keysKeys); + for (Integer k : keysKeys) { + if (keys.size() < MINKEYS) { + break; + } + keys.remove(k); + } + } + + } + + /** + * Get the value of a response header. + * + * @param response the response + * @param name the name of the response header. + * @return the value of the response header, null if none. + */ + private String getResponseHeader(HttpResponse response, String name) { + Header h = response.getFirstHeader(name); + if (h == null) { + return null; + } + return h.getValue(); + } + + /** + * Get the request body. + * + * @param request the request. + * @return the body as a string. + * @throws IOException + */ + private String getRequestBody(HttpServletRequest request) throws IOException { + final String contentEncoding = request.getHeader("Content-Encoding"); + if (contentEncoding!=null && contentEncoding.contains("gzip")) { + // then treat the request body as gzip: + final GZIPInputStream gzipIn = new GZIPInputStream(request.getInputStream()); + final String gunzippedEncodedJson = IOUtils.toString(gzipIn); + gzipIn.close(); + return gunzippedEncodedJson; + } else { + // otherwise assume plain-text: + return IOUtils.toString(request.getReader()); + } + } + + /** + * @param response the response + * @return the body of the response from the server. + * @throws IOException + */ + private String getResponseBody(HttpResponse response) throws IOException { + final Header contentEncoding = response.getFirstHeader("Content-Encoding"); + if (contentEncoding!=null && contentEncoding.getValue()!=null && + contentEncoding.getValue().contains("gzip")) { + // then the server sent gzip - treat it so: + final GZIPInputStream gzipIn = new GZIPInputStream(response.getEntity().getContent()); + final String gunzippedEncodedJson = IOUtils.toString(gzipIn); + gzipIn.close(); + return gunzippedEncodedJson; + } else { + // otherwise the server sent plaintext: + return IOUtils.toString(response.getEntity().getContent(), "UTF-8"); + } + } + + /** + * throw an exception if not active. + */ + private void checkActive() { + if (deactivated) { + throw new IllegalStateException(this.getClass().getName() + " is not active"); + } + if ((trustEnabled || encryptionEnabled) && sharedKey == null) { + throw new IllegalStateException(this.getClass().getName() + + " Shared Key must be set if encryption or signing is enabled."); + } + } + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Provides topology connector implementations for discovery + * implementors that choose to extend from discovery.base + * + * @version 1.0.0 + */ +@Version("1.0.0") +package org.apache.sling.discovery.base.connectors.ping; + +import aQute.bnd.annotation.Version; + Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping.wl; + +import javax.servlet.ServletRequest; + +import org.apache.commons.net.util.SubnetUtils; +import org.apache.commons.net.util.SubnetUtils.SubnetInfo; + +/** + * Implementation of a WhitelistEntry which accepts + * cidr and ip mask notations. + */ +public class SubnetWhitelistEntry implements WhitelistEntry { + + private final SubnetInfo subnetInfo; + + public SubnetWhitelistEntry(String cidrNotation) { + subnetInfo = new SubnetUtils(cidrNotation).getInfo(); + } + + public SubnetWhitelistEntry(String ip, String subnetMask) { + subnetInfo = new SubnetUtils(ip, subnetMask).getInfo(); + } + + public boolean accepts(ServletRequest request) { + final String remoteAddr = request.getRemoteAddr(); + return subnetInfo.isInRange(remoteAddr); + } + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping.wl; + +import javax.servlet.ServletRequest; + +/** + * A WhitelistEntry is capable of accepting certain requests + * depending on a configuration. + */ +public interface WhitelistEntry { + + /** + * @param request the incoming request which should be accepted or rejected + * @return true if the request is accepted by this WhitelistEntry + */ + public boolean accepts(ServletRequest request); + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping.wl; + +import java.util.regex.Pattern; + +/** Helper class for wildcards **/ +public class WildcardHelper { + + /** converts a string containing wildcards (* and ?) into a valid regex **/ + public static String wildcardAsRegex(String patternWithWildcards) { + if (patternWithWildcards==null) { + throw new IllegalArgumentException("patternWithWildcards must not be null"); + } + return "\\Q"+patternWithWildcards.replace("?", "\\E.\\Q").replace("*", "\\E.*\\Q")+"\\E"; + } + + /** + * Compare a given string (comparee) against a pattern that contains wildcards + * and return true if it matches. + * @param comparee the string which should be tested against a pattern containing wildcards + * @param patternWithWildcards the pattern containing wildcards (* and ?) + * @return true if the comparee string matches against the pattern containing wildcards + */ + public static boolean matchesWildcard(String comparee, String patternWithWildcards) { + if (comparee==null) { + throw new IllegalArgumentException("comparee must not be null"); + } + if (patternWithWildcards==null) { + throw new IllegalArgumentException("patternWithEWildcards must not be null"); + } + final String regex = wildcardAsRegex(patternWithWildcards); + return Pattern.matches(regex, comparee); + } + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.discovery.base.connectors.ping.wl; + +import javax.servlet.ServletRequest; + +/** + * Implementation of a WhitelistEntry which can accept + * wildcards (* and ?) in both IP and hostname + */ +public class WildcardWhitelistEntry implements WhitelistEntry { + + private final String hostOrAddressWithWildcard; + + public WildcardWhitelistEntry(String hostOrAddressWithWildcard) { + this.hostOrAddressWithWildcard = hostOrAddressWithWildcard; + } + + public boolean accepts(ServletRequest request) { + if (WildcardHelper.matchesWildcard(request.getRemoteAddr(), hostOrAddressWithWildcard)) { + return true; + } + if (WildcardHelper.matchesWildcard(request.getRemoteHost(), hostOrAddressWithWildcard)) { + return true; + } + return false; + } + +} Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java?rev=1709601&view=auto ============================================================================== --- sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java (added) +++ sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java Tue Oct 20 14:12:31 2015 @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Provides whitelist-related classes for discovery + * implementors that choose to extend from discovery.base + * + * @version 1.0.0 + */ +@Version("1.0.0") +package org.apache.sling.discovery.base.connectors.ping.wl; + +import aQute.bnd.annotation.Version; + Propchange: sling/trunk/bundles/extensions/discovery/base/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java ------------------------------------------------------------------------------ svn:eol-style = native