001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.remoting.httpinvoker;
018
019import java.io.IOException;
020import java.io.InvalidClassException;
021import java.net.ConnectException;
022
023import org.aopalliance.intercept.MethodInterceptor;
024import org.aopalliance.intercept.MethodInvocation;
025
026import org.springframework.aop.support.AopUtils;
027import org.springframework.remoting.RemoteAccessException;
028import org.springframework.remoting.RemoteConnectFailureException;
029import org.springframework.remoting.RemoteInvocationFailureException;
030import org.springframework.remoting.support.RemoteInvocation;
031import org.springframework.remoting.support.RemoteInvocationBasedAccessor;
032import org.springframework.remoting.support.RemoteInvocationResult;
033
034/**
035 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing an
036 * HTTP invoker service. The service URL must be an HTTP URL exposing
037 * an HTTP invoker service.
038 *
039 * <p>Serializes remote invocation objects and deserializes remote invocation
040 * result objects. Uses Java serialization just like RMI, but provides the
041 * same ease of setup as Caucho's HTTP-based Hessian and Burlap protocols.
042 *
043 * <P>HTTP invoker is a very extensible and customizable protocol.
044 * It supports the RemoteInvocationFactory mechanism, like RMI invoker,
045 * allowing to include additional invocation attributes (for example,
046 * a security context). Furthermore, it allows to customize request
047 * execution via the {@link HttpInvokerRequestExecutor} strategy.
048 *
049 * <p>Can use the JDK's {@link java.rmi.server.RMIClassLoader} to load classes
050 * from a given {@link #setCodebaseUrl codebase}, performing on-demand dynamic
051 * code download from a remote location. The codebase can consist of multiple
052 * URLs, separated by spaces. Note that RMIClassLoader requires a SecurityManager
053 * to be set, analogous to when using dynamic class download with standard RMI!
054 * (See the RMI documentation for details.)
055 *
056 * <p><b>WARNING: Be aware of vulnerabilities due to unsafe Java deserialization:
057 * Manipulated input streams could lead to unwanted code execution on the server
058 * during the deserialization step. As a consequence, do not expose HTTP invoker
059 * endpoints to untrusted clients but rather just between your own services.</b>
060 * In general, we strongly recommend any other message format (e.g. JSON) instead.
061 *
062 * @author Juergen Hoeller
063 * @since 1.1
064 * @see #setServiceUrl
065 * @see #setCodebaseUrl
066 * @see #setRemoteInvocationFactory
067 * @see #setHttpInvokerRequestExecutor
068 * @see HttpInvokerServiceExporter
069 * @see HttpInvokerProxyFactoryBean
070 * @see java.rmi.server.RMIClassLoader
071 */
072public class HttpInvokerClientInterceptor extends RemoteInvocationBasedAccessor
073                implements MethodInterceptor, HttpInvokerClientConfiguration {
074
075        private String codebaseUrl;
076
077        private HttpInvokerRequestExecutor httpInvokerRequestExecutor;
078
079
080        /**
081         * Set the codebase URL to download classes from if not found locally.
082         * Can consists of multiple URLs, separated by spaces.
083         * <p>Follows RMI's codebase conventions for dynamic class download.
084         * In contrast to RMI, where the server determines the URL for class download
085         * (via the "java.rmi.server.codebase" system property), it's the client
086         * that determines the codebase URL here. The server will usually be the
087         * same as for the service URL, just pointing to a different path there.
088         * @see #setServiceUrl
089         * @see org.springframework.remoting.rmi.CodebaseAwareObjectInputStream
090         * @see java.rmi.server.RMIClassLoader
091         */
092        public void setCodebaseUrl(String codebaseUrl) {
093                this.codebaseUrl = codebaseUrl;
094        }
095
096        /**
097         * Return the codebase URL to download classes from if not found locally.
098         */
099        @Override
100        public String getCodebaseUrl() {
101                return this.codebaseUrl;
102        }
103
104        /**
105         * Set the HttpInvokerRequestExecutor implementation to use for executing
106         * remote invocations.
107         * <p>Default is {@link SimpleHttpInvokerRequestExecutor}. Alternatively,
108         * consider using {@link HttpComponentsHttpInvokerRequestExecutor} for more
109         * sophisticated needs.
110         * @see SimpleHttpInvokerRequestExecutor
111         * @see HttpComponentsHttpInvokerRequestExecutor
112         */
113        public void setHttpInvokerRequestExecutor(HttpInvokerRequestExecutor httpInvokerRequestExecutor) {
114                this.httpInvokerRequestExecutor = httpInvokerRequestExecutor;
115        }
116
117        /**
118         * Return the HttpInvokerRequestExecutor used by this remote accessor.
119         * <p>Creates a default SimpleHttpInvokerRequestExecutor if no executor
120         * has been initialized already.
121         */
122        public HttpInvokerRequestExecutor getHttpInvokerRequestExecutor() {
123                if (this.httpInvokerRequestExecutor == null) {
124                        SimpleHttpInvokerRequestExecutor executor = new SimpleHttpInvokerRequestExecutor();
125                        executor.setBeanClassLoader(getBeanClassLoader());
126                        this.httpInvokerRequestExecutor = executor;
127                }
128                return this.httpInvokerRequestExecutor;
129        }
130
131        @Override
132        public void afterPropertiesSet() {
133                super.afterPropertiesSet();
134
135                // Eagerly initialize the default HttpInvokerRequestExecutor, if needed.
136                getHttpInvokerRequestExecutor();
137        }
138
139
140        @Override
141        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
142                if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
143                        return "HTTP invoker proxy for service URL [" + getServiceUrl() + "]";
144                }
145
146                RemoteInvocation invocation = createRemoteInvocation(methodInvocation);
147                RemoteInvocationResult result;
148
149                try {
150                        result = executeRequest(invocation, methodInvocation);
151                }
152                catch (Throwable ex) {
153                        RemoteAccessException rae = convertHttpInvokerAccessException(ex);
154                        throw (rae != null ? rae : ex);
155                }
156
157                try {
158                        return recreateRemoteInvocationResult(result);
159                }
160                catch (Throwable ex) {
161                        if (result.hasInvocationTargetException()) {
162                                throw ex;
163                        }
164                        else {
165                                throw new RemoteInvocationFailureException("Invocation of method [" + methodInvocation.getMethod() +
166                                                "] failed in HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
167                        }
168                }
169        }
170
171        /**
172         * Execute the given remote invocation via the {@link HttpInvokerRequestExecutor}.
173         * <p>This implementation delegates to {@link #executeRequest(RemoteInvocation)}.
174         * Can be overridden to react to the specific original MethodInvocation.
175         * @param invocation the RemoteInvocation to execute
176         * @param originalInvocation the original MethodInvocation (can e.g. be cast
177         * to the ProxyMethodInvocation interface for accessing user attributes)
178         * @return the RemoteInvocationResult object
179         * @throws Exception in case of errors
180         */
181        protected RemoteInvocationResult executeRequest(
182                        RemoteInvocation invocation, MethodInvocation originalInvocation) throws Exception {
183
184                return executeRequest(invocation);
185        }
186
187        /**
188         * Execute the given remote invocation via the {@link HttpInvokerRequestExecutor}.
189         * <p>Can be overridden in subclasses to pass a different configuration object
190         * to the executor. Alternatively, add further configuration properties in a
191         * subclass of this accessor: By default, the accessor passed itself as
192         * configuration object to the executor.
193         * @param invocation the RemoteInvocation to execute
194         * @return the RemoteInvocationResult object
195         * @throws IOException if thrown by I/O operations
196         * @throws ClassNotFoundException if thrown during deserialization
197         * @throws Exception in case of general errors
198         * @see #getHttpInvokerRequestExecutor
199         * @see HttpInvokerClientConfiguration
200         */
201        protected RemoteInvocationResult executeRequest(RemoteInvocation invocation) throws Exception {
202                return getHttpInvokerRequestExecutor().executeRequest(this, invocation);
203        }
204
205        /**
206         * Convert the given HTTP invoker access exception to an appropriate
207         * Spring {@link RemoteAccessException}.
208         * @param ex the exception to convert
209         * @return the RemoteAccessException to throw, or {@code null} to have the
210         * original exception propagated to the caller
211         */
212        protected RemoteAccessException convertHttpInvokerAccessException(Throwable ex) {
213                if (ex instanceof ConnectException) {
214                        return new RemoteConnectFailureException(
215                                        "Could not connect to HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
216                }
217
218                if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError ||
219                                ex instanceof InvalidClassException) {
220                        return new RemoteAccessException(
221                                        "Could not deserialize result from HTTP invoker remote service [" + getServiceUrl() + "]", ex);
222                }
223
224                if (ex instanceof Exception) {
225                        return new RemoteAccessException(
226                                        "Could not access HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
227                }
228
229                // For any other Throwable, e.g. OutOfMemoryError: let it get propagated as-is.
230                return null;
231        }
232
233}