View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.webscripts.servlet.mvc;
20  
21  import java.util.HashMap;
22  import java.util.Map;
23  import java.util.StringTokenizer;
24  
25  import javax.servlet.http.HttpServletRequest;
26  import javax.servlet.http.HttpServletResponse;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.springframework.extensions.config.ConfigService;
31  import org.springframework.extensions.config.RemoteConfigElement;
32  import org.springframework.extensions.config.RemoteConfigElement.EndpointDescriptor;
33  import org.springframework.extensions.config.RemoteConfigElement.IdentityType;
34  import org.springframework.extensions.surf.exception.WebScriptsPlatformException;
35  import org.springframework.extensions.surf.util.Base64;
36  import org.springframework.extensions.webscripts.connector.Connector;
37  import org.springframework.extensions.webscripts.connector.ConnectorContext;
38  import org.springframework.extensions.webscripts.connector.ConnectorService;
39  import org.springframework.extensions.webscripts.connector.Credentials;
40  import org.springframework.extensions.webscripts.connector.CredentialsImpl;
41  import org.springframework.extensions.webscripts.connector.HttpMethod;
42  import org.springframework.extensions.webscripts.connector.Response;
43  import org.springframework.web.servlet.HandlerMapping;
44  import org.springframework.web.servlet.ModelAndView;
45  import org.springframework.web.servlet.mvc.AbstractController;
46  
47  /**
48   * EndPoint HTTP Proxy Controller for Spring MVC.
49   * 
50   * Provides the ability to submit a URL request via a configured end point such as a
51   * remote Alfresco Server. Makes use of the Connector framework so that appropriate
52   * authentication is automatically applied to the proxied request as applicable.
53   * 
54   * This servlet accepts URIs of the following format:
55   * 
56   * /proxy/<endpointid>[/uri]*[?[<argName>=<argValue>]*]
57   * 
58   * Where:
59   * 
60   * - endpointid is the ID of a configured EndPoint model object to make a request against
61   * - url is the uri to call on the EndPoint URL e.g. /api/sites
62   * - argName is the name of a URL argument to append to the request
63   * - argValue is the value of URL argument
64   * 
65   * E.g.
66   * 
67   * /proxy/alfresco/api/sites?name=mysite&desc=description
68   * 
69   * The proxy supports all valid HTTP methods.
70   * 
71   * @author kevinr 
72   * @author muzquiano
73   */
74  public class EndPointProxyController extends AbstractController
75  {
76      private static final String USER_ID = "USER_ID";
77      private static final String PARAM_ALF_TICKET = "alf_ticket";
78      
79      private static Log logger = LogFactory.getLog(EndPointProxyController.class);
80      
81      private static final long serialVersionUID = -176412355613122789L;
82      
83      protected ConfigService configService;
84      protected RemoteConfigElement config;
85      protected ConnectorService connectorService;
86      
87      /**
88       * Sets the config service.
89       * 
90       * @param configService the new config service
91       */
92      public void setConfigService(ConfigService configService)
93      {
94          this.configService = configService;
95      }
96      
97      /**
98       * Sets the connector service.
99       * 
100      * @param connectorService the new connector service
101      */
102     public void setConnectorService(ConnectorService connectorService)
103     {
104         this.connectorService = connectorService;
105     }
106     
107     /**
108      * Gets the remote config.
109      * 
110      * @return the remote config
111      */
112     public RemoteConfigElement getRemoteConfig()
113     {
114         if (this.config == null)
115         {
116             // retrieve the remote configuration
117             this.config = (RemoteConfigElement) configService.getConfig("Remote").getConfigElement("remote");
118         }
119         
120         return this.config;
121     }
122     
123     /* (non-Javadoc)
124      * @see org.alfresco.web.framework.mvc.AbstractController#createModelAndView(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
125      */
126     public ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse res) throws Exception
127     {
128         ModelAndView mav = null;
129         
130         // get the portion of the uri beyond the handler mapping (resolved by Spring for us)
131         String uri = (String) req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);        
132         
133         // validate and return the endpoint id from the URI path - stripping the servlet context
134         StringTokenizer t = new StringTokenizer(uri, "/");
135         if (!t.hasMoreTokens())
136         {
137             throw new IllegalArgumentException("Proxy URL did not specify endpoint id.");
138         }
139         String endpointId = t.nextToken();
140         
141         // rebuild rest of the URL for the proxy request
142         StringBuilder buf = new StringBuilder(64);
143         if (t.hasMoreTokens())
144         {
145             do
146             {
147                 buf.append('/');
148                 buf.append(t.nextToken());
149             } while (t.hasMoreTokens());
150         }
151         else
152         {
153             // allow for an empty uri to be passed in
154             // this could therefore refer to the root of a service i.e. /webapp/axis
155             buf.append('/');
156         }
157         
158         try
159         {
160             // retrieve the endpoint descriptor - do not allow proxy access to unsecure endpoints
161             EndpointDescriptor descriptor = getRemoteConfig().getEndpointDescriptor(endpointId);
162             if (descriptor == null || descriptor.getUnsecure())
163             {
164                 // throw an exception if endpoint ID is does not exist or invalid
165                 throw new WebScriptsPlatformException("Invalid EndPoint Id: " + endpointId);
166             }
167             
168             // special case for some Flash based apps - they might pass in the alf_ticket directly
169             // as a parameter as POST requests do not correctly pickup the browser cookies and
170             // therefore do not share the same session so we must apply the ticket directly
171             String ticket = req.getParameter(PARAM_ALF_TICKET);
172             
173             // user id from session NOTE: @see org.alfresco.web.site.UserFactory
174             Connector connector;
175             String userId = (String)req.getSession().getAttribute(USER_ID);
176             if (userId != null)
177             {
178                 // build an authenticated connector - as we have a userId
179                 connector = this.connectorService.getConnector(endpointId, userId, req.getSession());
180             }
181             else if (ticket != null ||
182                      descriptor.getIdentity() == IdentityType.NONE ||
183                      descriptor.getIdentity() == IdentityType.DECLARED ||
184                      descriptor.getExternalAuth())
185             {
186                 // the authentication for this endpoint is either not required, declared in config or
187                 // managed "externally" (i.e. by a servlet filter such as NTLM) - this means we should
188                 // proceed on the assumption it will be dealt with later
189                 connector = this.connectorService.getConnector(endpointId, req.getSession());
190             }
191             else if (descriptor.getBasicAuth())
192             {
193                 // check for HTTP authorisation request (i.e. RSS feeds etc.)
194                 String authorization = req.getHeader("Authorization");
195                 if (authorization == null || authorization.length() == 0)
196                 {
197                     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED,
198                             "No USER_ID found in session and requested endpoint requires authentication.");
199                     res.setHeader("WWW-Authenticate", "Basic realm=\"Alfresco\"");
200                     
201                     // no further processing as authentication is required but not provided
202                     // the browser will now prompt the user for appropriate credentials
203                     return null;
204                 }
205                 else
206                 {
207                     // user has provided authentication details with the request
208                     String[] authParts = authorization.split(" ");
209                     if (!authParts[0].equalsIgnoreCase("basic"))
210                     {
211                         throw new WebScriptsPlatformException("Authorization '" + authParts[0] + "' not supported.");
212                     }
213                     
214                     String[] values = new String(Base64.decode(authParts[1])).split(":");
215                     if (values.length == 2)
216                     {
217                         if (logger.isDebugEnabled())
218                             logger.debug("Authenticating (BASIC HTTP) user " + values[0]);
219                         
220                         // assume username and password passed as the parts and
221                         // build an unauthenticated authentication connector then
222                         // apply the supplied credentials to it
223                         connector = this.connectorService.getConnector(endpointId, values[0], req.getSession());
224                         Credentials credentials = new CredentialsImpl(endpointId);
225                         credentials.setProperty(Credentials.CREDENTIAL_USERNAME, values[0]);
226                         credentials.setProperty(Credentials.CREDENTIAL_PASSWORD, values[1]);
227                         connector.setCredentials(credentials);
228                     }
229                     else
230                     {
231                         throw new WebScriptsPlatformException("Authorization request did not provide user/pass.");
232                     }
233                 }
234             }
235             else
236             {
237                 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED,
238                         "No USER_ID found in session and requested endpoint requires authentication.");
239                 return null;
240             }
241             
242             // build a connector context, stores information about how we will drive the remote client
243             ConnectorContext context;
244             if (ticket == null)
245             {
246                 context = new ConnectorContext();
247             }
248             else
249             {
250                 // special case for some Flash apps - see above
251                 Map<String, String> params = new HashMap<String, String>(1, 1.0f);
252                 params.put(PARAM_ALF_TICKET, ticket);
253                 context = new ConnectorContext(params, null);
254             }
255             context.setContentType(req.getContentType());
256             context.setMethod(HttpMethod.valueOf(req.getMethod().toUpperCase()));
257             
258             // build proxy URL referencing the endpoint
259             String q = req.getQueryString();
260             String url = buf.toString() + (q != null && q.length() != 0 ? "?" + q : "");
261             
262             if (logger.isDebugEnabled())
263             {
264                 logger.debug("EndPointProxyServlet preparing to proxy:");
265                 logger.debug(" - endpointId: " + endpointId);
266                 logger.debug(" - userId: " + userId);
267                 logger.debug(" - connector: " + connector);
268                 logger.debug(" - method: " + context.getMethod());
269                 logger.debug(" - url: " + url);
270             }
271             
272             // call through using our connector to proxy
273             Response response = connector.call(url, context, req, res);
274             
275             if (logger.isDebugEnabled())
276             {
277                 logger.debug("Return code: " + response.getStatus().getCode());
278                 if (response.getStatus().getCode() == 500)
279                 {
280                     logger.debug("Error detected: " + response.getStatus().getMessage() + "\n" +
281                             response.getStatus().getException().toString());
282                 }
283             }
284         }
285         catch (Throwable err)
286         {
287             // TODO: trap and handle errors!
288             throw new WebScriptsPlatformException("Error during endpoint proxy processing: " + err.getMessage(), err);
289         }
290         
291         return mav;
292     }
293 }
294 
295