001: /*
002: * Copyright 2005 Joe Walker
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: * http://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: package org.directwebremoting.dwrp;
017:
018: import java.io.IOException;
019: import java.io.PrintWriter;
020: import java.lang.reflect.Method;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025:
026: import javax.servlet.http.Cookie;
027: import javax.servlet.http.HttpServletRequest;
028: import javax.servlet.http.HttpServletResponse;
029:
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.directwebremoting.ScriptBuffer;
033: import org.directwebremoting.WebContextFactory;
034: import org.directwebremoting.extend.AccessControl;
035: import org.directwebremoting.extend.Call;
036: import org.directwebremoting.extend.Calls;
037: import org.directwebremoting.extend.ConverterManager;
038: import org.directwebremoting.extend.Creator;
039: import org.directwebremoting.extend.CreatorManager;
040: import org.directwebremoting.extend.EnginePrivate;
041: import org.directwebremoting.extend.FormField;
042: import org.directwebremoting.extend.InboundContext;
043: import org.directwebremoting.extend.InboundVariable;
044: import org.directwebremoting.extend.MarshallException;
045: import org.directwebremoting.extend.Marshaller;
046: import org.directwebremoting.extend.PageNormalizer;
047: import org.directwebremoting.extend.RealScriptSession;
048: import org.directwebremoting.extend.RealWebContext;
049: import org.directwebremoting.extend.Replies;
050: import org.directwebremoting.extend.Reply;
051: import org.directwebremoting.extend.ScriptBufferUtil;
052: import org.directwebremoting.extend.ScriptConduit;
053: import org.directwebremoting.extend.ServerException;
054: import org.directwebremoting.extend.TypeHintContext;
055: import org.directwebremoting.io.FileTransfer;
056: import org.directwebremoting.util.DebuggingPrintWriter;
057: import org.directwebremoting.util.Messages;
058:
059: /**
060: * A Marshaller that output plain Javascript.
061: * This marshaller can be tweaked to output Javascript in an HTML context.
062: * This class works in concert with CallScriptConduit, they should be
063: * considered closely related and it is important to understand what one does
064: * while editing the other.
065: * @author Joe Walker [joe at getahead dot ltd dot uk]
066: */
067: public abstract class BaseCallMarshaller implements Marshaller {
068: /* (non-Javadoc)
069: * @see org.directwebremoting.extend.Marshaller#marshallInbound(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
070: */
071: public Calls marshallInbound(HttpServletRequest request,
072: HttpServletResponse response) throws IOException,
073: ServerException {
074: RealWebContext webContext = (RealWebContext) WebContextFactory
075: .get();
076: Batch batch = new Batch(request);
077:
078: if (!allowGetForSafariButMakeForgeryEasier && batch.isGet()) {
079: log
080: .error("GET is disallowed because it makes request forgery easier. See http://getahead.org/dwr/security/allowGetForSafariButMakeForgeryEasier for more details.");
081: throw new SecurityException("GET Disalowed");
082: }
083:
084: if (crossDomainSessionSecurity) {
085: checkNotCsrfAttack(request, batch);
086: }
087:
088: // Save the batch so marshallException can get at a batch id
089: request.setAttribute(ATTRIBUTE_BATCH, batch);
090:
091: String normalizedPage = pageNormalizer.normalizePage(batch
092: .getPage());
093: webContext.checkPageInformation(normalizedPage, batch
094: .getScriptSessionId(), batch.getWindowName());
095:
096: // Various bits of the Batch need to be stashed away places
097: storeParsedRequest(request, webContext, batch);
098: return marshallInbound(batch);
099: }
100:
101: /**
102: * Check that this request is not subject to a CSRF attack
103: * @param request The original browser's request
104: * @param batch The data that we've parsed from the request body
105: */
106: private void checkNotCsrfAttack(HttpServletRequest request,
107: Batch batch) {
108: // A check to see that this isn't a csrf attack
109: // http://en.wikipedia.org/wiki/Cross-site_request_forgery
110: // http://www.tux.org/~peterw/csrf.txt
111: if (request.isRequestedSessionIdValid()
112: && request.isRequestedSessionIdFromCookie()) {
113: String headerSessionId = request.getRequestedSessionId();
114: if (headerSessionId.length() > 0) {
115: String bodySessionId = batch.getHttpSessionId();
116:
117: // Normal case; if same session cookie is supplied by DWR and
118: // in HTTP header then all is ok
119: if (headerSessionId.equals(bodySessionId)) {
120: return;
121: }
122:
123: // Weblogic adds creation time to the end of the incoming
124: // session cookie string (even for request.getRequestedSessionId()).
125: // Use the raw cookie instead
126: for (Cookie cookie : request.getCookies()) {
127: if (cookie.getName().equals(sessionCookieName)
128: && cookie.getValue().equals(bodySessionId)) {
129: return;
130: }
131: }
132:
133: // Otherwise error
134: log
135: .error("A request has been denied as a potential CSRF attack.");
136: throw new SecurityException("CSRF Security Error");
137: }
138: }
139: }
140:
141: /**
142: * Convert batch into calls.
143: * @param batch The data we've parsed from the request
144: * @return The function calls to make
145: */
146: @SuppressWarnings({"ThrowableInstanceNeverThrown"})
147: public Calls marshallInbound(Batch batch) {
148: Calls calls = batch.getCalls();
149:
150: // Debug the environment
151: if (log.isDebugEnabled() && calls.getCallCount() > 0) {
152: // We can just use 0 because they are all shared
153: InboundContext inctx = batch.getInboundContexts().get(0);
154: StringBuffer buffer = new StringBuffer();
155:
156: for (Iterator<String> it = inctx.getInboundVariableNames(); it
157: .hasNext();) {
158: String key = it.next();
159: InboundVariable value = inctx.getInboundVariable(key);
160: if (key
161: .startsWith(ProtocolConstants.INBOUND_CALLNUM_PREFIX)
162: && key
163: .contains(ProtocolConstants.INBOUND_CALLNUM_SUFFIX
164: + ProtocolConstants.INBOUND_KEY_ENV)) {
165: buffer.append(key);
166: buffer.append('=');
167: buffer.append(value.toString());
168: buffer.append(", ");
169: }
170: }
171:
172: if (buffer.length() > 0) {
173: log.debug("Environment: " + buffer.toString());
174: }
175: }
176:
177: callLoop: for (int callNum = 0; callNum < calls.getCallCount(); callNum++) {
178: Call call = calls.getCall(callNum);
179: InboundContext inctx = batch.getInboundContexts().get(
180: callNum);
181:
182: // Get a list of the available matching methods with the coerced
183: // parameters that we will use to call it if we choose to use
184: // that method.
185: Creator creator = creatorManager.getCreator(call
186: .getScriptName());
187:
188: // Which method are we using?
189: Method method = findMethod(call, inctx);
190: if (method == null) {
191: String name = call.getScriptName() + '.'
192: + call.getMethodName();
193: String error = Messages.getString(
194: "BaseCallMarshaller.UnknownMethod", name);
195: log.warn("Marshalling exception: " + error);
196:
197: call.setMethod(null);
198: call.setParameters(null);
199: call.setException(new IllegalArgumentException(error));
200:
201: continue callLoop;
202: }
203:
204: call.setMethod(method);
205:
206: // Check this method is accessible
207: accessControl.assertExecutionIsPossible(creator, call
208: .getScriptName(), method);
209:
210: // We are now sure we have the set of input lined up. They may
211: // cross-reference so we do the de-referencing all in one go.
212: try {
213: inctx.dereference();
214: } catch (MarshallException ex) {
215: log.warn("Marshalling exception", ex);
216:
217: call.setMethod(null);
218: call.setParameters(null);
219: call.setException(ex);
220:
221: continue callLoop;
222: }
223:
224: // Convert all the parameters to the correct types
225: Object[] params = new Object[method.getParameterTypes().length];
226: for (int j = 0; j < method.getParameterTypes().length; j++) {
227: try {
228: Class<?> paramType = method.getParameterTypes()[j];
229: InboundVariable param = inctx.getParameter(callNum,
230: j);
231: TypeHintContext incc = new TypeHintContext(
232: converterManager, method, j);
233: params[j] = converterManager.convertInbound(
234: paramType, param, inctx, incc);
235: } catch (MarshallException ex) {
236: log.warn("Marshalling exception", ex);
237:
238: call.setMethod(null);
239: call.setParameters(null);
240: call.setException(ex);
241:
242: continue callLoop;
243: }
244: }
245:
246: call.setParameters(params);
247: }
248:
249: return calls;
250: }
251:
252: /**
253: * Build a Batch and put it in the request
254: * @param request Where we store the parsed data
255: * @param webContext We need to notify others of some of the data we find
256: * @param batch The parsed data to store
257: */
258: private void storeParsedRequest(HttpServletRequest request,
259: RealWebContext webContext, Batch batch) {
260: // Remaining parameters get put into the request for later consumption
261: Map<String, FormField> paramMap = batch.getSpareParameters();
262: if (!paramMap.isEmpty()) {
263: for (Map.Entry<String, FormField> entry : paramMap
264: .entrySet()) {
265: String key = entry.getKey();
266: FormField formField = entry.getValue();
267: Object value;
268:
269: if (formField.isFile()) {
270: value = new FileTransfer(formField.getName(),
271: formField.getMimeType(), formField
272: .getInputStream());
273: } else {
274: value = formField.getString();
275: }
276:
277: request.setAttribute(key, value);
278: log.debug("Moved param to request: " + key + "="
279: + value);
280: }
281: }
282: }
283:
284: /**
285: * Find the method the best matches the method name and parameters
286: * @param call The function call we are going to make
287: * @param inctx The data conversion context
288: * @return A matching method, or null if one was not found.
289: */
290: private Method findMethod(Call call, InboundContext inctx) {
291: if (call.getScriptName() == null) {
292: throw new IllegalArgumentException(Messages
293: .getString("BaseCallMarshaller.MissingClassParam"));
294: }
295:
296: if (call.getMethodName() == null) {
297: throw new IllegalArgumentException(Messages
298: .getString("BaseCallMarshaller.MissingMethodParam"));
299: }
300:
301: Creator creator = creatorManager.getCreator(call
302: .getScriptName());
303: List<Method> available = new ArrayList<Method>();
304:
305: methods: for (Method method : creator.getType().getMethods()) {
306: // Check method name and access
307: if (method.getName().equals(call.getMethodName())) {
308: // Check number of parameters
309: if (method.getParameterTypes().length == inctx
310: .getParameterCount()) {
311: // Clear the previous conversion attempts (the param types
312: // will probably be different)
313: inctx.clearConverted();
314:
315: // Check parameter types
316: for (int j = 0; j < method.getParameterTypes().length; j++) {
317: Class<?> paramType = method.getParameterTypes()[j];
318: if (!converterManager.isConvertable(paramType)) {
319: // Give up with this method and try the next
320: continue methods;
321: }
322: }
323:
324: available.add(method);
325: }
326: }
327: }
328:
329: // Pick a method to call
330: if (available.size() > 1) {
331: log
332: .warn("Warning multiple matching methods. Using first match.");
333: }
334:
335: if (available.isEmpty()) {
336: return null;
337: }
338:
339: // At the moment we are just going to take the first match, for a
340: // later increment we might pick the best implementation
341: return available.get(0);
342: }
343:
344: /* (non-Javadoc)
345: * @see org.directwebremoting.Marshaller#marshallOutbound(org.directwebremoting.Replies, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
346: */
347: public void marshallOutbound(Replies replies,
348: HttpServletRequest request, HttpServletResponse response)
349: throws IOException {
350: // Get the output stream and setup the mime type
351: response.setContentType(getOutboundMimeType());
352: PrintWriter out;
353: if (debugScriptOutput && log.isDebugEnabled()) {
354: // This might be considered evil - altering the program flow
355: // depending on the log status, however DebuggingPrintWriter is
356: // very thin and only about logging
357: out = new DebuggingPrintWriter("", response.getWriter());
358: } else {
359: out = response.getWriter();
360: }
361:
362: // The conduit to pass on reverse ajax scripts
363: ScriptConduit conduit = new CallScriptConduit(out);
364:
365: // Setup a debugging prefix
366: if (out instanceof DebuggingPrintWriter) {
367: DebuggingPrintWriter dpw = (DebuggingPrintWriter) out;
368: dpw.setPrefix("out(" + conduit.hashCode() + "): ");
369: }
370:
371: // Send the script prefix (if any)
372: sendOutboundScriptPrefix(out, replies.getBatchId());
373:
374: // From the call to addScriptConduit() there could be 2 threads writing
375: // to 'out' so we synchronize on 'out' to make sure there are no
376: // clashes
377: RealScriptSession scriptSession = (RealScriptSession) WebContextFactory
378: .get().getScriptSession();
379:
380: out.println(ProtocolConstants.SCRIPT_CALL_INSERT);
381: scriptSession.writeScripts(conduit);
382: out.println(ProtocolConstants.SCRIPT_CALL_REPLY);
383:
384: String batchId = replies.getBatchId();
385: for (int i = 0; i < replies.getReplyCount(); i++) {
386: Reply reply = replies.getReply(i);
387: String callId = reply.getCallId();
388:
389: try {
390: // The existence of a throwable indicates that something went wrong
391: if (reply.getThrowable() != null) {
392: Throwable ex = reply.getThrowable();
393: EnginePrivate.remoteHandleException(conduit,
394: batchId, callId, ex);
395:
396: log.warn("--Erroring: batchId[" + batchId
397: + "] message[" + ex.toString() + ']');
398: } else {
399: Object data = reply.getReply();
400: EnginePrivate.remoteHandleCallback(conduit,
401: batchId, callId, data);
402: }
403: } catch (IOException ex) {
404: // We're a bit stuck we died half way through writing so
405: // we can't be sure the browser can react to the failure.
406: // Since we can no longer do output we just log and end
407: log.error("--Output Error: batchId[" + batchId
408: + "] message[" + ex.toString() + ']', ex);
409: } catch (MarshallException ex) {
410: EnginePrivate.remoteHandleException(conduit, batchId,
411: callId, ex);
412: log.warn("--MarshallException: batchId=" + batchId
413: + " class=" + ex.getConversionType().getName(),
414: ex);
415: } catch (Exception ex) {
416: // This is a bit of a "this can't happen" case so I am a bit
417: // nervous about sending the exception to the client, but we
418: // want to avoid silently dying so we need to do something.
419: EnginePrivate.remoteHandleException(conduit, batchId,
420: callId, ex);
421: log.error("--MarshallException: batchId=" + batchId
422: + " message=" + ex.toString());
423: }
424: }
425:
426: sendOutboundScriptSuffix(out, replies.getBatchId());
427: }
428:
429: /* (non-Javadoc)
430: * @see org.directwebremoting.extend.Marshaller#marshallException(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Exception)
431: */
432: public void marshallException(HttpServletRequest request,
433: HttpServletResponse response, Exception ex)
434: throws IOException {
435: response.setContentType(getOutboundMimeType());
436: PrintWriter out = response.getWriter();
437: Batch batch = (Batch) request.getAttribute(ATTRIBUTE_BATCH);
438:
439: String batchId;
440: if (batch != null && batch.getCalls() != null) {
441: batchId = batch.getCalls().getBatchId();
442: } else {
443: batchId = null;
444: }
445:
446: sendOutboundScriptPrefix(out, batchId);
447: String script = EnginePrivate
448: .getRemoteHandleBatchExceptionScript(batchId, ex);
449: out.print(script);
450: sendOutboundScriptSuffix(out, batchId);
451: }
452:
453: /**
454: * Send a script to the browser
455: * @param out The stream to write to
456: * @param script The script to send
457: * @throws IOException If the write fails
458: */
459: protected abstract void sendScript(PrintWriter out, String script)
460: throws IOException;
461:
462: /**
463: * What mime type should we send to the browser for this data?
464: * @return A mime-type
465: */
466: protected abstract String getOutboundMimeType();
467:
468: /**
469: * iframe mode starts as HTML, so get into script mode
470: * @param out The stream to write to
471: * @param batchId The batch identifier so we can prepare the environment
472: * @throws IOException If the write fails
473: */
474: protected abstract void sendOutboundScriptPrefix(PrintWriter out,
475: String batchId) throws IOException;
476:
477: /**
478: * iframe mode needs to get out of script mode
479: * @param out The stream to write to
480: * @param batchId The batch identifier so we can prepare the environment
481: * @throws IOException If the write fails
482: */
483: protected abstract void sendOutboundScriptSuffix(PrintWriter out,
484: String batchId) throws IOException;
485:
486: /* (non-Javadoc)
487: * @see org.directwebremoting.Marshaller#isConvertable(java.lang.Class)
488: */
489: public boolean isConvertable(Class<?> paramType) {
490: return converterManager.isConvertable(paramType);
491: }
492:
493: /**
494: * Accessor for the DefaultCreatorManager that we configure
495: * @param converterManager The new DefaultConverterManager
496: */
497: public void setConverterManager(ConverterManager converterManager) {
498: this .converterManager = converterManager;
499: }
500:
501: /**
502: * Accessor for the DefaultCreatorManager that we configure
503: * @param creatorManager The new DefaultConverterManager
504: */
505: public void setCreatorManager(CreatorManager creatorManager) {
506: this .creatorManager = creatorManager;
507: }
508:
509: /**
510: * Accessor for the security manager
511: * @param accessControl The accessControl to set.
512: */
513: public void setAccessControl(AccessControl accessControl) {
514: this .accessControl = accessControl;
515: }
516:
517: /**
518: * Accessor for the PageNormalizer.
519: * @param pageNormalizer The new PageNormalizer
520: */
521: public void setPageNormalizer(PageNormalizer pageNormalizer) {
522: this .pageNormalizer = pageNormalizer;
523: }
524:
525: /**
526: * To we perform cross-domain session security checks?
527: * @param crossDomainSessionSecurity the cross domain session security setting
528: */
529: public void setCrossDomainSessionSecurity(
530: boolean crossDomainSessionSecurity) {
531: this .crossDomainSessionSecurity = crossDomainSessionSecurity;
532: }
533:
534: /**
535: * @param allowGetForSafariButMakeForgeryEasier Do we reduce security to help Safari
536: */
537: public void setAllowGetForSafariButMakeForgeryEasier(
538: boolean allowGetForSafariButMakeForgeryEasier) {
539: this .allowGetForSafariButMakeForgeryEasier = allowGetForSafariButMakeForgeryEasier;
540: }
541:
542: /**
543: * Alter the session cookie name from the default JSESSIONID.
544: * @param sessionCookieName the sessionCookieName to set
545: */
546: public void setSessionCookieName(String sessionCookieName) {
547: this .sessionCookieName = sessionCookieName;
548: }
549:
550: /**
551: * @return Are we outputting in JSON mode?
552: */
553: public boolean isJsonOutput() {
554: return jsonOutput;
555: }
556:
557: /**
558: * @param jsonOutput Are we outputting in JSON mode?
559: */
560: public void setJsonOutput(boolean jsonOutput) {
561: this .jsonOutput = jsonOutput;
562: }
563:
564: /**
565: * Do we debug all the scripts that we output?
566: * @param debugScriptOutput true to debug all of the output scripts (verbose)
567: */
568: public void setDebugScriptOutput(boolean debugScriptOutput) {
569: this .debugScriptOutput = debugScriptOutput;
570: }
571:
572: /**
573: * A ScriptConduit that works with the parent Marshaller.
574: * In some ways this is nasty because it has access to essentially private parts
575: * of BaseCallMarshaller, however there is nowhere sensible to store them
576: * within that class, so this is a hacky simplification.
577: * @author Joe Walker [joe at getahead dot ltd dot uk]
578: */
579: protected class CallScriptConduit extends ScriptConduit {
580: /**
581: * Simple ctor
582: * @param out The stream to write to
583: */
584: protected CallScriptConduit(PrintWriter out) {
585: super (RANK_FAST);
586: if (out == null) {
587: throw new NullPointerException("out=null");
588: }
589:
590: this .out = out;
591: }
592:
593: /* (non-Javadoc)
594: * @see org.directwebremoting.ScriptConduit#addScript(org.directwebremoting.ScriptBuffer)
595: */
596: @Override
597: public boolean addScript(ScriptBuffer script)
598: throws IOException, MarshallException {
599: sendScript(out, ScriptBufferUtil.createOutput(script,
600: converterManager, jsonOutput));
601: return true;
602: }
603:
604: /**
605: * The PrintWriter to send output to, and that we should synchronize against
606: */
607: private final PrintWriter out;
608: }
609:
610: /**
611: * Do we debug all the scripts that we output?
612: */
613: protected boolean debugScriptOutput = false;
614:
615: /**
616: * Are we outputting in JSON mode?
617: */
618: protected boolean jsonOutput = false;
619:
620: /**
621: * The session cookie name
622: */
623: protected String sessionCookieName = "JSESSIONID";
624:
625: /**
626: * By default we disable GET, but this hinders old Safaris
627: */
628: private boolean allowGetForSafariButMakeForgeryEasier = false;
629:
630: /**
631: * To we perform cross-domain session security checks?
632: */
633: protected boolean crossDomainSessionSecurity = true;
634:
635: /**
636: * How we turn pages into the canonical form.
637: */
638: protected PageNormalizer pageNormalizer = null;
639:
640: /**
641: * How we convert parameters
642: */
643: protected ConverterManager converterManager = null;
644:
645: /**
646: * How we create new beans
647: */
648: protected CreatorManager creatorManager = null;
649:
650: /**
651: * The security manager
652: */
653: protected AccessControl accessControl = null;
654:
655: /**
656: * How we stash away the results of the request parse
657: */
658: protected static final String ATTRIBUTE_BATCH = "org.directwebremoting.dwrp.batch";
659:
660: /**
661: * The log stream
662: */
663: protected static final Log log = LogFactory
664: .getLog(BaseCallMarshaller.class);
665: }
|