/** * Copyright 2010 CosmoCode GmbH * * 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 de.cosmocode.palava.ipc.xml.rpc; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import java.util.Set; import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.lang.StringUtils; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandler.Sharable; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.handler.codec.http.Cookie; import org.jboss.netty.handler.codec.http.CookieDecoder; import org.jboss.netty.handler.codec.http.CookieEncoder; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpHeaders.Names; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.inject.Inject; import com.google.inject.name.Named; import de.cosmocode.palava.ipc.IpcSession; import de.cosmocode.palava.ipc.IpcSessionProvider; import de.cosmocode.palava.ipc.netty.ConnectionManager; import de.cosmocode.palava.ipc.protocol.DetachedConnection; /** * A {@link ChannelHandler} which decodes {@link HttpRequest}s * into {@link ChannelBuffer}s and encodes {@link ChannelBuffer}s into * {@link HttpResponse}s. * * @since 1.0 * @author Willi Schoenborn */ @Sharable @ThreadSafe final class HttpHandler extends SimpleChannelHandler { private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class); private final IpcSessionProvider provider; private final ConnectionManager manager; private String cookieName = "psessid"; @Inject public HttpHandler(IpcSessionProvider provider, ConnectionManager manager) { this.provider = Preconditions.checkNotNull(provider, "Provider"); this.manager = Preconditions.checkNotNull(manager, "Manager"); } @Inject(optional = true) public void setCookieName(@Named(XmlRpc.COOKIE_NAME) String cookieName) { this.cookieName = Preconditions.checkNotNull(cookieName, "CookieName"); } @Override public void messageReceived(ChannelHandlerContext context, MessageEvent event) throws Exception { final Object message = event.getMessage(); if (message instanceof HttpRequest) { final HttpRequest request = HttpRequest.class.cast(message); final SocketAddress remoteAddress = event.getRemoteAddress(); final Set<Cookie> cookies = getCookies(request); final String sessionId = findSessionId(cookies); final String identifier; if (remoteAddress instanceof InetSocketAddress) { identifier = InetSocketAddress.class.cast(remoteAddress).getHostName(); } else { identifier = null; } final IpcSession session = provider.getSession(sessionId, identifier); final DetachedConnection connection = manager.get(event.getChannel()); connection.attachTo(session); context.setAttachment(new Attachment(request, cookies)); LOG.trace("Decoding {} into channel buffer", request); Channels.fireMessageReceived(context, request.getContent(), remoteAddress); } else { context.sendUpstream(event); } } private Set<Cookie> getCookies(HttpRequest request) { final CookieDecoder decoder = new CookieDecoder(); final String cookieString = request.getHeader(Names.COOKIE); if (StringUtils.isBlank(cookieString)) return Collections.emptySet(); return decoder.decode(cookieString); } private String findSessionId(Set<Cookie> cookies) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { LOG.trace("Found sessionId in cookie: {}", cookie.getValue()); return cookie.getValue(); } } return null; } @Override public void writeRequested(ChannelHandlerContext context, MessageEvent event) throws Exception { final Object message = event.getMessage(); if (message instanceof ChannelBuffer) { final Attachment attachment = Attachment.class.cast(context.getAttachment()); Preconditions.checkState(attachment != null, "No attachment set"); final ChannelBuffer content = ChannelBuffer.class.cast(message); LOG.trace("Encoding {} into http response", content); final HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); // setting all requires headers response.setHeader(Names.CONTENT_TYPE, "text/xml"); response.setHeader(Names.CONTENT_LENGTH, content.readableBytes()); final DetachedConnection connection = manager.get(event.getChannel()); final IpcSession session = connection.getSession(); final CookieEncoder cookieEncoder = new CookieEncoder(true); for (Cookie cookie : attachment.getCookies()) { cookieEncoder.addCookie(cookie); } final String sessionId = session.getSessionId(); LOG.trace("Adding session cookie {}/{}", cookieName, sessionId); cookieEncoder.addCookie(cookieName, sessionId); response.setHeader(Names.SET_COOKIE, cookieEncoder.encode()); response.setContent(content); final ChannelFuture future = event.getFuture(); Channels.write(context, future, response, event.getRemoteAddress()); if (HttpHeaders.isKeepAlive(attachment.getRequest())) { LOG.trace("Http request was marked keep-alive, not closing the connection."); } else { LOG.trace("Http request was not marked as keep-alive, closing connection..."); future.addListener(ChannelFutureListener.CLOSE); } } else { context.sendDownstream(event); } } /** * Internal attachment which can be used to pass an {@link HttpRequest} and * a set of {@link Cookie}s as an attachment using {@link ChannelHandlerContext#setAttachment(Object)}. * * @since 1.0 * @author Willi Schoenborn */ private static final class Attachment { private final HttpRequest request; private final Set<Cookie> cookies; public Attachment(HttpRequest request, Set<Cookie> cookies) { this.request = request; this.cookies = cookies; } public HttpRequest getRequest() { return request; } public Set<Cookie> getCookies() { return cookies; } } }