org.openymsg.network.HTTPConnectionHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openymsg.network.HTTPConnectionHandler.java

Source

/*
 * OpenYMSG, an implementation of the Yahoo Instant Messaging and Chat protocol.
 * Copyright (C) 2007 G. der Kinderen, Nimbuzz.com 
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */
package org.openymsg.network;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Queue;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 
 * @author G. der Kinderen, Nimbuzz B.V. guus@nimbuzz.com
 * @author S.E. Morris
 */
public class HTTPConnectionHandler extends ConnectionHandler {
    private static final Log log = LogFactory.getLog(HTTPConnectionHandler.class);
    private final static long IDLE_TIMEOUT = 30 * 1000;

    private static final String HTTP_HEADER_POST = "POST http://" + Util.httpHost() + "/notify HTTP/1.0"
            + NetworkConstants.END;

    private static final String HTTP_HEADER_AGENT = "User-Agent: " + NetworkConstants.USER_AGENT
            + NetworkConstants.END;

    private static final String HTTP_HEADER_HOST = "Host: " + Util.httpHost() + NetworkConstants.END;

    private static final String HTTP_HEADER_PROXY_AUTH = "Proxy-Authorization: " + Util.httpProxyAuth()
            + NetworkConstants.END;

    private Session session; // Associated session object

    private String proxyHost; // HTTP proxy host name

    private int proxyPort; // HTTP proxy post

    private long lastFetch; // Time of last packet fetch

    private final Queue<YMSG9Packet> packets; // Incoming packet queue

    private boolean connected = false; // Sending/receiving data?

    private String cookie = null; // HTTP cookie field

    private long identifier = 0; // Some kind of id, from LOGON incoming

    private Notifier notifierThread; // Send IDLE packets on timeout

    /**
     * Creates a new instance based on the HTTP proxy settings as configured in property settings.
     */
    public HTTPConnectionHandler() {
        this(Util.httpProxyHost(), Util.httpProxyPort());
    }

    /**
     * Creates a new instance, using tshe host and port as configured. This constructor may contain a collection of
     * hosts of hosts to not proxy.
     * 
     * @param proxyHost
     *            Host to use.
     * @param proxyPort
     *            Port to use.
     * @param blacklist
     *            Option set of hosts explicitly forbidden to use.
     */
    public HTTPConnectionHandler(String proxyHost, int proxyPort, Collection<String> blacklist) {
        if (proxyHost == null || proxyHost.length() == 0 || proxyPort <= 0 || proxyPort > 65535) {
            throw new IllegalArgumentException("Bad HTTP proxy properties");
        }
        this.proxyHost = proxyHost;
        this.proxyPort = proxyPort;

        packets = new LinkedList<YMSG9Packet>();
        connected = false;

        // Names are prefixed with "http." for 1.3 and after
        Properties p = System.getProperties();
        p.put(NetworkConstants.PROXY_HOST_OLD, this.proxyHost);
        p.put(NetworkConstants.PROXY_HOST, this.proxyHost);
        p.put(NetworkConstants.PROXY_PORT_OLD, this.proxyPort + "");
        p.put(NetworkConstants.PROXY_PORT, this.proxyPort + "");
        p.put(NetworkConstants.PROXY_SET, "true");

        // Expand list to item|item|... , then store
        if (blacklist != null) {
            StringBuffer sb = new StringBuffer();
            for (String host : blacklist) {
                sb.append(host).append('|');
            }
            sb.setLength(sb.length() - 1);

            System.getProperties().put(NetworkConstants.PROXY_NON, sb.toString());
        }
    }

    /**
     * Creates a new instance, using tshe host and port as configured.
     * 
     * @param proxyHost
     *            Host to use.
     * @param proxyPort
     *            Port to use.
     */
    public HTTPConnectionHandler(String proxyHost, int proxyPort) {
        this(proxyHost, proxyPort, null);
    }

    /**
     * High level method for setting proxy authorization, for users behind firewalls which require credentials to access
     * a HTTP proxy.
     */
    public static void setProxyAuthorizationProperty(String method, String username, String password)
            throws UnsupportedOperationException {
        if (method.equalsIgnoreCase("basic")) {
            String a = username + ":" + password;
            String s = "Basic " + Util.base64(a.getBytes());
            System.setProperty(PropertyConstants.HTTP_PROXY_AUTH, s);
        } else {
            throw new UnsupportedOperationException("Method " + method + " unsupported.");
        }
    }

    /**
     * Session calls this when a connection handler is installed
     */
    @Override
    void install(Session session) {
        this.session = session;
    }

    /**
     * Do nothing more than make a note that we are on/off line
     */
    @Override
    void open(boolean searchForAddress) {
        connected = true;
        synchronized (this) // In case two threads call open()
        {
            if (notifierThread == null)
                notifierThread = new Notifier("HTTP Notifier");
        }
    }

    @Override
    void close() {
        connected = false;
        synchronized (this) // In case two threads call close()
        {
            if (notifierThread != null) {
                notifierThread.quitFlag = true;
                notifierThread = null;
            }
        }
    }

    /**
     * The only time Yahoo can actually send us any packets is when we send it some. Yahoo encodes its packets in a POST
     * body - the format is the same binary representation used for direct connections (with one or two extra codes).
     * 
     * After posting a packet, the connection will receive a HTTP response who's payload consists of four bytes followed
     * by zero or more packets. The first byte of the four is a count of packets encoded in the following body.
     * 
     * Each incoming packet is transfered to a queue, where receivePacket() takes them off - thus preserving the effect
     * that input and output packets are being received independently, as with other connection handlers. As
     * readPacket() can throw an exception, these are caught and transfered onto the queue too, then rethrown by
     * receivePacket() .
     */
    @Override
    synchronized void sendPacket(PacketBodyBuffer body, ServiceType service, long status, long sessionID)
            throws IOException, IllegalStateException {
        if (!connected)
            throw new IllegalStateException("Not logged in");

        if (filterOutput(body, service))
            return;
        byte[] b = body.getBuffer();

        Socket soc = new Socket(proxyHost, proxyPort);
        PushbackInputStream pbis = new PushbackInputStream(soc.getInputStream());
        DataOutputStream dos = new DataOutputStream(soc.getOutputStream());

        // HTTP header
        dos.writeBytes(HTTP_HEADER_POST);
        dos.writeBytes("Content-length: " + (b.length + NetworkConstants.YMSG9_HEADER_SIZE) + NetworkConstants.END);
        dos.writeBytes(HTTP_HEADER_AGENT);
        dos.writeBytes(HTTP_HEADER_HOST);
        if (HTTP_HEADER_PROXY_AUTH != null)
            dos.writeBytes(HTTP_HEADER_PROXY_AUTH);
        if (cookie != null)
            dos.writeBytes("Cookie: " + cookie + NetworkConstants.END);
        dos.writeBytes(NetworkConstants.END);
        // YMSG9 header
        dos.write(NetworkConstants.MAGIC, 0, 4);
        dos.write(NetworkConstants.VERSION_HTTP, 0, 4);
        dos.writeShort(b.length & 0xffff);
        dos.writeShort(service.getValue() & 0xffff);
        dos.writeInt((int) (status & 0xffffffff));
        dos.writeInt((int) (sessionID & 0xffffffff));
        // YMSG9 body
        dos.write(b, 0, b.length);
        dos.flush();

        // HTTP response header
        String s = readLine(pbis);
        if (s == null || s.indexOf(" 200 ") < 0) // Not "HTTP/1.0 200 OK"
        {
            throw new IOException("HTTP request returned didn't return OK (200): " + s);
        }
        while (s != null && s.trim().length() > 0)
            // Read past header
            s = readLine(pbis);
        // Payload count
        byte[] code = new byte[4];
        int res = pbis.read(code, 0, 4); // Packet count (Little-Endian?)
        if (res < 4) {
            throw new IOException("Premature end of HTTP data");
        }
        int count = code[0];
        // Payload body
        YMSG9InputStream yip = new YMSG9InputStream(pbis);
        YMSG9Packet pkt;
        for (int i = 0; i < count; i++) {
            pkt = yip.readPacket();
            if (!filterInput(pkt)) {
                if (!packets.add(pkt)) {
                    throw new IllegalArgumentException("Unable to add data to the packetQueue!");
                }
            }
        }

        soc.close();

        // Reset idle timeout
        lastFetch = System.currentTimeMillis();
    }

    // Read one line of text, terminating in usual \r \n combinations
    private String readLine(PushbackInputStream pbis) throws IOException {
        int c = pbis.read();
        StringBuffer sb = new StringBuffer();
        while (c != '\n' && c != '\r') {
            sb.append((char) c);
            c = pbis.read();
        }
        // Check next character
        int c2 = pbis.read();
        if ((c == '\n' && c2 != '\r') || (c == '\r' && c2 != '\n'))
            pbis.unread(c2);
        return sb.toString();
    }

    /**
     * This method blocks until there is a packet to deliver, during which time it releases its object lock.
     */
    @Override
    YMSG9Packet receivePacket() throws IOException {
        if (!connected)
            throw new IllegalStateException("Not logged in");
        while (true) {
            synchronized (this) {
                if (packets.size() > 0) {
                    Object o = packets.poll();
                    if (o instanceof IOException)
                        throw (IOException) o;
                    return (YMSG9Packet) o;
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * ConnectionHandler methods end
     */

    /**
     * Sometimes packets need to be altered, either going in or out. Typically this is about reading or adding extra
     * HTTP specific content which Yahoo uses. These methods are called to perform the necessary manipulations.
     * 
     * Returns true if this packet should be surpressed.
     * 
     * @throws IOException
     * @throws UnsupportedEncodingException
     */
    private boolean filterOutput(PacketBodyBuffer body, ServiceType service)
            throws UnsupportedEncodingException, IOException {
        switch (service) {
        case ISBACK:
        case LOGOFF:
            // Do not send ISBACK or LOGOFF
            return true;
        default:
            break;
        }
        if (identifier > 0)
            body.addElement("24", identifier + "");
        return false;
    }

    private boolean filterInput(YMSG9Packet pkt) {
        switch (pkt.service) {
        case LIST:
            // Remember cookie and send it in subsequent packets
            String[] cookieArr = extractCookies(pkt);
            cookie = cookieArr[NetworkConstants.COOKIE_Y] + "; " + cookieArr[NetworkConstants.COOKIE_T];
            break;
        case LOGON:
            // Remember the 24 tag and send it in subsequent packets
            identifier = Long.parseLong(pkt.getValue("24"));
            break;
        case MESSAGE:
            // When sending a message we often get a 0x06 packet back, empty
            // or containing the status tag (66) of friend we messaged.
            if (pkt.getValue("14") == null) {
                if (pkt.getValue("10") != null)
                    pkt.service = ServiceType.ISBACK;
                else if (pkt.body.length == 0)
                    return true;
            }
            break;
        default:
            // do nothing
            break;
        }
        return false;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer("HTTP connection: ").append(proxyHost).append(":").append(proxyPort);
        return sb.toString();
    }

    /**
     * This thread fires off a IDLE packet every thirty seconds. This is because the only way the server can deliver us
     * any incoming packets is on the input stream of a HTTP connection we have made ourselves.
     */
    class Notifier extends Thread {
        volatile boolean quitFlag = false;

        Notifier(String nm) {
            super(nm);
            lastFetch = System.currentTimeMillis();
            this.start();
        }

        @Override
        public void run() {
            while (!quitFlag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // ignore
                }
                long t = System.currentTimeMillis();
                if (!quitFlag && connected && (t - lastFetch > IDLE_TIMEOUT)
                        && session.getSessionStatus() == SessionState.LOGGED_ON) {
                    try {
                        session.transmitIdle();
                    } catch (IOException e) {
                        log.error(e, e);
                    }
                }
            }
        }
    }
}