/** * Copyright 2012 Nikita Koksharov * * 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 com.corundumstudio.socketio.handler; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.io.IOException; import java.net.InetSocketAddress; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.Disconnectable; import com.corundumstudio.socketio.DisconnectableHub; import com.corundumstudio.socketio.HandshakeData; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.Transport; import com.corundumstudio.socketio.ack.AckManager; import com.corundumstudio.socketio.messages.HttpErrorMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; import com.corundumstudio.socketio.protocol.AuthPacket; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; import com.corundumstudio.socketio.scheduler.CancelableScheduler; import com.corundumstudio.socketio.scheduler.SchedulerKey; import com.corundumstudio.socketio.scheduler.SchedulerKey.Type; import com.corundumstudio.socketio.store.StoreFactory; import com.corundumstudio.socketio.store.pubsub.ConnectMessage; import com.corundumstudio.socketio.store.pubsub.PubSubType; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; @Sharable public class AuthorizeHandler extends ChannelInboundHandlerAdapter implements Disconnectable { private static final Logger log = LoggerFactory.getLogger(AuthorizeHandler.class); private final CancelableScheduler disconnectScheduler; private final String connectPath; private final Configuration configuration; private final NamespacesHub namespacesHub; private final StoreFactory storeFactory; private final DisconnectableHub disconnectable; private final AckManager ackManager; private final ClientsBox clientsBox; public AuthorizeHandler(String connectPath, CancelableScheduler scheduler, Configuration configuration, NamespacesHub namespacesHub, StoreFactory storeFactory, DisconnectableHub disconnectable, AckManager ackManager, ClientsBox clientsBox) { super(); this.connectPath = connectPath; this.configuration = configuration; this.disconnectScheduler = scheduler; this.namespacesHub = namespacesHub; this.storeFactory = storeFactory; this.disconnectable = disconnectable; this.ackManager = ackManager; this.clientsBox = clientsBox; } @Override public void channelActive(final ChannelHandlerContext ctx) throws Exception { SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, ctx.channel()); disconnectScheduler.schedule(key, new Runnable() { @Override public void run() { ctx.channel().close(); log.debug("Client with ip {} opened channel but doesn't send any data! Channel closed!", ctx.channel().remoteAddress()); } }, configuration.getFirstDataTimeout(), TimeUnit.MILLISECONDS); super.channelActive(ctx); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, ctx.channel()); disconnectScheduler.cancel(key); if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; Channel channel = ctx.channel(); QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); if (!configuration.isAllowCustomRequests() && !queryDecoder.path().startsWith(connectPath)) { HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST); channel.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); req.release(); return; } List<String> sid = queryDecoder.parameters().get("sid"); if (queryDecoder.path().equals(connectPath) && sid == null) { String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (!authorize(ctx, channel, origin, queryDecoder.parameters(), req)) { req.release(); return; } // forward message to polling or websocket handler to bind channel } } ctx.fireChannelRead(msg); } private boolean authorize(ChannelHandlerContext ctx, Channel channel, String origin, Map<String, List<String>> params, FullHttpRequest req) throws IOException { Map<String, List<String>> headers = new HashMap<String, List<String>>(req.headers().names().size()); for (String name : req.headers().names()) { List<String> values = req.headers().getAll(name); headers.put(name, values); } HandshakeData data = new HandshakeData(req.headers(), params, (InetSocketAddress)channel.remoteAddress(), req.uri(), origin != null && !origin.equalsIgnoreCase("null")); boolean result = false; try { result = configuration.getAuthorizationListener().isAuthorized(data); } catch (Exception e) { log.error("Authorization error", e); } if (!result) { HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.UNAUTHORIZED); channel.writeAndFlush(res) .addListener(ChannelFutureListener.CLOSE); log.debug("Handshake unauthorized, query params: {} headers: {}", params, headers); return false; } UUID sessionId = this.generateOrGetSessionIdFromRequest(req.headers()); List<String> transportValue = params.get("transport"); if (transportValue == null) { log.error("Got no transports for request {}", req.uri()); HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.UNAUTHORIZED); channel.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); return false; } Transport transport = Transport.byName(transportValue.get(0)); if (!configuration.getTransports().contains(transport)) { Map<String, Object> errorData = new HashMap<String, Object>(); errorData.put("code", 0); errorData.put("message", "Transport unknown"); channel.attr(EncoderHandler.ORIGIN).set(origin); channel.writeAndFlush(new HttpErrorMessage(errorData)); return false; } ClientHead client = new ClientHead(sessionId, ackManager, disconnectable, storeFactory, data, clientsBox, transport, disconnectScheduler, configuration); channel.attr(ClientHead.CLIENT).set(client); clientsBox.addClient(client); String[] transports = {}; if (configuration.getTransports().contains(Transport.WEBSOCKET)) { transports = new String[] {"websocket"}; } AuthPacket authPacket = new AuthPacket(sessionId, transports, configuration.getPingInterval(), configuration.getPingTimeout()); Packet packet = new Packet(PacketType.OPEN); packet.setData(authPacket); client.send(packet); client.schedulePingTimeout(); log.debug("Handshake authorized for sessionId: {}, query params: {} headers: {}", sessionId, params, headers); return true; } /** This method will either generate a new random sessionId or will retrieve the value stored in the "io" cookie. Failures to parse will cause a logging warning to be generated and a random uuid to be generated instead (same as not passing a cookie in the first place). */ private UUID generateOrGetSessionIdFromRequest(HttpHeaders headers) { List<String> values = headers.getAll("io"); if (values.size() == 1) { try { return UUID.fromString(values.get(0)); } catch ( IllegalArgumentException iaex ) { log.warn("Malformed UUID received for session! io=" + values.get(0)); } } return UUID.randomUUID(); } public void connect(UUID sessionId) { SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, sessionId); disconnectScheduler.cancel(key); } public void connect(ClientHead client) { Namespace ns = namespacesHub.get(Namespace.DEFAULT_NAME); if (!client.getNamespaces().contains(ns)) { Packet packet = new Packet(PacketType.MESSAGE); packet.setSubType(PacketType.CONNECT); client.send(packet); configuration.getStoreFactory().pubSubStore().publish(PubSubType.CONNECT, new ConnectMessage(client.getSessionId())); SocketIOClient nsClient = client.addNamespaceClient(ns); ns.onConnect(nsClient); } } @Override public void onDisconnect(ClientHead client) { clientsBox.removeClient(client.getSessionId()); } }