All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mobicents.media.io.ice.IceHandler Maven / Gradle / Ivy

There is a newer version: 6.0.23
Show newest version
/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2015, Telestax Inc and individual contributors
 * by the @authors tag. 
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.mobicents.media.io.ice;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Logger;
import org.mobicents.media.io.ice.events.IceEventListener;
import org.mobicents.media.io.ice.events.SelectedCandidatesEvent;
import org.mobicents.media.io.stun.StunException;
import org.mobicents.media.io.stun.messages.StunMessage;
import org.mobicents.media.io.stun.messages.StunMessageFactory;
import org.mobicents.media.io.stun.messages.StunRequest;
import org.mobicents.media.io.stun.messages.StunResponse;
import org.mobicents.media.io.stun.messages.attributes.StunAttribute;
import org.mobicents.media.io.stun.messages.attributes.StunAttributeFactory;
import org.mobicents.media.io.stun.messages.attributes.general.MessageIntegrityAttribute;
import org.mobicents.media.io.stun.messages.attributes.general.UsernameAttribute;
import org.mobicents.media.server.io.network.TransportAddress;
import org.mobicents.media.server.io.network.TransportAddress.TransportProtocol;
import org.mobicents.media.server.io.network.channel.PacketHandler;
import org.mobicents.media.server.io.network.channel.PacketHandlerException;

/**
 * @author Henrique Rosa ([email protected])
 *
 */
public class IceHandler implements PacketHandler {
    
    private static final Logger logger = Logger.getLogger(IceHandler.class);

    // Packet Handler properties
    private int pipelinePriority;

    // Candidate properties
    private final short componentId;

    // Message Integrity
    private IceAuthenticator authenticator;

    // Handshake state
    private final IceEventListener iceListener;
    private final AtomicBoolean candidateSelected;

    public IceHandler(short componentId, IceEventListener iceListener) {
        // Packet Handler properties
        this.pipelinePriority = 1;

        // Candidate properties
        switch (componentId) {
            case IceComponent.RTP_ID:
            case IceComponent.RTCP_ID:
                this.componentId = componentId;
                break;

            default:
                throw new IllegalArgumentException("Invalid component ID: " + componentId);
        }

        // Handshake state
        this.iceListener = iceListener;
        this.candidateSelected = new AtomicBoolean(false);
    }

    public short getComponentId() {
        return componentId;
    }
    
    public void setAuthenticator(IceAuthenticator authenticator) {
        this.authenticator = authenticator;
    }

    @Override
    public int compareTo(PacketHandler o) {
        return (o == null) ? 1 : this.getPipelinePriority() - o.getPipelinePriority();
    }

    /*
     * All STUN messages MUST start with a 20-byte header followed by zero or more Attributes.
     * The STUN header contains a STUN message type, magic cookie, transaction ID, and message length.
     * 
     *  0                   1                   2                   3
     *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |0 0|     STUN Message Type     |         Message Length        |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |                         Magic Cookie                          |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |                                                               |
     * |                     Transaction ID (96 bits)                  |
     * |                                                               |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * 
     * @see RFC5389
     */
    @Override
    public boolean canHandle(byte[] packet) {
        return canHandle(packet, packet.length, 0);
    }

    @Override
    public boolean canHandle(byte[] packet, int dataLength, int offset) {
        byte b0 = packet[offset];
        int b0Int = b0 & 0xff;

        // STUN message must start with 20-byte header followed by zero or more attributes
        // First byte must be less than 2 (https://tools.ietf.org/html/rfc5764#section-5.1.2)
        if (b0Int < 2 && dataLength >= 20) {
            // The most significant 2 bits of every STUN message MUST be zeroes.
            boolean firstBitsValid = ((b0 & 0xC0) == 0);

            // The magic cookie field MUST contain the fixed value 0x2112A442 in network byte order.
            boolean hasMagicCookie = packet[offset + 4] == StunMessage.MAGIC_COOKIE[0]
                    && packet[offset + 5] == StunMessage.MAGIC_COOKIE[1] && packet[offset + 6] == StunMessage.MAGIC_COOKIE[2]
                    && packet[offset + 7] == StunMessage.MAGIC_COOKIE[3];
            return firstBitsValid && hasMagicCookie;
        }
        return false;
    }

    @Override
    public byte[] handle(byte[] packet, InetSocketAddress localPeer, InetSocketAddress remotePeer)
            throws PacketHandlerException {
        return handle(packet, packet.length, 0, localPeer, remotePeer);
    }

    @Override
    public byte[] handle(byte[] packet, int dataLength, int offset, InetSocketAddress localPeer, InetSocketAddress remotePeer)
            throws PacketHandlerException {
        try {
            StunMessage message = StunMessage.decode(packet, (char) offset, (char) dataLength);
            if (message instanceof StunRequest) {
                return processRequest((StunRequest) message, localPeer, remotePeer);
            } else if (message instanceof StunResponse) {
                return processResponse((StunResponse) message);
            } else {
                // TODO STUN Indication is not supported as of yet
                return null;
            }
        } catch (StunException e) {
            throw new PacketHandlerException("Could not decode STUN packet", e);
        } catch (IOException e) {
            throw new PacketHandlerException(e.getMessage(), e);
        }
    }

    private byte[] processRequest(StunRequest request, InetSocketAddress localPeer, InetSocketAddress remotePeer)
            throws IOException {
        // The agent MUST use a short-term credential to authenticate the request and perform a message integrity check.
        UsernameAttribute remoteUnameAttribute = (UsernameAttribute) request.getAttribute(StunAttribute.USERNAME);
        String remoteUsername = new String(remoteUnameAttribute.getUsername());

        // The agent MUST consider the username to be valid if it consists of two values separated by a colon, where the first
        // value is equal to the username fragment generated by the agent in an offer or answer for a session in-progress.
        if (!this.authenticator.validateUsername(remoteUsername)) {
            // TODO return error response
            throw new IOException("Invalid username " + remoteUsername);
        }

        // The username for the credential is formed by concatenating the username fragment provided by the peer with the
        // username fragment of the agent sending the request, separated by a colon (":").
        int colon = remoteUsername.indexOf(":");
        String localUFrag = remoteUsername.substring(0, colon);
        String remoteUfrag = remoteUsername.substring(colon + 1);

        // Produce Binding Response
        TransportAddress transportAddress = new TransportAddress(remotePeer.getAddress(), remotePeer.getPort(),
                TransportProtocol.UDP);
        StunResponse response = StunMessageFactory.createBindingResponse(request, transportAddress);
        byte[] transactionID = request.getTransactionId();
        try {
            response.setTransactionID(transactionID);
        } catch (StunException e) {
            throw new IOException("Illegal STUN Transaction ID: " + new String(transactionID), e);
        }

        // Add USERNAME and MESSAGE-INTEGRITY attribute in the response.
        // The responses utilize the same usernames and passwords as the requests.
        String localUsername = remoteUfrag.concat(":").concat(localUFrag);
        StunAttribute unameAttribute = StunAttributeFactory.createUsernameAttribute(localUsername);
        response.addAttribute(unameAttribute);

        byte[] localKey = this.authenticator.getLocalKey(localUFrag);
        MessageIntegrityAttribute integrityAttribute = StunAttributeFactory.createMessageIntegrityAttribute(remoteUsername,
                localKey);
        response.addAttribute(integrityAttribute);

        // If the client issues a USE-CANDIDATE, tell ICE Agent to select the candidate
        if (request.containsAttribute(StunAttribute.USE_CANDIDATE)) {
            if (!this.candidateSelected.get()) {
                this.candidateSelected.set(true);
                if (logger.isDebugEnabled()) {
                    logger.debug("Selected candidate " + remotePeer.toString());
                }
                this.iceListener.onSelectedCandidates(new SelectedCandidatesEvent(remotePeer));
            }
        }

        // Pass response to the server
        return response.encode();
    }

    private byte[] processResponse(StunResponse response) {
        throw new UnsupportedOperationException("Support to handle STUN responses is not implemented.");
    }

    @Override
    public int getPipelinePriority() {
        return this.pipelinePriority;
    }

    public void setPipelinePriority(int pipelinePriority) {
        this.pipelinePriority = pipelinePriority;
    }
    
    public void reset() {
        this.authenticator = null;
        this.candidateSelected.set(false);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy