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