/* * $HeadURL$ * $Id$ * * Copyright (c) 2007-2012 by Public Library of Science * http://plos.org * http://ambraproject.org * * Licensed 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.ambraproject.action.trackback; import org.ambraproject.action.BaseActionSupport; import org.ambraproject.service.trackback.PingbackFault; import org.ambraproject.service.trackback.PingbackService; import org.ambraproject.web.VirtualJournalContext; import org.apache.struts2.ServletActionContext; import org.apache.xmlrpc.XmlRpcException; import org.apache.xmlrpc.XmlRpcHandler; import org.apache.xmlrpc.XmlRpcRequest; import org.apache.xmlrpc.server.XmlRpcHandlerMapping; import org.apache.xmlrpc.webserver.XmlRpcServletServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.List; /** * Action for incoming pingback requests. * * @author Ryan Skonnord */ public class CreatePingbackAction extends BaseActionSupport { private static final Logger log = LoggerFactory.getLogger(CreatePingbackAction.class); protected static final String CONTENT_TYPE_HEADER = "content-type"; protected static final String XML_CONTENT_TYPE = "text/xml"; /** * The standard XML-RPC method name string for sending requests in the pingback protocol. Specified by <a * href="http://www.hixie.ch/specs/pingback/pingback-1.0">Pingback 1.0</a> */ protected static final String PINGBACK_METHOD_NAME = "pingback.ping"; private PingbackService pingbackService; @Required public void setPingbackService(PingbackService pingbackService) { this.pingbackService = pingbackService; } /** * Return a string to be the return value to a successful Pingback RPC. The <a href="http://www.hixie.ch/specs/pingback/pingback-1.0">Pingback * 1.0 specification</a> requires that, if a fault code is not returned, the RPC must return a single string * "containing as much information as the server deems useful. This string is only expected to be used for debugging * purposes." * * @param sourceUri * @return the string to send as an RPC return value */ private static String makeSuccessMessage(URI sourceUri, URI targetUri) { return "Received " + CreatePingbackAction.PINGBACK_METHOD_NAME + "(\"" + sourceUri.toASCIIString() + "\", \"" + targetUri.toASCIIString() + "\")"; } private static final String FAULT_LOG_FORMAT = "XML-RPC Fault: code=%d; message=\"%s\"; RPC method=\"%s\"; RPC parameters=%s"; /** * Log the content of and response to a faulty pingback request. * <p/> * When the XML-RPC server library handles an {@link XmlRpcException}, it logs the stack trace, but otherwise the * system would not retain data about the faulty input from the client. So this method does. (This makes it an * exception to the "log or throw but not both" pattern: it logs data that would be discarded after the exception is * thrown.) * * @param fault the exception encapsulating the fault message to send as an XML-RPC response * @param request the faulty request */ private void logXmlRpcFault(XmlRpcException fault, XmlRpcRequest request) { if (log.isInfoEnabled()) { int parameterCount = request.getParameterCount(); List<String> requestParameters = new ArrayList<String>(parameterCount); for (int i = 0; i < parameterCount; i++) { requestParameters.add(String.valueOf(request.getParameter(i))); } String logMessage = String.format(FAULT_LOG_FORMAT, fault.code, fault.getMessage(), request.getMethodName(), requestParameters); log.info(logMessage); } } /** * Log the content of and response to an XML-RPC request to an unsupported method name. * * @param fault the exception encapsulating the fault message to send as an XML-RPC response * @param methodName the remote procedure name */ private void logXmlRpcFault(XmlRpcException fault, String methodName) { if (log.isInfoEnabled()) { String logMessage = String.format(FAULT_LOG_FORMAT, fault.code, fault.getMessage(), methodName, null); log.info(logMessage); } } /** * Adapter object to make {@link XmlRpcServletServer} delegate to {@link PingbackService}. */ private class PingbackHandler implements XmlRpcHandler { private final String pingbackServerHostname; private PingbackHandler(String pingbackServerHostname) { this.pingbackServerHostname = pingbackServerHostname; } @Override public Object execute(XmlRpcRequest request) throws XmlRpcException { try { return handleRequest(request); } catch (XmlRpcException fault) { logXmlRpcFault(fault, request); throw fault; } } /** * Receive an XML-RPC request and, if it's a valid pingback, store it. * * @param request the XML-RPC request * @return a response message * @throws XmlRpcException if the request is invalid or the pingback will not be stored */ private String handleRequest(XmlRpcRequest request) throws XmlRpcException { int paramCount = request.getParameterCount(); if (paramCount != 2) { throw PingbackFault.INVALID_PARAMS.getException(); } String sourceUriStr; String targetUriStr; try { sourceUriStr = (String) request.getParameter(0); targetUriStr = (String) request.getParameter(1); } catch (ClassCastException e) { throw PingbackFault.INVALID_PARAMS.getException(e); } URI sourceUri; try { sourceUri = new URI(sourceUriStr); } catch (URISyntaxException e) { throw PingbackFault.SOURCE_DNE.getException(e); } URI targetUri; try { targetUri = new URI(targetUriStr); } catch (URISyntaxException e) { throw PingbackFault.TARGET_DNE.getException(e); } pingbackService.createPingback(sourceUri, targetUri, pingbackServerHostname); return makeSuccessMessage(sourceUri, targetUri); } } /** * @return a server object that, when it executes on an XML-RPC request, will translate to pingback parameters and * delegate to {@link PingbackService} */ private XmlRpcServletServer constructServer(final String pingbackServerHostname) { XmlRpcServletServer server = new XmlRpcServletServer(); XmlRpcHandlerMapping mapping = new XmlRpcHandlerMapping() { @Override public XmlRpcHandler getHandler(String handlerName) throws XmlRpcException { if (!PINGBACK_METHOD_NAME.equals(handlerName)) { String message = "Method not found: \"" + handlerName + "\". The only supported RPC method is \"" + PINGBACK_METHOD_NAME + "\"."; XmlRpcException fault = PingbackFault.METHOD_NOT_FOUND.getException(message); logXmlRpcFault(fault, handlerName); throw fault; } return new PingbackHandler(pingbackServerHostname); } }; server.setHandlerMapping(mapping); return server; } @Override public String execute() throws IOException, ServletException { HttpServletRequest request = ServletActionContext.getRequest(); String contentType = request.getHeader(CONTENT_TYPE_HEADER); if (!XML_CONTENT_TYPE.equals(contentType)) { return ERROR; } VirtualJournalContext requestContent = (VirtualJournalContext) request.getAttribute(VirtualJournalContext.PUB_VIRTUALJOURNAL_CONTEXT); URL baseUrl = new URL(requestContent.getBaseUrl()); String pingbackServerHostname = baseUrl.getHost(); HttpServletResponse response = ServletActionContext.getResponse(); constructServer(pingbackServerHostname).execute(request, response); // The XmlRpcServletServer has already written the response; return null so Struts won't try to return null; } }