/* * Copyright (c) 2005-2010, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.synapse.rest; import org.apache.axis2.Constants; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpStatus; import org.apache.http.protocol.HTTP; import org.apache.synapse.ManagedLifecycle; import org.apache.synapse.Mediator; import org.apache.synapse.MessageContext; import org.apache.synapse.SynapseConstants; import org.apache.synapse.aspects.AspectConfigurable; import org.apache.synapse.aspects.AspectConfiguration; import org.apache.synapse.aspects.ComponentType; import org.apache.synapse.aspects.flow.statistics.StatisticIdentityGenerator; import org.apache.synapse.aspects.flow.statistics.data.artifact.ArtifactHolder; import org.apache.synapse.transport.customlogsetter.CustomLogSetter; import org.apache.synapse.core.SynapseEnvironment; import org.apache.synapse.core.axis2.Axis2MessageContext; import org.apache.synapse.rest.dispatch.DispatcherHelper; import org.apache.synapse.rest.dispatch.RESTDispatcher; import org.apache.synapse.rest.version.DefaultStrategy; import org.apache.synapse.rest.version.URLBasedVersionStrategy; import org.apache.synapse.rest.version.VersionStrategy; import org.apache.synapse.config.xml.rest.VersionStrategyFactory; import org.apache.synapse.transport.http.conn.SynapseDebugInfoHolder; import org.apache.synapse.transport.http.conn.SynapseWireLogHolder; import org.apache.synapse.transport.nhttp.NhttpConstants; import java.util.*; public class API extends AbstractRESTProcessor implements ManagedLifecycle, AspectConfigurable { private String host; private int port = -1; private String context; private Map<String,Resource> resources = new LinkedHashMap<String,Resource>(); private List<Handler> handlers = new ArrayList<Handler>(); private int protocol = RESTConstants.PROTOCOL_HTTP_AND_HTTPS; private VersionStrategy versionStrategy = new DefaultStrategy(this); private String fileName; private Log apiLog; private static final Log trace = LogFactory.getLog(SynapseConstants.TRACE_LOGGER); private int traceState = SynapseConstants.TRACING_UNSET; private String artifactContainerName; private boolean isEdited = false; private AspectConfiguration aspectConfiguration; public API(String name, String context) { super(name); setContext(context); } public void setContext(String context) { if (!context.startsWith("/")) { handleException("API context must begin with '/' character"); } this.context = RESTUtils.trimTrailingSlashes(context); apiLog = LogFactory.getLog(SynapseConstants.API_LOGGER_PREFIX + name); } public void setArtifactContainerName (String name) { artifactContainerName = name; } public String getArtifactContainerName() { return artifactContainerName; } public boolean isEdited() { return isEdited; } public void setIsEdited(boolean isEdited) { this.isEdited = isEdited; } public void setLogSetterValue () { CustomLogSetter.getInstance().setLogAppender(artifactContainerName); } /** * Get the fully qualified name of this API * * @return returns the key combination for API NAME + VERSION */ public String getName() { // check if a versioning strategy exists if (versionStrategy.getVersion() != null && !"".equals(versionStrategy.getVersion()) ) { return name + ":v" +versionStrategy.getVersion(); } return name; } public int getProtocol() { return protocol; } public void setProtocol(int protocol) { this.protocol = protocol; } public String getAPIName() { return name; } public String getVersion(){ return versionStrategy.getVersion(); } public String getContext() { return context; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public void addResource(Resource resource) { DispatcherHelper dispatcherHelper = resource.getDispatcherHelper(); if (dispatcherHelper != null) { String mapping = dispatcherHelper.getString(); for (Resource r : resources.values()) { DispatcherHelper helper = r.getDispatcherHelper(); if (helper != null && helper.getString().equals(mapping) && resourceMatches(resource, r)) { handleException("Two resources cannot have the same path mapping and methods"); } } } else { for (Resource r : resources.values()) { DispatcherHelper helper = r.getDispatcherHelper(); if (helper == null) { handleException("Only one resource can be designated as default"); } } } resources.put(resource.getName(), resource); } private boolean resourceMatches(Resource r1, Resource r2) { String[] methods1 = r1.getMethods(); String[] methods2 = r2.getMethods(); for (String m1 : methods1) { for (String m2 : methods2) { if (m1.equals(m2)) { return true; } } } return false; } public Resource[] getResources() { return resources.values().toArray(new Resource[resources.size()]); } public void addHandler(Handler handler) { handlers.add(handler); } public Handler[] getHandlers() { return handlers.toArray(new Handler[handlers.size()]); } boolean canProcess(MessageContext synCtx) { if (synCtx.isResponse()) { String apiName = (String) synCtx.getProperty(RESTConstants.SYNAPSE_REST_API); String version = synCtx.getProperty(RESTConstants.SYNAPSE_REST_API_VERSION) == null ? "": (String) synCtx.getProperty(RESTConstants.SYNAPSE_REST_API_VERSION); //if api name is not matching OR versions are different if (!getName().equals(apiName) || !versionStrategy.getVersion().equals(version)) { return false; } } else { String path = RESTUtils.getFullRequestPath(synCtx); if (!path.startsWith(context + "/") && !path.startsWith(context + "?") && !context.equals(path) && !"/".equals(context)) { auditDebug("API context: " + context + " does not match request URI: " + path); return false; } if(!versionStrategy.isMatchingVersion(synCtx)){ return false; } org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); if (host != null || port != -1) { String hostHeader = getHostHeader(msgCtx); if (hostHeader != null) { if (host != null && !host.equals(extractHostName(hostHeader))) { auditDebug("API host: " + host + " does not match host information " + "in the request: " + hostHeader); return false; } if (port != -1 && port != extractPortNumber(hostHeader, msgCtx.getIncomingTransportName())) { auditDebug("API port: " + port + " does not match port information " + "in the request: " + hostHeader); return false; } } else { auditDebug("Host information not available on the message"); return false; } } if (protocol == RESTConstants.PROTOCOL_HTTP_ONLY && !Constants.TRANSPORT_HTTP.equals(msgCtx.getIncomingTransportName())) { if (log.isDebugEnabled()) { log.debug("Protocol information does not match - Expected HTTP"); } synCtx.setProperty(SynapseConstants.TRANSPORT_DENIED,new Boolean(true)); synCtx.setProperty(SynapseConstants.IN_TRANSPORT,msgCtx.getTransportIn().getName()); log.warn("Trying to access API : "+name+" on restricted transport chanel ["+msgCtx.getTransportIn().getName()+"]"); return false; } if (protocol == RESTConstants.PROTOCOL_HTTPS_ONLY && !Constants.TRANSPORT_HTTPS.equals(msgCtx.getIncomingTransportName())) { if (log.isDebugEnabled()) { log.debug("Protocol information does not match - Expected HTTPS"); } synCtx.setProperty(SynapseConstants.TRANSPORT_DENIED,new Boolean(true)); synCtx.setProperty(SynapseConstants.IN_TRANSPORT,msgCtx.getTransportIn().getName()); log.warn("Trying to access API : "+name+" on restricted transport chanel ["+msgCtx.getTransportIn().getName()+"]"); return false; } } return true; } void process(MessageContext synCtx) { auditDebug("Processing message with ID: " + synCtx.getMessageID() + " through the " + "API: " + name); synCtx.setProperty(RESTConstants.SYNAPSE_REST_API, getName()); synCtx.setProperty(RESTConstants.SYNAPSE_REST_API_VERSION, versionStrategy.getVersion()); synCtx.setProperty(RESTConstants.REST_API_CONTEXT, context); synCtx.setProperty(RESTConstants.SYNAPSE_REST_API_VERSION_STRATEGY, versionStrategy.getVersionType()); // get API log for this message and attach to the message context ((Axis2MessageContext) synCtx).setServiceLog(apiLog); // Calculate REST_URL_POSTFIX from full request path String restURLPostfix = (String) synCtx.getProperty(RESTConstants.REST_FULL_REQUEST_PATH); if (!synCtx.isResponse() && restURLPostfix != null) { // Skip for response path if (!restURLPostfix.startsWith("/")) { restURLPostfix = "/" + restURLPostfix; } if (restURLPostfix.startsWith(context)) { restURLPostfix = restURLPostfix.substring(context.length()); } if (versionStrategy instanceof URLBasedVersionStrategy) { String version = versionStrategy.getVersion(); if (restURLPostfix.startsWith(version)) { restURLPostfix = restURLPostfix.substring(version.length()); } else if (restURLPostfix.startsWith("/" + version)) { restURLPostfix = restURLPostfix.substring(version.length() + 1); } } ((Axis2MessageContext) synCtx).getAxis2MessageContext(). setProperty(NhttpConstants.REST_URL_POSTFIX,restURLPostfix); } for (Handler handler : handlers) { auditDebug("Processing message with ID: " + synCtx.getMessageID() + " through " + "handler: " + handler.getClass().getName()); boolean proceed; if (synCtx.isResponse()) { proceed = handler.handleResponse(synCtx); } else { proceed = handler.handleRequest(synCtx); } if (!proceed) { return; } } if (synCtx.isResponse()) { String resourceName = (String) synCtx.getProperty(RESTConstants.SYNAPSE_RESOURCE); if (resourceName != null) { Resource resource = resources.get(resourceName); if (resource != null) { resource.process(synCtx); } } else if (log.isDebugEnabled()) { auditDebug("No resource information on the response: " + synCtx.getMessageID()); } return; } String path = RESTUtils.getFullRequestPath(synCtx); String subPath; if (versionStrategy.getVersionType().equals(VersionStrategyFactory.TYPE_URL)) { //for URL based //request --> http://{host:port}/context/version/path/to/resource subPath = path.substring(context.length() + versionStrategy.getVersion().length() + 1); } else { subPath = path.substring(context.length()); } if ("".equals(subPath)) { subPath = "/"; } synCtx.setProperty(RESTConstants.REST_SUB_REQUEST_PATH, subPath); org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); String hostHeader = getHostHeader(msgCtx); if (hostHeader != null) { synCtx.setProperty(RESTConstants.REST_URL_PREFIX, msgCtx.getIncomingTransportName() + "://" + hostHeader); } Set<Resource> acceptableResources = new LinkedHashSet<Resource>(); for (Resource r : resources.values()) { if (r.canProcess(synCtx)) { acceptableResources.add(r); } } boolean processed = false; if (!acceptableResources.isEmpty()) { for (RESTDispatcher dispatcher : RESTUtils.getDispatchers()) { Resource resource = dispatcher.findResource(synCtx, acceptableResources); if (resource != null) { if (synCtx.getEnvironment().isDebuggerEnabled()) { if (!synCtx.isResponse()) { SynapseWireLogHolder wireLogHolder = (SynapseWireLogHolder) ((Axis2MessageContext) synCtx).getAxis2MessageContext() .getProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_HOLDER_PROPERTY); if (wireLogHolder == null) { wireLogHolder = new SynapseWireLogHolder(); } if (synCtx.getProperty(RESTConstants.SYNAPSE_REST_API) != null && !synCtx.getProperty(RESTConstants.SYNAPSE_REST_API).toString().isEmpty()) { wireLogHolder.setApiName(synCtx.getProperty(RESTConstants.SYNAPSE_REST_API).toString()); if (resource.getDispatcherHelper() != null) { if (resource.getDispatcherHelper().getString() != null && !resource.getDispatcherHelper().getString().isEmpty()) { wireLogHolder.setResourceUrlString(resource.getDispatcherHelper().getString()); } } } ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_HOLDER_PROPERTY, wireLogHolder); } } resource.process(synCtx); return; } } handleResourceNotFound(synCtx); } else { //This will get executed only in unhappy path. So ok to have the iterator. boolean resourceFound = false; boolean matchingMethodFound = false; for (RESTDispatcher dispatcher : RESTUtils.getDispatchers()) { Resource resource = dispatcher.findResource(synCtx, resources.values()); if (resource != null) { resourceFound = true; String method = (String) msgCtx.getProperty(Constants.Configuration.HTTP_METHOD); matchingMethodFound = resource.hasMatchingMethod(method); break; } } if (!resourceFound) { handleResourceNotFound(synCtx); } else if (resourceFound && !matchingMethodFound) { //Resource found, but in that resource, requested method not allowed. So sending method not allowed http status (405) msgCtx.setProperty(SynapseConstants.HTTP_SC, HttpStatus.SC_METHOD_NOT_ALLOWED); msgCtx.removeProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS); msgCtx.setProperty("NIO-ACK-Requested", true); } else { //Resource found, and matching method also found, which means request is BAD_REQUEST(400) msgCtx.setProperty(SynapseConstants.HTTP_SC, HttpStatus.SC_BAD_REQUEST); msgCtx.setProperty("NIO-ACK-Requested", true); } } } /** * Helper method to use when no matching resource found * * @param synCtx */ private void handleResourceNotFound(MessageContext synCtx) { auditDebug("No matching resource was found for the request: " + synCtx.getMessageID()); Mediator sequence = synCtx.getSequence(RESTConstants.NO_MATCHING_RESOURCE_HANDLER); if (sequence != null) { sequence.mediate(synCtx); } else { //Matching resource with method not found org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); msgCtx.setProperty(SynapseConstants.HTTP_SC, HttpStatus.SC_NOT_FOUND); msgCtx.removeProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS); msgCtx.setProperty("NIO-ACK-Requested", true); } } private String getHostHeader(org.apache.axis2.context.MessageContext msgCtx) { Map transportHeaders = (Map) msgCtx.getProperty( org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS); String hostHeader = null; if (transportHeaders != null) { hostHeader = (String) transportHeaders.get(HTTP.TARGET_HOST); } if (hostHeader == null) { hostHeader = (String) msgCtx.getProperty(NhttpConstants.SERVICE_PREFIX); } return hostHeader; } private String extractHostName(String hostHeader) { int index = hostHeader.indexOf(':'); if (index != -1) { return hostHeader.substring(0, index); } else { return hostHeader; } } private int extractPortNumber(String hostHeader, String transport) { int index = hostHeader.indexOf(':'); if (index != -1) { return Integer.parseInt(hostHeader.substring(index + 1)); } else if (Constants.TRANSPORT_HTTP.equals(transport)) { return 80; } else { return 443; } } public void init(SynapseEnvironment se) { if (resources.isEmpty()) { handleException("The API: " + getName() + " has been configured without " + "any resource definitions"); } auditInfo("Initializing API: " + getName()); for (Resource resource : resources.values()) { resource.init(se); } for (Handler handler : handlers) { if (handler instanceof ManagedLifecycle) { ((ManagedLifecycle) handler).init(se); } } } public void destroy() { auditInfo("Destroying API: " + getName()); for (Resource resource : resources.values()) { resource.destroy(); } for (Handler handler : handlers) { if (handler instanceof ManagedLifecycle) { ((ManagedLifecycle) handler).destroy(); } } } public VersionStrategy getVersionStrategy() { return versionStrategy; } public void setVersionStrategy(VersionStrategy versionStrategy) { this.versionStrategy = versionStrategy; } public Resource getResource(String resourceName) { return resources.get(resourceName); } private boolean trace() { return this.aspectConfiguration.isTracingEnabled(); } /** * Write to the general log, as well as any API specific logs the audit message at INFO * @param message the INFO level audit message */ private void auditInfo(String message) { log.info(message); apiLog.info(message); //TODO - Implement 'trace' attribute support in API configuration. if (trace()) { trace.info(message); } } /** * Write to the general log, as well as any API specific logs the audit message at DEBUG * @param message the DEBUG level audit message */ private void auditDebug(String message) { if (log.isDebugEnabled()){ log.debug(message); apiLog.debug(message); //TODO - Implement 'trace' attribute support in API configuration. if (trace()) { trace.debug(message); } } } @Override public void configure(AspectConfiguration aspectConfiguration) { this.aspectConfiguration = aspectConfiguration; } @Override public AspectConfiguration getAspectConfiguration() { return aspectConfiguration; } public void setComponentStatisticsId(ArtifactHolder holder){ if (aspectConfiguration == null) { aspectConfiguration = new AspectConfiguration(name); } String apiId = StatisticIdentityGenerator.getIdForComponent(name, ComponentType.API, holder); aspectConfiguration.setUniqueId(apiId); for (Resource resource : resources.values()) { resource.setComponentStatisticsId(holder); } StatisticIdentityGenerator.reportingEndEvent(apiId, ComponentType.API, holder); } }