ClientSession.java 13.7 KB
Newer Older
Gaston Dombiak's avatar
Gaston Dombiak committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright (C) 2004 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.messenger;

import org.jivesoftware.messenger.auth.AuthToken;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.user.User;
import org.jivesoftware.messenger.user.UserManager;
import org.jivesoftware.messenger.user.UserNotFoundException;
19
import org.jivesoftware.messenger.net.SocketConnection;
Gaston Dombiak's avatar
Gaston Dombiak committed
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.dom4j.io.XPPPacketReader;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParser;

import java.io.Writer;
import java.io.IOException;

/**
 * Represents a session between the server and a client.
 *
 * @author Gaston Dombiak
 */
public class ClientSession extends Session {

    private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
    private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
41 42 43 44 45 46 47
    private static final String TLS_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-tls";

    /**
     * Version of the XMPP spec supported as MAJOR_VERSION.MINOR_VERSION (e.g. 1.0).
     */
    private static final int MAJOR_VERSION = 0;
    private static final int MINOR_VERSION = 0;
Gaston Dombiak's avatar
Gaston Dombiak committed
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

    /**
     * The authentication token for this session.
     */
    protected AuthToken authToken;

    /**
     * Flag indicating if this session has been initialized yet (upon first available transition).
     */
    private boolean initialized;

    private Presence presence = null;

    private int conflictCount = 0;

    /**
64 65 66
     * Returns a newly created session between the server and a client. The session will
     * be created and returned only if correct name/prefix (i.e. 'stream' or 'flash')
     * and namespace were provided by the client.
Gaston Dombiak's avatar
Gaston Dombiak committed
67 68 69 70 71 72 73
     *
     * @param serverName the name of the server where the session is connecting to.
     * @param reader the reader that is reading the provided XML through the connection.
     * @param connection the connection with the client.
     * @return a newly created session between the server and a client.
     */
    public static Session createSession(String serverName, XPPPacketReader reader,
74 75 76
            SocketConnection connection) throws XmlPullParserException, UnauthorizedException,
            IOException
    {
Gaston Dombiak's avatar
Gaston Dombiak committed
77 78 79
        XmlPullParser xpp = reader.getXPPParser();

        boolean isFlashClient = xpp.getPrefix().equals("flash");
80 81
        connection.setFlashClient(isFlashClient);

Gaston Dombiak's avatar
Gaston Dombiak committed
82 83 84
        // Conduct error checking, the opening tag should be 'stream'
        // in the 'etherx' namespace
        if (!xpp.getName().equals("stream") && !isFlashClient) {
85 86
            throw new XmlPullParserException(
                    LocaleUtils.getLocalizedString("admin.error.bad-stream"));
Gaston Dombiak's avatar
Gaston Dombiak committed
87 88 89
        }

        if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) &&
90 91 92 93
                !(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE)))
        {
            throw new XmlPullParserException(LocaleUtils.getLocalizedString(
                    "admin.error.bad-namespace"));
Gaston Dombiak's avatar
Gaston Dombiak committed
94 95
        }

96
        // Default language is English ("en").
Gaston Dombiak's avatar
Gaston Dombiak committed
97
        String language = "en";
98 99 100 101 102
        // Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
        // not report a version in which case "0.0" should be assumed (per rfc3920
        // section 4.4.1).
        int majorVersion = 0;
        int minorVersion = 0;
Gaston Dombiak's avatar
Gaston Dombiak committed
103 104 105 106
        for (int i = 0; i < xpp.getAttributeCount(); i++) {
            if ("lang".equals(xpp.getAttributeName(i))) {
                language = xpp.getAttributeValue(i);
            }
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
            if ("version".equals(xpp.getAttributeName(i))) {
                try {
                    String [] versionString = xpp.getAttributeValue(i).split("\\.");
                    majorVersion = Integer.parseInt(versionString[0]);
                    minorVersion = Integer.parseInt(versionString[1]);
                }
                catch (Exception e) {
                    Log.error(e);
                }
            }
        }

        // If the client supports a greater major version than the server,
        // set the version to the highest one the server supports.
        if (majorVersion > MAJOR_VERSION) {
            majorVersion = MAJOR_VERSION;
            minorVersion = MINOR_VERSION;
        }
        else if (majorVersion == MAJOR_VERSION) {
            // If the client supports a greater minor version than the
            // server, set the version to the highest one that the server
            // supports.
            if (minorVersion > MINOR_VERSION) {
                minorVersion = MINOR_VERSION;
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
132 133
        }

134 135 136 137 138 139 140
        // Store language and version information in the connection.
        connection.setLanaguage(language);
        connection.setXMPPVersion(majorVersion, minorVersion);

        // Create a ClientSession for this user.
        Session session = SessionManager.getInstance().createClientSession(connection);

Gaston Dombiak's avatar
Gaston Dombiak committed
141 142
        Writer writer = connection.getWriter();
        // Build the start packet response
143
        StringBuilder sb = new StringBuilder();
Gaston Dombiak's avatar
Gaston Dombiak committed
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
        sb.append("<?xml version='1.0' encoding='");
        sb.append(CHARSET);
        sb.append("'?>");
        if (isFlashClient) {
            sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
        }
        else {
            sb.append("<stream:stream ");
        }
        sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
        sb.append(serverName);
        sb.append("\" id=\"");
        sb.append(session.getStreamID().toString());
        sb.append("\" xml:lang=\"");
        sb.append(language);
159 160 161 162 163
        // Don't include version info if the version is 0.0.
        if (majorVersion != 0) {
            sb.append("\" version=\"");
            sb.append(majorVersion).append(".").append(minorVersion);
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
164 165 166
        sb.append("\">");
        writer.write(sb.toString());

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
        // If this is a "Jabber" connection, the session is now initialized and we can
        // return to allow normal packet parsing.
        if (majorVersion == 0) {
            // If this is a flash client append a special caracter to the response.
            if (isFlashClient) {
                writer.write('\0');
            }
            writer.flush();

            return session;
        }
        // Otherwise, this is at least XMPP 1.0 so we need to announce stream features.

        sb = new StringBuilder();
        sb.append("<stream:features>");
        sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
        // sb.append("<required/>");
        sb.append("</starttls></stream:features>");

        writer.write(sb.toString());

Gaston Dombiak's avatar
Gaston Dombiak committed
188 189 190 191
        if (isFlashClient) {
            writer.write('\0');
        }
        writer.flush();
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

        boolean done = false;
        while (!done) {
            if (xpp.next() == XmlPullParser.START_TAG) {
                done = true;
                if (xpp.getName().equals("starttls") &&
                        xpp.getNamespace(xpp.getPrefix()).equals(TLS_NAMESPACE))
                {
                    writer.write("<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
                    if (isFlashClient) {
                        writer.write('\0');
                    }
                    writer.flush();

                    // TODO: setup SSLEngine and negotiate TLS.
                }
            }
        }

Gaston Dombiak's avatar
Gaston Dombiak committed
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
        return session;
    }

    /**
     * Creates a session with an underlying connection and permission protection.
     *
     * @param connection The connection we are proxying
     */
    public ClientSession(String serverName, Connection connection, StreamID streamID) {
        super(serverName, connection, streamID);
        // Set an unavailable initial presence
        presence = new Presence();
        presence.setType(Presence.Type.unavailable);
    }

    /**
     * Returns the username associated with this session. Use this information
     * with the user manager to obtain the user based on username.
     *
     * @return the username associated with this session
     * @throws UserNotFoundException if a user is not associated with a session
     *      (the session has not authenticated yet)
     */
    public String getUsername() throws UserNotFoundException {
        if (authToken == null) {
            throw new UserNotFoundException();
        }
        return getAddress().getNode();
    }


    /**
     * Initialize the session with a valid authentication token and
     * resource name. This automatically upgrades the session's
     * status to authenticated and enables many features that are not
     * available until authenticated (obtaining managers for example).
     *
248 249 250
     * @param auth the authentication token obtained from the AuthFactory.
     * @param resource the resource this session authenticated under.
     * @param userManager the user manager this authentication occured under.
Gaston Dombiak's avatar
Gaston Dombiak committed
251
     */
252 253 254
    public void setAuthToken(AuthToken auth, UserManager userManager, String resource)
            throws UserNotFoundException
    {
Gaston Dombiak's avatar
Gaston Dombiak committed
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
        User user = userManager.getUser(auth.getUsername());
        setAddress(new JID(user.getUsername(), getServerName(), resource));
        authToken = auth;

        sessionManager.addSession(this);
        setStatus(Session.STATUS_AUTHENTICATED);
    }

    /**
     * <p>Initialize the session as an anonymous login.</p>
     * <p>This automatically upgrades the session's
     * status to authenticated and enables many features that are not
     * available until authenticated (obtaining managers for example).</p>
     */
    public void setAnonymousAuth() {
        sessionManager.addAnonymousSession(this);
        setStatus(Session.STATUS_AUTHENTICATED);
    }

    /**
275
     * Returns the authentication token associated with this session.
Gaston Dombiak's avatar
Gaston Dombiak committed
276
     *
277
     * @return the authentication token associated with this session (can be null).
Gaston Dombiak's avatar
Gaston Dombiak committed
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
     */
    public AuthToken getAuthToken() {
        return authToken;
    }

    /**
     * Flag indicating if this session has been initialized once coming
     * online. Session initialization occurs after the session receives
     * the first "available" presence update from the client. Initialization
     * actions include pushing offline messages, presence subscription requests,
     * and presence statuses to the client. Initialization occurs only once
     * following the first available presence transition.
     *
     * @return True if the session has already been initializsed
     */
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Sets the initialization state of the session.
     *
     * @param isInit True if the session has been initialized
     * @see #isInitialized
     */
    public void setInitialized(boolean isInit) {
        initialized = isInit;
    }

    /**
     * Obtain the presence of this session.
     *
     * @return The presence of this session or null if not authenticated
     */
    public Presence getPresence() {
        return presence;
    }

    /**
     * Set the presence of this session
     *
     * @param presence The presence for the session
     * @return The old priority of the session or null if not authenticated
     */
    public Presence setPresence(Presence presence) {
        Presence oldPresence = this.presence;
        this.presence = presence;
        if (oldPresence.isAvailable() && !this.presence.isAvailable()) {
            // The client is no longer available
            sessionManager.sessionUnavailable(this);
            // Mark that the session is no longer initialized. This means that if the user sends
            // an available presence again the session will be initialized again thus receiving
            // offline messages and offline presence subscription requests
            setInitialized(false);
        }
        else if (!oldPresence.isAvailable() && this.presence.isAvailable()) {
            // The client is available
            sessionManager.sessionAvailable(this);
        }
        else if (oldPresence.getPriority() != this.presence.getPriority()) {
            // The client has changed the priority of his presence
            sessionManager.changePriority(getAddress(), this.presence.getPriority());
        }
        return oldPresence;
    }

    /**
     * Returns the number of conflicts detected on this session.
     * Conflicts typically occur when another session authenticates properly
     * to the user account and requests to use a resource matching the one
     * in use by this session. Administrators may configure the server to automatically
     * kick off existing sessions when their conflict count exceeds some limit including
     * 0 (old sessions are kicked off immediately to accommodate new sessions). Conflicts
     * typically signify the existing (old) session is broken/hung.
     *
     * @return The number of conflicts detected for this session
     */
    public int getConflictCount() {
        return conflictCount;
    }

    /**
     * Increments the conflict by one.
     */
    public void incrementConflictCount() {
        conflictCount++;
    }

    public void process(Packet packet) {
        deliver(packet);
    }

    private void deliver(Packet packet) {
        if (conn != null && !conn.isClosed()) {
            try {
                conn.deliver(packet);
            }
            catch (Exception e) {
376
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
Gaston Dombiak's avatar
Gaston Dombiak committed
377 378 379
            }
        }
    }
380

Gaston Dombiak's avatar
Gaston Dombiak committed
381 382 383
    public String toString() {
        return super.toString() + " presence: " + presence;
    }
Gaston Dombiak's avatar
Gaston Dombiak committed
384
}