package javaforce.voip; import java.net.*; import java.util.*; import javaforce.*; /** * Handles the client end of a SIP link. */ public class SIPClient extends SIP implements SIPInterface, STUN.Listener { public enum NAT {None, STUN, TURN, ICE}; private String remotehost, remoteip; private InetAddress remoteaddr; private int remoteport; private String name; //display name (usually same as user) private String user; //acct name (usually a phone number) private String auth; //auth name (usually same as user) private String pass; //password private SIPClientInterface iface; private String localhost; private int localport, rport = -1; private static boolean use_received = true; private static boolean use_rport = true; private Hashtable<String, CallDetails> cdlist; private boolean registered; private static NAT nat = NAT.None; private static boolean useNATOnPrivateNetwork = false; //do not use NATing techniques on private network servers private static String stunHost, stunUser, stunPass; public Object rtmp; //used by RTMP2SIPServer public Object userobj; //user definable public int expires; //expires /** * Returns the registered user name. */ public String getUser() { return user; } /** * Returns the remote host. */ public String getRemoteHost() { return remotehost; } /** * Determines if the SIP call is currently on hold. */ public boolean isHold(String callid) { CallDetails cd = getCallDetails(callid); if (cd.dst.sdp == null) return false; return cd.dst.sdp.getFirstAudioStream().canSend(); } /** * Returns the registration status. */ public boolean isRegistered() { return registered; } /** * Initialize this instance.<br> * * @param remotehost,remoteport is the SIP Server/Proxy address.<br> * @param localport is the UDP port to bind to locally.<br> * @param iface must be a SIPClientInterface where SIP events are dispatched * to.<br> */ public boolean init(String remotehost, int remoteport, int localport, SIPClientInterface iface, Transport type) { this.iface = iface; this.localport = localport; this.remoteport = remoteport; this.remotehost = remotehost; this.remoteip = resolve(remotehost); cdlist = new Hashtable<String, CallDetails>(); try { this.remoteaddr = InetAddress.getByName(remoteip); if (nat == NAT.STUN || nat == NAT.ICE) { if (!startSTUN()) return false; } findlocalhost(); JFLog.log("localhost = " + localhost + " for remotehost = " + remotehost); if (this.remotehost.equals("127.0.0.1")) { this.remotehost = localhost; remoteip = resolve(this.remotehost); JFLog.log("changed 127.0.0.1 to " + this.remotehost + " " + this.remoteip); } if (nat == NAT.STUN || nat == NAT.ICE) { stopSTUN(); } return super.init(localport, this, false, type); } catch (Exception e) { if (stun != null) stopSTUN(); JFLog.log(e); return false; } } /** * Free all resources. */ public void uninit() { super.uninit(); } /** * Sets the type of NAT traversal type (global setting). */ public static void setNAT(NAT nat, String host, String user, String pass) { SIPClient.nat = nat; stunHost = host; stunUser = user; stunPass = pass; } /** * Disable/enable use of NAT traversal on private networks (global setting) * Private networks : 192.168.x.x , 10.x.x.x , 172.[16-31].x.x */ public static void useNATOnPrivateNetwork(boolean state) { useNATOnPrivateNetwork = state; } private String cleanString(String in) { return in.replaceAll("\"", ""); } /** * Registers this client with the SIP server/proxy. <br> * * @param displayName : display name<br> * @param userAccount : username<br> * @param authName : authorization name (optional, default=user)<br> * @param password : password<br> * @return : if message was sent to server successfully<br> This function does * not block waiting for a reply. You should receive onRegister() thru the * SIPClientInterface when a reply is returned from server.<br> */ public boolean register(String displayName, String userAccount, String authName, String password) { return register(displayName, userAccount, authName, password, 3600); } /** * Registers this client with the SIP server/proxy. <br> * * @param displayName : display name (usually same as userAccount)<br> * @param userAccount : username<br> * @param authName : authorization name (optional, default=userAccount)<br> * @param password : password<br> * @param expires : number seconds to register for (0=unregisters)<br> * @return : if message was sent to server<br> This function does not block * waiting for a reply. You should receive either registered() or * unauthorized() thru the SIPClientInterface when a reply is returned from * server.<br> * */ public boolean register(String displayName, String userAccount, String authName, String password, int expires) { String regcallid = getcallid(); CallDetails cd = getCallDetails(regcallid); //new CallDetails if (userAccount == null || userAccount.length() == 0) return false; userAccount = cleanString(userAccount); if ((authName == null) || (authName.length() == 0)) { this.auth = userAccount; } else { this.auth = cleanString(authName); } if (displayName == null || displayName.length() == 0) { this.name = userAccount; } else { this.name = cleanString(displayName); } this.user = userAccount; this.pass = password; this.expires = expires; cd.src.expires = expires; cd.src.to = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.from = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.from = replacetag(cd.src.from, generatetag()); cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.uri = "sip:" + remotehost; // + ";rinstance=" + getrinstance(); cd.src.branch = getbranch(); cd.src.cseq++; if ((password == null) || (password.length() == 0)) { return true; //non-register mode } cd.authsent = false; cd.src.extra = null; cd.src.epass = null; boolean ret = issue(cd, "REGISTER", false, true); return ret; } /** * Reregister with the server. */ public boolean reregister() { return register(name, user, auth, pass, expires); } /** * Reregister with the server using an expiration of 0 (zero). Effectively * unregisters. */ public boolean unregister() { return register(name, user, auth, pass, 0); } /** * Publishes Presence to server. (not tested since Asterisk doesn't support it) */ public boolean publish(String state) { String pubcallid = getcallid(); CallDetails cd = getCallDetails(pubcallid); //new CallDetails cd.src.to = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.from = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.from = replacetag(cd.src.from, generatetag()); cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.uri = "sip:" + user + "@" + remotehost; cd.src.branch = getbranch(); cd.src.cseq++; cd.sdp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><presence xmlns=\"urn:ietf:params:xml:ns:pidf\" entity=\"pres:" + user + "@" + remotehost + "\">" + "<tuple id=\"" + gettupleid() + "\"><status><basic>" + state + "</basic></status></tuple></presence>"; cd.authsent = false; cd.src.extra = "Event: presence\r\n"; cd.src.epass = null; return issue(cd, "PUBLISH", true, true); } /** * Subscribe to a user's presence on server. */ public boolean subscribe(String subuser, String event, int expires) { String subcallid = getcallid(); CallDetails cd = getCallDetails(subcallid); //new CallDetails cd.src.to = new String[]{subuser, subuser, remotehost + ":" + remoteport, ":"}; cd.src.from = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.from = replacetag(cd.src.from, generatetag()); cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.uri = "sip:" + subuser + "@" + remotehost; cd.src.branch = getbranch(); cd.src.cseq++; cd.src.expires = expires; cd.src.extra = "Accept: multipart/related, application/rlmi+xml, application/pidf+xml\r\nEvent: " + event + "\r\n"; cd.src.epass = null; return issue(cd, "SUBSCRIBE", false, true); } /** * Send an empty SIP message to server. This should be done periodically to * keep firewalls open. Most routers close UDP connections after 60 seconds. * Not sure if needed with TCP/TLS but is done anyways. */ public void keepalive() { send(remoteaddr, remoteport, "\r\n\r\n"); //must resemble a complete packet } /** * Determine if server is on a local private network. */ public static boolean isPrivateNetwork(String ip) { //in case your PBX is on your own local IP network //see http://en.wikipedia.org/wiki/Private_network if (ip.startsWith("192.168.")) { return true; } if (ip.startsWith("10.")) { return true; } if (ip.startsWith("169.254.")) { return true; } for(int a=16;a<=31;a++) { if (ip.startsWith("172." + a + ".")) return true; } return false; } /** * Returns local RTP IP address. */ public String getlocalRTPhost(CallDetails cd) { if (RTP.useTURN) return RTP.getTurnIP(); else return cd.localhost; } private boolean findlocalhost_webserver(String host) { //this returns your local ip (more accurate on multi-homed systems than java api) Socket s; try { s = new Socket(); s.connect(new InetSocketAddress(host, 80), 1000); localhost = s.getLocalAddress().getHostAddress(); try { s.close(); } catch (Exception e) {} JFLog.log("Detected IP connecting to WebServer at " + host); return true; } catch (Exception e) { return false; } } private STUN stun; private final Object stunLock = new Object(); private volatile boolean stunWaiting = false; private volatile boolean stunResponse = false; private boolean startSTUN() { stun = new STUN(); if (!stun.start(localport, stunHost, stunUser, stunPass, this)) return false; return true; } private void stopSTUN() { if (stun == null) return; stun.close(); stun = null; } //interface STUN.Listener public void stunPublicIP(STUN stun, String ip, int port) { synchronized(stunLock) { if (stunWaiting) { // localhost = ip; rport = port; //NOTE : this port may be wrong if router is symmetrical stunResponse = true; stunLock.notify(); } } } public void turnAlloc(STUN stun, String ip, int port, byte token[], int lifetime) {} public void turnBind(STUN stun) {} public void turnRefresh(STUN stun, int lifetime) {} public void turnFailed(STUN stun) {} public void turnData(STUN stun, byte data[], int offset, int length, short channel) {} private boolean findlocalhost_stun() { stunResponse = false; stunWaiting = true; synchronized(stunLock) { stun.requestPublicIP(); try {stunLock.wait(1000);} catch (Exception e) {JFLog.log(e);} stunWaiting = false; } if (stunResponse) { JFLog.log("Detected IP using STUN"); } return stunResponse; } private boolean findlocalhost_java() { //this only detects your local IP, not your internet ip //not accurate on multi-homed systems try { InetAddress local = InetAddress.getLocalHost(); localhost = local.getHostAddress(); JFLog.log("Detected IP using Java:" + localhost); return true; } catch (Exception e) { return false; } } /** * Determines local IP address. Method depends on NAT traversal type selected. */ private void findlocalhost() { JFLog.log("Detecting localhost for remotehost = " + remotehost); if (useNATOnPrivateNetwork || !isPrivateNetwork(remoteip)) { if (nat == NAT.STUN || nat == NAT.ICE) { if (findlocalhost_stun()) return; JFLog.log("SIP:STUN:Failed"); } } //try connecting to remotehost on webserver port if (findlocalhost_webserver(remotehost)) return; //use java (returns local ip, not internet ip) (not reliable on multi-homed systems) if (findlocalhost_java()) return; //if all else fails (use a dummy DHCP failure IP) Random r = new Random(); localhost = "169.254." + r.nextInt(256) + "." + r.nextInt(256); } private synchronized CallDetails getCallDetails(String callid) { CallDetails cd = cdlist.get(callid); if (cd == null) { cd = new CallDetails(); JFLog.log("Create CallDetails:" + callid); cd.callid = callid; cd.localhost = localhost; setCallDetails(callid, cd); } return cd; } private void setCallDetails(String callid, CallDetails cd) { if (cd == null) { JFLog.log("Delete CallDetails:" + callid); cdlist.remove(callid); } else { cdlist.put(callid, cd); } } /** * Issues a command to the SIP server. */ private boolean issue(CallDetails cd, String cmd, boolean sdp, boolean src) { CallDetails.SideDetails cdsd = (src ? cd.src : cd.dst); JFLog.log("callid:" + cd.callid + "\r\nissue command : " + cmd + " from : " + user + " to : " + remotehost); cd.dst.host = remoteip; cd.dst.port = remoteport; StringBuilder req = new StringBuilder(); if (cd.uri == null) { cd.uri = "sip:" + user + "@" + remotehost; } req.append(cmd + " " + cd.uri + " SIP/2.0\r\n"); req.append("Via: SIP/2.0/" + transport.getName() + " " + cd.localhost + ":" + getlocalport() + ";branch=" + cdsd.branch + (use_rport ? ";rport" : "") + "\r\n"); req.append("Max-Forwards: 70\r\n"); if (cdsd.routelist != null) { for (int a = cdsd.routelist.length-1; a >=0; a--) { req.append(cdsd.routelist[a]); req.append("\r\n"); } } if (cdsd.contact != null) { req.append("Contact: " + cdsd.contact + "\r\n"); } req.append("To: " + join(cdsd.to) + "\r\n"); req.append("From: " + join(cdsd.from) + "\r\n"); req.append("Call-ID: " + cd.callid + "\r\n"); req.append("Cseq: " + cdsd.cseq + " " + cmd + "\r\n"); if ((cmd.equals("REGISTER")) || (cmd.equals("PUBLISH")) || (cmd.equals("SUBSCRIBE"))) { req.append("Expires: " + cdsd.expires + "\r\n"); } req.append("Allow: INVITE, ACK, CANCEL, BYE, REFER, NOTIFY, OPTIONS\r\n"); req.append("User-Agent: " + useragent + "\r\n"); if (cdsd.extra != null) { req.append(cdsd.extra); } if (cdsd.epass != null) { req.append(cdsd.epass); } if ((cd.sdp != null) && (sdp)) { if (cd.sdp.startsWith("<?xml")) { req.append("Content-Type: application/pidf+xml\r\n"); } else if (cd.sdp.startsWith("SIP/2.0")) { req.append("Content-Type: message/sipfrag;version=2.0\r\n"); } else { req.append("Content-Type: application/sdp\r\n"); } req.append("Content-Length: " + cd.sdp.length() + "\r\n\r\n"); req.append(cd.sdp); } else { req.append("Content-Length: 0\r\n\r\n"); } return send(remoteaddr, remoteport, req.toString()); } /** * Sends a reply to a SIP server. */ private boolean reply(CallDetails cd, String cmd, int code, String msg, boolean sdp, boolean src) { JFLog.log("callid:" + cd.callid + "\r\nissue reply : " + code + " to : " + remotehost); CallDetails.SideDetails cdsd = (src ? cd.src : cd.dst); StringBuilder req = new StringBuilder(); req.append("SIP/2.0 " + code + " " + msg + "\r\n"); if (cdsd.vialist != null) { for (int a = 0; a < cdsd.vialist.length; a++) { req.append(cdsd.vialist[a]); req.append("\r\n"); } } if ((cdsd.vialist == null) || (cdsd.vialist.length == 0)) { req.append("Via: SIP/2.0/" + transport.getName() + " " + cd.localhost + ":" + getlocalport() + ";branch=" + cdsd.branch + (use_rport ? ";rport" : "") + "\r\n"); } if (cdsd.routelist != null) { for (int a = cdsd.routelist.length-1; a >=0; a--) { req.append(cdsd.routelist[a]); req.append("\r\n"); } } req.append("Contact: " + cdsd.contact + "\r\n"); req.append("To: " + join(cdsd.to) + "\r\n"); req.append("From: " + join(cdsd.from) + "\r\n"); req.append("Call-ID: " + cd.callid + "\r\n"); req.append("Cseq: " + cdsd.cseq + " " + cmd + "\r\n"); req.append("Allow: INVITE, ACK, CANCEL, BYE, REFER, NOTIFY, OPTIONS\r\n"); req.append("User-Agent: JavaForce\r\n"); if ((cd.sdp != null) && (sdp)) { req.append("Content-Type: application/sdp\r\n"); req.append("Content-Length: " + cd.sdp.length() + "\r\n\r\n"); req.append(cd.sdp); } else { req.append("Content-Length: 0\r\n\r\n"); } send(remoteaddr, remoteport, req.toString()); return true; } /** * Send an invite to server.<br> * * @param to : number to dial * @param sdp : SDP (only stream types/modes/codecs are needed) (ip not needed) * * @return unique Call-ID (not caller id)<br> */ public String invite(String to, SDP sdp) { String callid = getcallid(); CallDetails cd = getCallDetails(callid); //new CallDetails cd.src.to = new String[]{to, to, remotehost + ":" + remoteport, ":"}; cd.src.from = new String[]{name, user, remotehost + ":" + remoteport, ":"}; cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.uri = "sip:" + to + "@" + remotehost + ":" + remoteport; cd.src.from = replacetag(cd.src.from, generatetag()); cd.src.branch = getbranch(); cd.src.o1 = 256; cd.src.o2 = 256; cd.src.sdp = sdp; buildsdp(cd, cd.src); cd.src.cseq++; cd.authsent = false; cd.src.extra = null; cd.src.epass = null; if (!issue(cd, "INVITE", true, true)) { return null; } return callid; } /** * Send a refer command to server (blind transfer) */ public boolean refer(String callid, String to) { String headers = "Refer-To: <sip:" + to + "@" + remotehost + ">\r\nReferred-By: <sip:" + user + "@" + remotehost + ":" + getlocalport() + ">\r\n"; CallDetails cd = getCallDetails(callid); if (cd.authstr != null) { cd.src.epass = getAuthResponse(cd, auth, pass, remotehost, "REFER", "Proxy-Authorization:"); } else { cd.src.epass = null; } cd.src.cseq++; cd.authsent = false; cd.src.extra = headers; cd.src.epass = null; boolean ret = issue(cd, "REFER", false, true); return ret; } /** * Send a refer command to server (non-blind transfer) */ public boolean referLive(String callid, String othercallid) { //see RFC5589 part 7.2 //this MUST be sent to the transfer target from the transferor (not to the transferee) [we are actually transfering the target to the tranferee - kinda backwards thinking] //callid = transfer target call leg //othercallid = transferee call leg CallDetails cd = getCallDetails(callid); CallDetails othercd = getCallDetails(othercallid); String headers = "Refer-To: <sip:" + othercd.src.to[1] + "@" + remotehost + "?Replaces=" + othercallid + "%3Bto-tag%3D" + gettag(othercd.src.to) + "%3Bfrom-tag%3D" + gettag(othercd.src.from) + ">\r\n"; // headers += "Referred-By: <sip:" + user + "@" + remotehost + ":" + getlocalport() + ">\r\n"; headers += "Supported: gruu, replaces, tdialog\r\n"; headers += "Require: tdialog\r\n"; headers += "Target-Dialog: " + callid + ";local-tag=" + gettag(cd.src.to) + ";remote-tag=" + gettag(cd.src.from) + "\r\n"; cd.src.cseq++; cd.authsent = false; cd.src.extra = headers; cd.src.epass = null; boolean ret = issue(cd, "REFER", false, true); return ret; } /** * Set/clear hold state, must call reinvite() after to notify server. */ public boolean setHold(String callid, boolean state) { CallDetails cd = getCallDetails(callid); cd.src.sdp.getFirstAudioStream().mode = (state ? SDP.Mode.inactive : SDP.Mode.sendrecv); return true; } /** * Sends a reINVITE to server with a new SDP packet. * * @param callid : id of call to reinvite * @param sdp : SDP (only stream types/modes/codecs are needed) (ip not needed) */ public boolean reinvite(String callid, SDP sdp) { CallDetails cd = getCallDetails(callid); cd.src.o2++; cd.src.sdp = sdp; buildsdp(cd, cd.src); cd.src.cseq++; cd.authsent = false; cd.src.extra = null; if (cd.authstr != null) { cd.src.epass = getAuthResponse(cd, auth, pass, remotehost, "INVITE", "Proxy-Authorization:"); } else { cd.src.epass = null; } if (!issue(cd, "INVITE", true, true)) { return false; } return true; } /** * Sends a reINVITE to server using previous SDP packet. * * @param callid : id of call to reinvite */ public boolean reinvite(String callid) { CallDetails cd = getCallDetails(callid); return reinvite(callid, cd.src.sdp); } /** * Cancels an INVITE request. Usually done while phone is ringing. */ public boolean cancel(String callid) { CallDetails cd = getCallDetails(callid); if (cd.authstr != null) { cd.src.epass = getAuthResponse(cd, auth, pass, remotehost, "BYE", "Proxy-Authorization:"); } else { cd.src.epass = null; } cd.authsent = false; if (cd.dst.to != null) cd.src.to = cd.dst.to; //update tag if avail if (cd.dst.from != null) cd.src.from = cd.dst.from; //is this line even needed??? cd.src.extra = null; boolean ret = issue(cd, "CANCEL", false, true); return ret; } /** * Send a request to terminate a call in progress. */ public boolean bye(String callid) { CallDetails cd = getCallDetails(callid); if (cd.authstr != null) { cd.src.epass = getAuthResponse(cd, auth, pass, remotehost, "BYE", "Proxy-Authorization:"); } else { cd.src.epass = null; } cd.src.cseq++; cd.authsent = false; cd.src.extra = null; boolean ret = issue(cd, "BYE", false, true); return ret; } /** * Sends a reply to accept an inbound (re)INVITE. * @param sdp : SDP (only stream types/modes/codecs are needed) (ip not needed) */ public boolean accept(String callid, SDP sdp) { CallDetails cd = getCallDetails(callid); cd.src.sdp = sdp; buildsdp(cd, cd.src); reply(cd, "INVITE", 200, "OK", true, false); //need to swap to/from for BYE (only if accept()ing a call) cd.src.to = cd.dst.from.clone(); cd.src.from = cd.dst.to.clone(); //copy all other needed fields cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.src.branch = cd.dst.branch; return true; } /** * Sends a reply to accept an inbound REINVITE. * @param sdp : SDP (only stream types/modes/codecs are needed) (ip not needed) */ public boolean reaccept(String callid, SDP sdp) { CallDetails cd = getCallDetails(callid); cd.src.sdp = sdp; buildsdp(cd, cd.src); reply(cd, "INVITE", 200, "OK", true, false); //do NOT swap to/from in this case //copy all other needed fields cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; cd.src.branch = cd.dst.branch; return true; } /** * Denies an INVITE for whatever reason. */ public boolean deny(String callid, String msg, int code) { CallDetails cd = getCallDetails(callid); reply(cd, "INVITE", code, msg, false, false); return true; } /** * Processes SIP messages sent from the SIP server. */ public void packet(String msg[], String remoteip, int remoteport) { try { if (!remoteip.equals(this.remoteip) || remoteport != this.remoteport) { JFLog.log("Ignoring packet from unknown host:" + remoteip + ":" + remoteport); return; } String tmp, req = null; String callid = getHeader("Call-ID:", msg); if (callid == null) callid = getHeader("i:", msg); if (callid == null) { JFLog.log("Bad packet (no Call-ID) from:" + remoteip + ":" + remoteport); return; } CallDetails cd = getCallDetails(callid); if (remoteip.equals("127.0.0.1")) { remoteip = cd.localhost; } cd.dst.host = remoteip; cd.dst.port = remoteport; cd.dst.cseq = getcseq(msg); cd.dst.branch = getbranch(msg); cd.headers = msg; //get cd.dst.to tmp = getHeader("To:", msg); if (tmp == null) { tmp = getHeader("t:", msg); } cd.dst.to = split(tmp); //get cd.dst.from tmp = getHeader("From:", msg); if (tmp == null) { tmp = getHeader("f:", msg); } cd.dst.from = split(tmp); //RFC 3581 - rport String via = getHeader("Via:", msg); boolean localhost_changed = false; if (via == null) { via = getHeader("v:", msg); } if (via != null) { String f[] = via.split(";"); if (use_received) { //check for received in via header which equals my IP as seen by server String received = getHeader("received=", f); if (received != null) { if (!cd.localhost.equals(received)) { localhost = received; cd.localhost = received; JFLog.log("received ip=" + received + " for remotehost = " + remotehost); localhost_changed = true; } } } if (use_rport) { //check for rport in via header which equals my port as seen by server String rportstr = getHeader("rport=", f); if (rportstr != null && rportstr.length() > 0) { int newrport = JF.atoi(rportstr); if (rport != newrport) { rport = newrport; JFLog.log("received port=" + rport + " for remotehost = " + remotehost); localhost_changed = true; } } } } //get via list cd.dst.vialist = getvialist(msg); //get route list (RFC 2543 6.29) cd.dst.routelist = getroutelist(msg); //set contact // cd.dst.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; //get uri (it must equal the Contact field) cd.dst.contact = getHeader("Contact:", msg); if (cd.dst.contact == null) { cd.dst.contact = getHeader("m:", msg); } String cmd = getcseqcmd(msg); int type = getResponseType(msg); if (type != -1) { JFLog.log("callid:" + callid + "\r\nreply=" + type); //RFC 3261 : 8.1.3.3 - Vias if (cd.dst.vialist.length > 1) { JFLog.log("Multiple Via:s detected in reply, discarding reply"); return; } } else { req = getRequest(msg); cd.uri = getURI(msg); JFLog.log("callid:" + callid + "\r\nrequest=" + req); } switch (type) { case -1: if (req.equals("INVITE")) { //generate toTag if (gettag(cd.dst.to) == null) { cd.dst.to = replacetag(cd.dst.to, generatetag()); } //get o1/o2 cd.dst.o1 = geto(msg, 1) + 1; cd.dst.o2 = geto(msg, 2) + 1; cd.dst.sdp = getSDP(msg); switch (iface.onInvite(this, callid, cd.dst.from[0], cd.dst.from[1], cd.dst.sdp)) { case 180: //this is the normal return reply(cd, cmd, 180, "RINGING", false, false); break; case 200: //this was usually a reINVITE to change sdp details buildsdp(cd, cd.src); reply(cd, cmd, 200, "OK", true, false); break; case 486: reply(cd, cmd, 486, "BUSY HERE", false, false); break; case -1: //do nothing break; } break; } if (req.equals("CANCEL")) { iface.onCancel(this, callid, 0); reply(cd, cmd, 200, "OK", false, false); reply(cd, "INVITE", 487, "CANCELLED", false, false); //then should receive ACK // setCallDetails(callid, null); //need to wait for ACK break; } if (req.equals("BYE")) { reply(cd, cmd, 200, "OK", false, false); iface.onBye(this, callid); // setCallDetails(callid, null); //need to wait for ACK break; } if (req.equals("OPTIONS")) { reply(cd, cmd, 405, "Method Not Allowed", false, false); break; } if (req.equals("NOTIFY")) { reply(cd, cmd, 200, "OK", false, false); for (int a = 0; a < msg.length; a++) { //look for double \r\n (which appears as an empty line) that marks end of SIP header if (msg[a].length() == 0) { //send the rest of packet as content of NOTIFY event String content = ""; for (int b = a + 1; b < msg.length; b++) { content += msg[b]; content += "\r\n"; } String event = getHeader("Event:", msg); if (event == null) event = getHeader("o:", msg); iface.onNotify(this, callid, event, content); break; } } break; } if (req.equals("ACK")) { SDP sdp = getSDP(msg); if (cd.dst.sdp == null) { cd.dst.sdp = sdp; } iface.onAck(this, callid, sdp); if (cmd.equals("BYE")) { setCallDetails(callid, null); } break; } break; case 100: iface.onTrying(this, callid); break; case 180: iface.onRinging(this, callid); break; case 181: //call if being forwarded //ignore for now break; case 183: case 200: if (cmd.equals("REGISTER")) { if (type == 183) break; //not used in REGISTER command if (cd.src.expires > 0) { if (localhost_changed) { JFLog.log("localhost change detected, reregister()ing"); reregister(); break; } registered = true; iface.onRegister(this, true); } else { registered = false; //iface.onRegister() ??? } } else if (cmd.equals("INVITE")) { cd.dst.sdp = getSDP(msg); //update cd.src.to tag value cd.src.to = cd.dst.to; cd.src.epass = null; cd.src.routelist = cd.dst.routelist; //RFC 2543 6.29 if (type == 200) issue(cd, "ACK", false, true); iface.onSuccess(this, callid, cd.dst.sdp, type == 200); } else if (cmd.equals("BYE")) { if (type == 183) break; //not used in BYE command //call leg ended setCallDetails(callid, null); } break; case 202: if (cmd.equals("REFER")) { iface.onRefer(this, callid); } break; case 401: case 407: if (cd.authsent) { JFLog.log("Server Error : Double " + type); } else { String src_to[] = cd.src.to; cd.src.to = cd.dst.to; //update to (tag may have been added) issue(cd, "ACK", false, true); cd.src.to = src_to; cd.authstr = getHeader("WWW-Authenticate:", msg); if (cd.authstr == null) { cd.authstr = getHeader("Proxy-Authenticate:", msg); } if (cd.authstr == null) { JFLog.log("err:401/407 without Authenticate tag"); break; } cd.src.epass = getAuthResponse(cd, auth, pass, remotehost, cmd, (type == 401 ? "Authorization:" : "Proxy-Authorization:")); if (cd.src.epass == null) { JFLog.log("err:gen auth failed"); break; } cd.src.cseq++; //update contact info cd.src.contact = "<sip:" + user + "@" + cd.localhost + ":" + getlocalport() + ">"; issue(cd, cmd, cmd.equals("INVITE"), true); cd.authsent = true; } break; case 403: cd.src.epass = null; cd.src.cseq = cd.dst.cseq; issue(cd, "ACK", false, true); if (cmd.equals("REGISTER")) { //bad password iface.onRegister(this, false); } else { iface.onCancel(this, callid, type); } break; case 404: //no one there case 486: //busy if (cd.dst.to != null) cd.src.to = cd.dst.to; cd.src.epass = null; cd.src.cseq = cd.dst.cseq; issue(cd, "ACK", false, true); iface.onCancel(this, callid, type); setCallDetails(callid, null); break; default: //treat all other codes as a cancel if (cd.dst.to != null) cd.src.to = cd.dst.to; cd.src.epass = null; cd.src.cseq = cd.dst.cseq; issue(cd, "ACK", false, true); iface.onCancel(this, callid, type); // setCallDetails(callid, null); //might not be done break; } } catch (Exception e) { JFLog.log(e); } } private int getlocalport() { if (rport != -1) return rport; else return localport; } public static void setEnableRport(boolean state) { use_rport = state; } public static void setEnableReceived(boolean state) { use_received = state; } /** Returns the raw SDP from onSuccess() event */ public String getSDP(String callid) { CallDetails cd = getCallDetails(callid); buildsdp(cd, cd.dst); return cd.sdp; } public String[] getHeaders(String callid) { CallDetails cd = getCallDetails(callid); return cd.headers; } }