org.elasticsoftware.elasterix.server.actors.User.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticsoftware.elasterix.server.actors.User.java

Source

/*
 * Copyright 2013 Leonard Wolters
 *
 * 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 org.elasticsoftware.elasterix.server.actors;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonProperty;
import org.elasticsoftware.elasterix.server.ServerConfig;
import org.elasticsoftware.elasterix.server.messages.ApiHttpMessage;
import org.elasticsoftware.elasterix.server.messages.SipRequestMessage;
import org.elasticsoftware.elasterix.server.messages.TimeoutMessage;
import org.elasticsoftware.elasterix.server.sip.SipMessageHelper;
import org.elasticsoftware.elasticactors.ActorRef;
import org.elasticsoftware.elasticactors.UntypedActor;
import org.elasticsoftware.sip.codec.SipHeader;
import org.elasticsoftware.sip.codec.SipMethod;
import org.elasticsoftware.sip.codec.SipResponseStatus;
import org.elasticsoftware.sip.codec.SipUser;
import org.elasticsoftware.sip.codec.SipVersion;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.util.StringUtils;

/**
 * User Actor
 *
 * @author Leonard Wolters
 */
public final class User extends UntypedActor {
    private static final Logger log = Logger.getLogger(User.class);
    private static final boolean STRICT_UAC = true;
    private static final boolean SEND_OPTIONS = false;
    /** To be used for generating alphanumeric nonce */
    private static final char[] CHARACTERS = new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
            'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
    private Md5PasswordEncoder encoder = new Md5PasswordEncoder();

    @Override
    public void onReceive(ActorRef sender, Object message) throws Exception {
        ActorRef sipService = getSystem().serviceActorFor("sipService");
        State state = getState(null).getAsObject(State.class);

        if (message instanceof SipRequestMessage) {
            SipRequestMessage request = (SipRequestMessage) message;

            // check if request authenticated
            if (request.isAuthenticated()) {
                // previously authenticated. Continue
            } else {
                if (authenticate(sender, request, state)) {
                    request.setAuthenticated(true);
                    if (request.getSipMethod() == SipMethod.REGISTER) {
                        // OK, Update nonce so that next requests are automatically rejected. 
                        state.setNonce(generateNonce());
                    }
                    sender.tell(request, getSelf());
                } else {
                    if (request.getSipMethod() == SipMethod.REGISTER) {
                        state.setNonce(generateNonce());
                        request.addHeader(SipHeader.WWW_AUTHENTICATE,
                                String.format("Digest algorithm=%s, " + "realm=\"%s\", nonce=\"%s\"",
                                        ServerConfig.getDigestAlgorithm(), ServerConfig.getRealm(),
                                        state.getNonce()));
                    }
                    sipService.tell(request.toSipResponseMessage(SipResponseStatus.UNAUTHORIZED), getSelf());
                }
                return;
            }

            switch (request.getSipMethod()) {
            case REGISTER:
                register(sipService, request, state);
                return;
            case INVITE:
                invite(sipService, request, state);
                return;
            default:
                log.warn(String.format("onReceive. Unsupported message[%s]", message.getClass().getSimpleName()));
                unhandled(message);
            }
        } else if (message instanceof ApiHttpMessage) {
            ApiHttpMessage apiMessage = (ApiHttpMessage) message;

            HttpMethod method = apiMessage.getMethod();
            if (HttpMethod.GET == method) {
                sender.tell(apiMessage.toHttpResponse(HttpResponseStatus.OK, state), getSelf());
            } else if (HttpMethod.PUT == method || HttpMethod.POST == method) {

                // check action. No matter if GET or POST/UPDATE?
                if ("reset".equalsIgnoreCase(apiMessage.getAction())
                        || "clear".equalsIgnoreCase(apiMessage.getAction())) {
                    log.info("Resetting: " + state.getUsername());
                    state.userAgentClients.clear();
                    sender.tell(apiMessage.toHttpResponse(HttpResponseStatus.OK, state), getSelf());
                    return;
                }

                // update and post state afterwards...
                User.State update = apiMessage.getContent(User.State.class);
                if (update == null) {
                    sender.tell(apiMessage.toHttpResponse(HttpResponseStatus.NO_CONTENT), getSelf());
                    return;
                }

                // update current state...
                if (StringUtils.hasLength(update.getFirstName())) {
                    state.firstName = update.getFirstName();
                }
                if (StringUtils.hasLength(update.getLastName())) {
                    state.lastName = update.getLastName();
                }
                if (StringUtils.hasLength(update.getPassword())) {
                    state.password = update.getPassword();
                }
                sender.tell(apiMessage.toHttpResponse(HttpResponseStatus.OK, state), getSelf());
            } else if (HttpMethod.DELETE == method) {
                // TODO: we can either remove user here or at UserController#onReceive
                getSystem().stop(getSelf());
                sender.tell(apiMessage.toHttpResponse(HttpResponseStatus.OK, state), getSelf());
            }
        } else if (message instanceof TimeoutMessage) {
            state.clearUserAgentClients();
        } else {
            unhandled(message);
        }
    }

    @Override
    public void onUndeliverable(ActorRef receiver, Object message) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug(String.format("onUndeliverable. Message[%s]", message));
        }

        ActorRef sipService = getSystem().serviceActorFor("sipService");
        if (message instanceof SipRequestMessage) {
            SipRequestMessage m = (SipRequestMessage) message;
            switch (m.getSipMethod()) {
            case INVITE:
                if (log.isDebugEnabled()) {
                    log.debug(String.format("onUndeliverable. UAC[%s] does not exist", receiver.getActorId()));
                }

                if (STRICT_UAC) {
                    sipService.tell(
                            m.toSipResponseMessage(SipResponseStatus.GONE
                                    .setOptionalMessage(String.format("UAC[%s] not found", receiver.getActorId()))),
                            getSelf());
                } else {
                    // create UAC and resent message
                    StringTokenizer st = new StringTokenizer(receiver.getActorId(), "/_", false);
                    if (st.countTokens() == 4) {
                        st.nextToken(); // skip "uac"
                        UserAgentClient.State state = new UserAgentClient.State(st.nextToken(), st.nextToken(),
                                Integer.parseInt(st.nextToken()));
                        ActorRef ref = getSystem().actorOf(receiver.getActorId(), UserAgentClient.class, state);
                        ref.tell(message, getSelf());
                    }
                }
            }
        }
    }

    /**
     * See http://en.wikipedia.org/wiki/Digest_access_authentication
     * 
     * @param state
     * @param props
     * @return
     */
    protected String generateHash(User.State state, Map<String, String> props) {
        // please see 
        // http://en.wikipedia.org/wiki/Digest_access_authentication
        // http://hashcat.net/forum/thread-1455.html
        String ha1 = String.format("%s:%s:%s", props.get("username"), props.get("realm"), state.password);
        String ha2 = String.format("%s:%s", "REGISTER", props.get("uri"));
        //log.debug(String.format("HA1(%s) -> %s", ha1, encoder.encodePassword(ha1, null)));
        //log.debug(String.format("HA2(%s) -> %s", ha2, encoder.encodePassword(ha2, null)));
        return encoder.encodePassword(String.format("%s:%s:%s", encoder.encodePassword(ha1, null),
                props.get("nonce"), encoder.encodePassword(ha2, null)), null);
    }

    protected void register(ActorRef sipService, SipRequestMessage message, State state) {
        if (log.isDebugEnabled())
            log.debug(String.format("register. [%s]", message));

        // get uac 
        SipUser user = message.getSipUser(SipHeader.CONTACT);

        // check expiration...
        long expires = message.getExpires();
        if (expires == 0) {
            // remove current binding with UAC set in message (if exist)
            state.removeUserAgentClient(user);

            // TODO remove UAC actor as well?
        } else {
            if (log.isDebugEnabled()) {
                log.debug(String.format("register. Registering UAC[%s:%d] for User[%s]", user.getDomain(),
                        user.getPort(), user.getUsername()));
            }

            if (STRICT_UAC) {
                try {
                    UserAgentClient.State uacState = new UserAgentClient.State(user.getUsername(), user.getDomain(),
                            user.getPort());
                    getSystem().actorOf(state.key(user), UserAgentClient.class, uacState);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }

            // update binding (with new expiration)
            state.addUserAgentClient(user, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expires));

            // schedule timeout for destruction (i.e. removal of binding UAC / user)
            TimeoutMessage timeoutMessage = new TimeoutMessage();
            timeoutMessage.setTimeoutInMilliSeconds(TimeUnit.SECONDS.toMillis(expires));
            timeoutMessage.setUserAgentClient(state.key(user));
            getSystem().getScheduler().scheduleOnce(getSelf(), timeoutMessage, getSelf(), expires,
                    TimeUnit.SECONDS);

            // send SIP Options message to client in order to find out which services are supported
            if (SEND_OPTIONS) {
                sipService.tell(SipMessageHelper.createOptions(user, SipVersion.SIP_2_0,
                        String.format("%s:%d", user.getDomain(), user.getPort())), getSelf());
            }
        }

        // send OK back to client
        sipService.tell(message.toSipResponseMessage(SipResponseStatus.OK), getSelf());
    }

    protected void invite(ActorRef sipService, SipRequestMessage message, State state) {
        if (log.isDebugEnabled()) {
            log.debug(String.format("invite: notify UAC's of callee[%s]", state.getUsername()));
        }

        // forward message to all registered UAC's of callee
        long now = System.currentTimeMillis();
        boolean ringing = false;
        for (Map.Entry<String, Long> uacEntry : state.getUserAgentClients().entrySet()) {
            // check is UAC is expired
            if (uacEntry.getValue() < now) {
                log.info(String.format("invite. User[%s] -> UAC[%s, %s] expired", state.getUsername(),
                        uacEntry.getKey(), new Date(uacEntry.getValue())));
            } else {
                if (log.isDebugEnabled()) {
                    log.debug(String.format("invite. User[%s], ringing UAC[%s]", state.getUsername(),
                            uacEntry.getKey()));
                }
                // sent message to UAC
                ActorRef actor = getSystem().actorFor(uacEntry.getKey());
                actor.tell(message, getSelf());
                ringing = true;
            }
        }

        // did we rang a device (or at least notified a single UAC)?
        if (!ringing) {
            log.info(String.format("invite. No registered UAC for user[%s]", state.getUsername()));
            sipService.tell(message.toSipResponseMessage(SipResponseStatus.GONE
                    .setOptionalMessage(String.format("No registered UAC for user[%s]", state.getUsername()))),
                    getSelf());
        } else {
            // OK, the message is sent to at least one UAC. Wait for the response
            // to be sent back by this UAC. For now, return a 'trying' which is a
            // decent message
            sipService.tell(message.toSipResponseMessage(SipResponseStatus.TRYING), getSelf());
        }
    }

    private final boolean authenticate(ActorRef dialog, SipRequestMessage request, State state) {
        if (log.isDebugEnabled()) {
            log.debug(String.format("onReceive. Authenticating %s[%s]", request.getMethod(), state.getUsername()));
        }

        switch (request.getSipMethod()) {
        case REGISTER:
            // check if authentication is present...
            String authorization = request.getHeader(SipHeader.AUTHORIZATION);
            if (!StringUtils.hasLength(authorization)) {
                if (log.isDebugEnabled())
                    log.debug("authenticate. No authorization set");
                return false;
            }

            Map<String, String> map = request.tokenize(SipHeader.AUTHORIZATION);

            // check username
            String val = map.get("username");
            if (!state.getUsername().equalsIgnoreCase(val)) {
                if (log.isDebugEnabled())
                    log.debug(String.format("authenticate. Provided username[%s] " + "!= given username[%s]", val,
                            state.getUsername()));
                return false;
            }

            // check nonce
            val = map.get("nonce");
            if (!state.getNonce().equalsIgnoreCase(val)) {
                if (log.isDebugEnabled())
                    log.debug(String.format("authenticate. Provided nonce[%s] " + "!= given nonce[%s]", val,
                            state.getNonce()));
                return false;
            }

            // check hash
            val = map.get("response");
            String secretHash = generateHash(state, map);
            if (!secretHash.equals(val)) {
                if (log.isDebugEnabled())
                    log.debug(String.format("authenticate. Provided hash[%s] " + "!= given hash[%s]", val,
                            secretHash));
                return false;
            }
            return true;
        case INVITE:
            // check if we have a UAC registered for user
            Long expires = state.getUserAgentClient(request.getSipUser(SipHeader.CONTACT));
            if (expires == null || expires.longValue() < System.currentTimeMillis()) {
                return false;
            } else {
                return true;
            }
        }
        return false;
    }

    private String generateNonce() {
        // a value between 10M and 100M 
        long nonce = (10000000 + ((long) (Math.random() * 90000000.0)));
        // add 5 random characters at random places...
        StringBuffer sb = new StringBuffer(Long.toString(nonce));
        for (int i = 0; i < 5; i++) {
            char c = CHARACTERS[(int) ((double) (CHARACTERS.length - 1) * Math.random())];
            int idx = (int) (Math.random() * (double) (sb.length() - 1));
            sb.insert(idx, c);
        }
        return sb.toString();
    }

    /**
     * State belonging to User
     */
    public static final class State {
        private final String email;
        private final String username;
        /** UID of User Agent Client (key) and expires (seconds) as value */
        private Map<String, Long> userAgentClients = new HashMap<String, Long>();
        private String nonce;
        private String tag;
        private String firstName;
        private String lastName;
        private String password;

        @JsonCreator
        public State(@JsonProperty("email") String email, @JsonProperty("username") String username,
                @JsonProperty("password") String password) {
            this.email = email;
            this.username = username;
            this.password = password;
        }

        @JsonProperty("email")
        public String getEmail() {
            return email;
        }

        @JsonProperty("username")
        public String getUsername() {
            return username;
        }

        @JsonProperty("password")
        public String getPassword() {
            return password;
        }

        @JsonProperty("userAgentClients")
        public Map<String, Long> getUserAgentClients() {
            return userAgentClients;
        }

        @JsonProperty("nonce")
        public String getNonce() {
            return nonce;
        }

        @JsonProperty("tag")
        public String getTag() {
            return tag;
        }

        @JsonProperty("firstName")
        public String getFirstName() {
            return firstName;
        }

        @JsonProperty("lastName")
        public String getLastName() {
            return lastName;
        }

        public boolean removeUserAgentClient(SipUser user) {
            return userAgentClients.remove(key(user)) != null;
        }

        /**
         * ExpirationDate is the actual Date in ms when timeout occurs
         * 
         * @param uac
         * @param expiration
         */
        public void addUserAgentClient(SipUser user, long expirationDate) {
            removeUserAgentClient(user);
            userAgentClients.put(key(user), expirationDate);
        }

        public void clearUserAgentClients() {
            long now = System.currentTimeMillis();
            Iterator<Map.Entry<String, Long>> it = userAgentClients.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, Long> entry = (Map.Entry<String, Long>) it.next();
                if (entry.getValue() < now) {
                    if (log.isDebugEnabled()) {
                        log.debug(String.format("Removing %s %s", entry.getKey(), new Date(entry.getValue())));
                    }
                    it.remove();
                }
            }
        }

        public Long getUserAgentClient(SipUser user) {
            return userAgentClients.get(key(user));
        }

        protected void setNonce(String nonce) {
            this.nonce = nonce;
        }

        protected String key(SipUser user) {
            return String.format("uac/%s_%s_%d", user.getUsername(), user.getDomain(), user.getPort());
        }
    }
}