KNOX-998 - Bring branch up to speed with 0.14.0 RC1
Project: http://git-wip-us.apache.org/repos/asf/knox/repo Commit: http://git-wip-us.apache.org/repos/asf/knox/commit/e70904b3 Tree: http://git-wip-us.apache.org/repos/asf/knox/tree/e70904b3 Diff: http://git-wip-us.apache.org/repos/asf/knox/diff/e70904b3 Branch: refs/heads/master Commit: e70904b3d32af5df4c55f652187eda6b3719ab37 Parents: 2c69152 Author: Sandeep More <[email protected]> Authored: Mon Nov 13 10:47:33 2017 -0500 Committer: Sandeep More <[email protected]> Committed: Mon Nov 13 10:47:33 2017 -0500 ---------------------------------------------------------------------- gateway-adapter/pom.xml | 2 +- .../service/admin/TopologiesResource.java | 2 +- .../resources/services/nifi/1.4.0/service.xml | 2 +- .../hadoop/gateway/dispatch/NiFiDispatch.java | 106 ------------------ .../hadoop/gateway/dispatch/NiFiHaDispatch.java | 111 ------------------- .../hadoop/gateway/dispatch/NiFiHeaders.java | 26 ----- .../gateway/dispatch/NiFiRequestUtil.java | 89 --------------- .../gateway/dispatch/NiFiResponseUtil.java | 89 --------------- .../knox/gateway/dispatch/NiFiDispatch.java | 106 ++++++++++++++++++ .../knox/gateway/dispatch/NiFiHaDispatch.java | 111 +++++++++++++++++++ .../knox/gateway/dispatch/NiFiHeaders.java | 26 +++++ .../knox/gateway/dispatch/NiFiRequestUtil.java | 89 +++++++++++++++ .../knox/gateway/dispatch/NiFiResponseUtil.java | 88 +++++++++++++++ .../src/test/resources/log4j.properties | 2 +- 14 files changed, 424 insertions(+), 425 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-adapter/pom.xml ---------------------------------------------------------------------- diff --git a/gateway-adapter/pom.xml b/gateway-adapter/pom.xml index 4bb62e3..d6bd49e 100644 --- a/gateway-adapter/pom.xml +++ b/gateway-adapter/pom.xml @@ -23,7 +23,7 @@ <parent> <artifactId>gateway</artifactId> <groupId>org.apache.knox</groupId> - <version>0.14.0-SNAPSHOT</version> + <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>gateway-adapter</artifactId> <name>gateway-adapter</name> http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java ---------------------------------------------------------------------- diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java index 9ecd7fc..f960734 100644 --- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java +++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java @@ -145,7 +145,7 @@ public class TopologiesResource { // Check for existing topology with the same name, to see if it had been generated boolean existingGenerated = false; - for (org.apache.hadoop.gateway.topology.Topology existingTopology : ts.getTopologies()) { + for (org.apache.knox.gateway.topology.Topology existingTopology : ts.getTopologies()) { if(existingTopology.getName().equals(id)) { existingGenerated = existingTopology.isGenerated(); break; http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-definitions/src/main/resources/services/nifi/1.4.0/service.xml ---------------------------------------------------------------------- diff --git a/gateway-service-definitions/src/main/resources/services/nifi/1.4.0/service.xml b/gateway-service-definitions/src/main/resources/services/nifi/1.4.0/service.xml index 2ccc10d..d600e2d 100644 --- a/gateway-service-definitions/src/main/resources/services/nifi/1.4.0/service.xml +++ b/gateway-service-definitions/src/main/resources/services/nifi/1.4.0/service.xml @@ -26,5 +26,5 @@ <rewrite apply="NIFI/nifi/inbound/path/query-other" to="request.url"/> </route> </routes> - <dispatch classname="org.apache.hadoop.gateway.dispatch.NiFiDispatch" ha-classname="org.apache.hadoop.gateway.dispatch.NiFiHaDispatch" /> + <dispatch classname="org.apache.knox.gateway.dispatch.NiFiDispatch" ha-classname="org.apache.knox.gateway.dispatch.NiFiHaDispatch" /> </service> http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiDispatch.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiDispatch.java b/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiDispatch.java deleted file mode 100644 index 013fd9c..0000000 --- a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiDispatch.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.hadoop.gateway.dispatch; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.hadoop.gateway.util.MimeTypes; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.ContentType; - -public class NiFiDispatch extends DefaultDispatch { - - @Override - protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException { - outboundRequest = NiFiRequestUtil.modifyOutboundRequest(outboundRequest, inboundRequest); - HttpResponse inboundResponse = executeOutboundRequest(outboundRequest); - writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse); - } - - /** - * Overridden to provide a spot to modify the outbound response before its stream is closed. - */ - protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { - // Copy the client respond header to the server respond. - outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode()); - Header[] headers = inboundResponse.getAllHeaders(); - Set<String> excludeHeaders = getOutboundResponseExcludeHeaders(); - boolean hasExcludeHeaders = false; - if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) { - hasExcludeHeaders = true; - } - for ( Header header : headers ) { - String name = header.getName(); - if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase())) { - continue; - } - String value = header.getValue(); - outboundResponse.addHeader(name, value); - } - - HttpEntity entity = inboundResponse.getEntity(); - if( entity != null ) { - outboundResponse.setContentType( getInboundResponseContentType( entity ) ); - InputStream stream = entity.getContent(); - try { - NiFiResponseUtil.modifyOutboundResponse(inboundRequest, outboundResponse, inboundResponse); - writeResponse( inboundRequest, outboundResponse, stream ); - } finally { - closeInboundResponse( inboundResponse, stream ); - } - } - } - - /** - * Overriden due to {@link DefaultDispatch#getInboundResponseContentType(HttpEntity) having private access, and the method is used by - * {@link #writeOutboundResponse(HttpUriRequest, HttpServletRequest, HttpServletResponse, HttpResponse)}} - */ - private String getInboundResponseContentType( final HttpEntity entity ) { - String fullContentType = null; - if( entity != null ) { - ContentType entityContentType = ContentType.get( entity ); - if( entityContentType != null ) { - if( entityContentType.getCharset() == null ) { - final String entityMimeType = entityContentType.getMimeType(); - final String defaultCharset = MimeTypes.getDefaultCharsetForMimeType( entityMimeType ); - if( defaultCharset != null ) { - LOG.usingDefaultCharsetForEntity( entityMimeType, defaultCharset ); - entityContentType = entityContentType.withCharset( defaultCharset ); - } - } else { - LOG.usingExplicitCharsetForEntity( entityContentType.getMimeType(), entityContentType.getCharset() ); - } - fullContentType = entityContentType.toString(); - } - } - if( fullContentType == null ) { - LOG.unknownResponseEntityContentType(); - } else { - LOG.inboundResponseEntityContentType( fullContentType ); - } - return fullContentType; - } -} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHaDispatch.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHaDispatch.java b/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHaDispatch.java deleted file mode 100644 index 4272086..0000000 --- a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHaDispatch.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.hadoop.gateway.dispatch; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.hadoop.gateway.ha.dispatch.DefaultHaDispatch; -import org.apache.hadoop.gateway.util.MimeTypes; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.ContentType; - -public class NiFiHaDispatch extends DefaultHaDispatch { - - public NiFiHaDispatch() { - setServiceRole("NIFI"); - } - - @Override - protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException { - outboundRequest = NiFiRequestUtil.modifyOutboundRequest(outboundRequest, inboundRequest); - HttpResponse inboundResponse = executeOutboundRequest(outboundRequest); - writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse); - } - - /** - * Overridden to provide a spot to modify the outbound response before its stream is closed. - */ - protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { - // Copy the client respond header to the server respond. - outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode()); - Header[] headers = inboundResponse.getAllHeaders(); - Set<String> excludeHeaders = getOutboundResponseExcludeHeaders(); - boolean hasExcludeHeaders = false; - if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) { - hasExcludeHeaders = true; - } - for ( Header header : headers ) { - String name = header.getName(); - if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase())) { - continue; - } - String value = header.getValue(); - outboundResponse.addHeader(name, value); - } - - HttpEntity entity = inboundResponse.getEntity(); - if( entity != null ) { - outboundResponse.setContentType( getInboundResponseContentType( entity ) ); - InputStream stream = entity.getContent(); - try { - NiFiResponseUtil.modifyOutboundResponse(inboundRequest, outboundResponse, inboundResponse); - writeResponse( inboundRequest, outboundResponse, stream ); - } finally { - closeInboundResponse( inboundResponse, stream ); - } - } - } - - /** - * Overriden due to {@link DefaultDispatch#getInboundResponseContentType(HttpEntity) having private access, and the method is used by - * {@link #writeOutboundResponse(HttpUriRequest, HttpServletRequest, HttpServletResponse, HttpResponse)}} - */ - private String getInboundResponseContentType( final HttpEntity entity ) { - String fullContentType = null; - if( entity != null ) { - ContentType entityContentType = ContentType.get( entity ); - if( entityContentType != null ) { - if( entityContentType.getCharset() == null ) { - final String entityMimeType = entityContentType.getMimeType(); - final String defaultCharset = MimeTypes.getDefaultCharsetForMimeType( entityMimeType ); - if( defaultCharset != null ) { - DefaultDispatch.LOG.usingDefaultCharsetForEntity( entityMimeType, defaultCharset ); - entityContentType = entityContentType.withCharset( defaultCharset ); - } - } else { - DefaultDispatch.LOG.usingExplicitCharsetForEntity( entityContentType.getMimeType(), entityContentType.getCharset() ); - } - fullContentType = entityContentType.toString(); - } - } - if( fullContentType == null ) { - DefaultDispatch.LOG.unknownResponseEntityContentType(); - } else { - DefaultDispatch.LOG.inboundResponseEntityContentType( fullContentType ); - } - return fullContentType; - } -} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHeaders.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHeaders.java b/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHeaders.java deleted file mode 100644 index f3e8e68..0000000 --- a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiHeaders.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.hadoop.gateway.dispatch; - -class NiFiHeaders { - static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; - static final String X_FORWARDED_HOST = "X-Forwarded-Server"; - static final String X_FORWARDED_PORT = "X-Forwarded-Port"; - static final String X_FORWARDED_CONTEXT = "X-Forwarded-Context"; - static final String X_PROXIED_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; -} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiRequestUtil.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiRequestUtil.java b/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiRequestUtil.java deleted file mode 100644 index 9fdc425..0000000 --- a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiRequestUtil.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.hadoop.gateway.dispatch; - -import java.io.IOException; - -import javax.security.auth.Subject; -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.lang.StringUtils; -import org.apache.hadoop.gateway.security.SubjectUtils; -import org.apache.http.Header; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.methods.RequestBuilder; -import org.apache.log4j.Logger; - -import com.google.common.base.Objects; -import com.google.common.base.Strings; - -class NiFiRequestUtil { - - static HttpUriRequest modifyOutboundRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest) throws IOException { - // preserve trailing slash from inbound request in the outbound request - if (inboundRequest.getPathInfo().endsWith("/")) { - String[] split = outboundRequest.getURI().toString().split("\\?"); - if (!split[0].endsWith("/")) { - outboundRequest = RequestBuilder.copy(outboundRequest).setUri(split[0] + "/" + (split.length == 2 ? "?" + split[1] : "")).build(); - } - } - // update the X-Forwarded-Context header to include the Knox-specific context path - final Header originalXForwardedContextHeader = outboundRequest.getFirstHeader(NiFiHeaders.X_FORWARDED_CONTEXT); - if (originalXForwardedContextHeader != null) { - String xForwardedContextHeaderValue = originalXForwardedContextHeader.getValue(); - if (!Strings.isNullOrEmpty(xForwardedContextHeaderValue)) { - // Inspect the inbound request and outbound request to determine the additional context path from the rewrite - // rules that needs to be added to the X-Forwarded-Context header to allow proper proxying to NiFi. - // - // NiFi does its own URL rewriting, and will not work with the context path provided by Knox - // (ie, "/gateway/sandbox"). - // - // For example, if Knox has a rewrite rule "*://*:*/**/nifi-app/{**}?{**}", "/nifi-app" needs to be added - // to the existing value of the X-Forwarded-Context header, which ends up being "/gateway/sandbox/nifi-app". - String inboundRequestPathInfo = inboundRequest.getPathInfo(); - String outboundRequestUriPath = outboundRequest.getURI().getPath(); - String outboundRequestUriPathNoTrailingSlash = StringUtils.removeEnd(outboundRequestUriPath, "/"); - String knoxRouteContext = null; - int index = inboundRequestPathInfo.lastIndexOf(outboundRequestUriPathNoTrailingSlash); - if (index >= 0) { - knoxRouteContext = inboundRequestPathInfo.substring(0, index); - } else { - Logger.getLogger(NiFiHaDispatch.class.getName()).error(String.format("Unable to find index of %s in %s", outboundRequestUriPathNoTrailingSlash, inboundRequestPathInfo)); - } - outboundRequest.setHeader(NiFiHeaders.X_FORWARDED_CONTEXT, xForwardedContextHeaderValue + knoxRouteContext); - } - } - - // NiFi requires the header "X-ProxiedEntitiesChain" to be set with the identity or identities of the authenticated requester. - // The effective principal (identity) in the requester subject must be added to "X-ProxiedEntitiesChain". - // If the request already has a populated "X-ProxiedEntitiesChain" header, the identities must be appended to it. - // If the user proxied through Knox is anonymous, the "Anonymous" identity needs to be represented in X-ProxiedEntitiesChain - // as empty angle brackets "<>". - final Subject subject = SubjectUtils.getCurrentSubject(); - String effectivePrincipalName = SubjectUtils.getEffectivePrincipalName(subject); - outboundRequest.setHeader(NiFiHeaders.X_PROXIED_ENTITIES_CHAIN, Objects.firstNonNull(inboundRequest.getHeader(NiFiHeaders.X_PROXIED_ENTITIES_CHAIN), "") + - String.format("<%s>", effectivePrincipalName.equalsIgnoreCase("anonymous") ? "" : effectivePrincipalName)); - - // Make sure headers named "Cookie" are removed from the request to NiFi, since NiFi does not use cookies. - Header[] cookieHeaders = outboundRequest.getHeaders("Cookie"); - for (Header cookieHeader : cookieHeaders) { - outboundRequest.removeHeader(cookieHeader); - } - return outboundRequest; - } -} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiResponseUtil.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiResponseUtil.java b/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiResponseUtil.java deleted file mode 100644 index 38c98b3..0000000 --- a/gateway-service-nifi/src/main/java/org/apache/hadoop/gateway/dispatch/NiFiResponseUtil.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.hadoop.gateway.dispatch; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.lang.StringUtils; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URIBuilder; - -class NiFiResponseUtil { - - static void modifyOutboundResponse(HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { - // Only want to rewrite the Location header on a HTTP 302 - if (inboundResponse.getStatusLine().getStatusCode() == HttpServletResponse.SC_FOUND) { - Header originalLocationHeader = inboundResponse.getFirstHeader("Location"); - if (originalLocationHeader != null) { - String originalLocation = originalLocationHeader.getValue(); - URIBuilder originalLocationUriBuilder; - try { - originalLocationUriBuilder = new URIBuilder(originalLocation); - } catch (URISyntaxException e) { - throw new RuntimeException("Unable to parse URI from Location header", e); - } - URIBuilder inboundRequestUriBuilder = null; - try { - inboundRequestUriBuilder = new URIBuilder(inboundRequest.getRequestURI()); - } catch (URISyntaxException e) { - throw new RuntimeException("Unable to parse the inbound request URI", e); - } - /* - * if the path specified in the Location header fron the inbound response contains the inbound request's URI's path, - * then it's going to the same web context, and the Location header should be updated based on the X_FORWARDED_* headers. - */ - String inboundRequestUriPath = inboundRequestUriBuilder.getPath(); - String originalLocationUriPath = originalLocationUriBuilder.getPath(); - if (originalLocationUriPath.contains(inboundRequestUriPath)) { - // check for trailing slash of Location header if it exists and preserve it - final String trailingSlash = originalLocationUriPath.endsWith("/") ? "/" : ""; - // retain query params - final List<NameValuePair> queryParams = originalLocationUriBuilder.getQueryParams(); - - // check for proxy settings - final String scheme = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PROTO); - final String host = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_HOST); - final String port = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PORT); - - final String baseContextPath = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_CONTEXT); - final String pathInfo = inboundRequest.getPathInfo(); - - try { - final URI newLocation = new URIBuilder().setScheme(scheme).setHost(host).setPort((StringUtils.isNumeric(port) ? Integer.parseInt(port) : -1)).setPath( - baseContextPath + pathInfo + trailingSlash).setParameters(queryParams).build(); - outboundResponse.setHeader("Location", newLocation.toString()); - } catch (URISyntaxException e) { - throw new RuntimeException("Unable to rewrite Location header in response", e); - } - } - } else { - throw new RuntimeException("Received HTTP 302, but response is missing Location header"); - } - } - } -} - http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiDispatch.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiDispatch.java b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiDispatch.java new file mode 100644 index 0000000..d939180 --- /dev/null +++ b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiDispatch.java @@ -0,0 +1,106 @@ +/* + * 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.knox.gateway.dispatch; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.knox.gateway.util.MimeTypes; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; + +public class NiFiDispatch extends DefaultDispatch { + + @Override + protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException { + outboundRequest = NiFiRequestUtil.modifyOutboundRequest(outboundRequest, inboundRequest); + HttpResponse inboundResponse = executeOutboundRequest(outboundRequest); + writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse); + } + + /** + * Overridden to provide a spot to modify the outbound response before its stream is closed. + */ + protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { + // Copy the client respond header to the server respond. + outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode()); + Header[] headers = inboundResponse.getAllHeaders(); + Set<String> excludeHeaders = getOutboundResponseExcludeHeaders(); + boolean hasExcludeHeaders = false; + if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) { + hasExcludeHeaders = true; + } + for ( Header header : headers ) { + String name = header.getName(); + if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase())) { + continue; + } + String value = header.getValue(); + outboundResponse.addHeader(name, value); + } + + HttpEntity entity = inboundResponse.getEntity(); + if( entity != null ) { + outboundResponse.setContentType( getInboundResponseContentType( entity ) ); + InputStream stream = entity.getContent(); + try { + NiFiResponseUtil.modifyOutboundResponse(inboundRequest, outboundResponse, inboundResponse); + writeResponse( inboundRequest, outboundResponse, stream ); + } finally { + closeInboundResponse( inboundResponse, stream ); + } + } + } + + /** + * Overriden due to {@link DefaultDispatch#getInboundResponseContentType(HttpEntity) having private access, and the method is used by + * {@link #writeOutboundResponse(HttpUriRequest, HttpServletRequest, HttpServletResponse, HttpResponse)}} + */ + private String getInboundResponseContentType( final HttpEntity entity ) { + String fullContentType = null; + if( entity != null ) { + ContentType entityContentType = ContentType.get( entity ); + if( entityContentType != null ) { + if( entityContentType.getCharset() == null ) { + final String entityMimeType = entityContentType.getMimeType(); + final String defaultCharset = MimeTypes.getDefaultCharsetForMimeType( entityMimeType ); + if( defaultCharset != null ) { + LOG.usingDefaultCharsetForEntity( entityMimeType, defaultCharset ); + entityContentType = entityContentType.withCharset( defaultCharset ); + } + } else { + LOG.usingExplicitCharsetForEntity( entityContentType.getMimeType(), entityContentType.getCharset() ); + } + fullContentType = entityContentType.toString(); + } + } + if( fullContentType == null ) { + LOG.unknownResponseEntityContentType(); + } else { + LOG.inboundResponseEntityContentType( fullContentType ); + } + return fullContentType; + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java new file mode 100644 index 0000000..5e1e3a0 --- /dev/null +++ b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java @@ -0,0 +1,111 @@ +/* + * 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.knox.gateway.dispatch; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.knox.gateway.ha.dispatch.DefaultHaDispatch; +import org.apache.knox.gateway.util.MimeTypes; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; + +public class NiFiHaDispatch extends DefaultHaDispatch { + + public NiFiHaDispatch() { + setServiceRole("NIFI"); + } + + @Override + protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException { + outboundRequest = NiFiRequestUtil.modifyOutboundRequest(outboundRequest, inboundRequest); + HttpResponse inboundResponse = executeOutboundRequest(outboundRequest); + writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse); + } + + /** + * Overridden to provide a spot to modify the outbound response before its stream is closed. + */ + protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { + // Copy the client respond header to the server respond. + outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode()); + Header[] headers = inboundResponse.getAllHeaders(); + Set<String> excludeHeaders = getOutboundResponseExcludeHeaders(); + boolean hasExcludeHeaders = false; + if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) { + hasExcludeHeaders = true; + } + for ( Header header : headers ) { + String name = header.getName(); + if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase())) { + continue; + } + String value = header.getValue(); + outboundResponse.addHeader(name, value); + } + + HttpEntity entity = inboundResponse.getEntity(); + if( entity != null ) { + outboundResponse.setContentType( getInboundResponseContentType( entity ) ); + InputStream stream = entity.getContent(); + try { + NiFiResponseUtil.modifyOutboundResponse(inboundRequest, outboundResponse, inboundResponse); + writeResponse( inboundRequest, outboundResponse, stream ); + } finally { + closeInboundResponse( inboundResponse, stream ); + } + } + } + + /** + * Overriden due to {@link DefaultDispatch#getInboundResponseContentType(HttpEntity) having private access, and the method is used by + * {@link #writeOutboundResponse(HttpUriRequest, HttpServletRequest, HttpServletResponse, HttpResponse)}} + */ + private String getInboundResponseContentType( final HttpEntity entity ) { + String fullContentType = null; + if( entity != null ) { + ContentType entityContentType = ContentType.get( entity ); + if( entityContentType != null ) { + if( entityContentType.getCharset() == null ) { + final String entityMimeType = entityContentType.getMimeType(); + final String defaultCharset = MimeTypes.getDefaultCharsetForMimeType( entityMimeType ); + if( defaultCharset != null ) { + DefaultDispatch.LOG.usingDefaultCharsetForEntity( entityMimeType, defaultCharset ); + entityContentType = entityContentType.withCharset( defaultCharset ); + } + } else { + DefaultDispatch.LOG.usingExplicitCharsetForEntity( entityContentType.getMimeType(), entityContentType.getCharset() ); + } + fullContentType = entityContentType.toString(); + } + } + if( fullContentType == null ) { + DefaultDispatch.LOG.unknownResponseEntityContentType(); + } else { + DefaultDispatch.LOG.inboundResponseEntityContentType( fullContentType ); + } + return fullContentType; + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHeaders.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHeaders.java b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHeaders.java new file mode 100644 index 0000000..2de967e --- /dev/null +++ b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHeaders.java @@ -0,0 +1,26 @@ +/* + * 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.knox.gateway.dispatch; + +class NiFiHeaders { + static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; + static final String X_FORWARDED_HOST = "X-Forwarded-Server"; + static final String X_FORWARDED_PORT = "X-Forwarded-Port"; + static final String X_FORWARDED_CONTEXT = "X-Forwarded-Context"; + static final String X_PROXIED_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; +} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiRequestUtil.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiRequestUtil.java b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiRequestUtil.java new file mode 100644 index 0000000..7df3a09 --- /dev/null +++ b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiRequestUtil.java @@ -0,0 +1,89 @@ +/* + * 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.knox.gateway.dispatch; + +import java.io.IOException; + +import javax.security.auth.Subject; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.apache.knox.gateway.security.SubjectUtils; +import org.apache.http.Header; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.log4j.Logger; + +import com.google.common.base.Objects; +import com.google.common.base.Strings; + +class NiFiRequestUtil { + + static HttpUriRequest modifyOutboundRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest) throws IOException { + // preserve trailing slash from inbound request in the outbound request + if (inboundRequest.getPathInfo().endsWith("/")) { + String[] split = outboundRequest.getURI().toString().split("\\?"); + if (!split[0].endsWith("/")) { + outboundRequest = RequestBuilder.copy(outboundRequest).setUri(split[0] + "/" + (split.length == 2 ? "?" + split[1] : "")).build(); + } + } + // update the X-Forwarded-Context header to include the Knox-specific context path + final Header originalXForwardedContextHeader = outboundRequest.getFirstHeader(NiFiHeaders.X_FORWARDED_CONTEXT); + if (originalXForwardedContextHeader != null) { + String xForwardedContextHeaderValue = originalXForwardedContextHeader.getValue(); + if (!Strings.isNullOrEmpty(xForwardedContextHeaderValue)) { + // Inspect the inbound request and outbound request to determine the additional context path from the rewrite + // rules that needs to be added to the X-Forwarded-Context header to allow proper proxying to NiFi. + // + // NiFi does its own URL rewriting, and will not work with the context path provided by Knox + // (ie, "/gateway/sandbox"). + // + // For example, if Knox has a rewrite rule "*://*:*/**/nifi-app/{**}?{**}", "/nifi-app" needs to be added + // to the existing value of the X-Forwarded-Context header, which ends up being "/gateway/sandbox/nifi-app". + String inboundRequestPathInfo = inboundRequest.getPathInfo(); + String outboundRequestUriPath = outboundRequest.getURI().getPath(); + String outboundRequestUriPathNoTrailingSlash = StringUtils.removeEnd(outboundRequestUriPath, "/"); + String knoxRouteContext = null; + int index = inboundRequestPathInfo.lastIndexOf(outboundRequestUriPathNoTrailingSlash); + if (index >= 0) { + knoxRouteContext = inboundRequestPathInfo.substring(0, index); + } else { + Logger.getLogger(NiFiHaDispatch.class.getName()).error(String.format("Unable to find index of %s in %s", outboundRequestUriPathNoTrailingSlash, inboundRequestPathInfo)); + } + outboundRequest.setHeader(NiFiHeaders.X_FORWARDED_CONTEXT, xForwardedContextHeaderValue + knoxRouteContext); + } + } + + // NiFi requires the header "X-ProxiedEntitiesChain" to be set with the identity or identities of the authenticated requester. + // The effective principal (identity) in the requester subject must be added to "X-ProxiedEntitiesChain". + // If the request already has a populated "X-ProxiedEntitiesChain" header, the identities must be appended to it. + // If the user proxied through Knox is anonymous, the "Anonymous" identity needs to be represented in X-ProxiedEntitiesChain + // as empty angle brackets "<>". + final Subject subject = SubjectUtils.getCurrentSubject(); + String effectivePrincipalName = SubjectUtils.getEffectivePrincipalName(subject); + outboundRequest.setHeader(NiFiHeaders.X_PROXIED_ENTITIES_CHAIN, Objects.firstNonNull(inboundRequest.getHeader(NiFiHeaders.X_PROXIED_ENTITIES_CHAIN), "") + + String.format("<%s>", effectivePrincipalName.equalsIgnoreCase("anonymous") ? "" : effectivePrincipalName)); + + // Make sure headers named "Cookie" are removed from the request to NiFi, since NiFi does not use cookies. + Header[] cookieHeaders = outboundRequest.getHeaders("Cookie"); + for (Header cookieHeader : cookieHeaders) { + outboundRequest.removeHeader(cookieHeader); + } + return outboundRequest; + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java ---------------------------------------------------------------------- diff --git a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java new file mode 100644 index 0000000..b2d9ebb --- /dev/null +++ b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java @@ -0,0 +1,88 @@ +/* + * 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.knox.gateway.dispatch; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; + +class NiFiResponseUtil { + + static void modifyOutboundResponse(HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { + // Only want to rewrite the Location header on a HTTP 302 + if (inboundResponse.getStatusLine().getStatusCode() == HttpServletResponse.SC_FOUND) { + Header originalLocationHeader = inboundResponse.getFirstHeader("Location"); + if (originalLocationHeader != null) { + String originalLocation = originalLocationHeader.getValue(); + URIBuilder originalLocationUriBuilder; + try { + originalLocationUriBuilder = new URIBuilder(originalLocation); + } catch (URISyntaxException e) { + throw new RuntimeException("Unable to parse URI from Location header", e); + } + URIBuilder inboundRequestUriBuilder = null; + try { + inboundRequestUriBuilder = new URIBuilder(inboundRequest.getRequestURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("Unable to parse the inbound request URI", e); + } + /* + * if the path specified in the Location header fron the inbound response contains the inbound request's URI's path, + * then it's going to the same web context, and the Location header should be updated based on the X_FORWARDED_* headers. + */ + String inboundRequestUriPath = inboundRequestUriBuilder.getPath(); + String originalLocationUriPath = originalLocationUriBuilder.getPath(); + if (originalLocationUriPath.contains(inboundRequestUriPath)) { + // check for trailing slash of Location header if it exists and preserve it + final String trailingSlash = originalLocationUriPath.endsWith("/") ? "/" : ""; + // retain query params + final List<NameValuePair> queryParams = originalLocationUriBuilder.getQueryParams(); + + // check for proxy settings + final String scheme = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PROTO); + final String host = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_HOST); + final String port = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PORT); + + final String baseContextPath = inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_CONTEXT); + final String pathInfo = inboundRequest.getPathInfo(); + + try { + final URI newLocation = new URIBuilder().setScheme(scheme).setHost(host).setPort((StringUtils.isNumeric(port) ? Integer.parseInt(port) : -1)).setPath( + baseContextPath + pathInfo + trailingSlash).setParameters(queryParams).build(); + outboundResponse.setHeader("Location", newLocation.toString()); + } catch (URISyntaxException e) { + throw new RuntimeException("Unable to rewrite Location header in response", e); + } + } + } else { + throw new RuntimeException("Received HTTP 302, but response is missing Location header"); + } + } + } +} + http://git-wip-us.apache.org/repos/asf/knox/blob/e70904b3/gateway-test/src/test/resources/log4j.properties ---------------------------------------------------------------------- diff --git a/gateway-test/src/test/resources/log4j.properties b/gateway-test/src/test/resources/log4j.properties index e500707..f3ee344 100644 --- a/gateway-test/src/test/resources/log4j.properties +++ b/gateway-test/src/test/resources/log4j.properties @@ -24,7 +24,7 @@ log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%c] %m%n #log4j.logger.org.apache.knox.gateway=DEBUG -#log4j.logger.org.apache.hadoop.test=DEBUG +#log4j.logger.org.apache.knox.test=DEBUG #log4j.logger.org.apache.knox.gateway.http=TRACE #log4j.logger.org.apache.knox.gateway.http.request.body=OFF #log4j.logger.org.apache.knox.gateway.http.response.body=OFF
