package javaforce.voip;
import java.io.*;
import java.net.*;
import java.util.*;
import javaforce.*;
/**
* Handles the server end of a SIP link.
*/
public class SIPServer extends SIP implements SIPInterface {
private int localport;
private String localhost;
private Hashtable<String, CallDetailsServer> cdlist;
private SIPServerInterface iface;
private boolean use_qop = false;
public boolean init(int localport, SIPServerInterface iface, Transport type) {
this.iface = iface;
this.localport = localport;
cdlist = new Hashtable<String, CallDetailsServer>();
try {
JFLog.log("Starting SIP Server on port " + localport);
super.init(localport, this, true, type);
} catch (Exception e) {
JFLog.log("SIPServer:init() failed : " + e);
return false;
}
return true;
}
public void uninit() {
super.uninit();
}
public void enableQOP(boolean state) {
use_qop = state;
}
public CallDetailsServer getCallDetailsServer(String callid) {
CallDetailsServer cd = cdlist.get(callid);
if (cd == null) {
cd = iface.createCallDetailsServer();
cd.sip = this;
cd.callid = callid;
cd.localhost = localhost;
setCallDetailsServer(callid, cd);
}
return cd;
}
public void setCallDetailsServer(String callid, CallDetailsServer cd) {
if (cd == null) {
cdlist.remove(callid);
} else {
cdlist.put(callid, cd);
}
}
public boolean issue(CallDetailsServer cd, String header, boolean sdp, boolean src) {
CallDetails.SideDetails cdsd = (src ? cd.pbxsrc : cd.pbxdst);
JFLog.log("callid:" + cd.callid + "\r\nissue command : " + cd.cmd + " from : " + cd.user + " to : " + cdsd.host + ":" + cdsd.port);
StringBuffer req = new StringBuffer();
req.append(cd.cmd + " " + cdsd.uri + " SIP/2.0\r\n");
req.append("Via: SIP/2.0/UDP " + cd.localhost + ":" + localport + ";branch=" + cdsd.branch + "\r\n");
req.append("Max-Forwards: 70\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 + " " + cd.cmd + "\r\n");
req.append("Allow: INVITE, ACK, CANCEL, BYE, REFER, NOTIFY, OPTIONS\r\n");
req.append("User-Agent: " + useragent + "\r\n");
if (header != null) {
req.append(header);
}
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");
}
if (cdsd.addr == null) {
try {
cdsd.addr = InetAddress.getByName(cdsd.host);
} catch (Exception e) {
JFLog.log(e);
return false;
}
}
return send(cdsd.addr, cdsd.port, req.toString());
}
public boolean reply(CallDetailsServer cd, int code, String msg, String header, boolean sdp, boolean src) {
CallDetails.SideDetails cdsd = (src ? cd.pbxsrc : cd.pbxdst);
JFLog.log("callid:" + cd.callid + "\r\nissue reply : " + code + " to : " + cdsd.host + ":" + cdsd.port);
StringBuffer req = new StringBuffer();
req.append("SIP/2.0 " + code + " " + msg + "\r\n");
if (cdsd.vialist != null) {
for (int a = 0; a < cdsd.vialist.length; a++) {
if (a == 0) {
//add received to first via entry (and rport if requested)
String via = cdsd.vialist[a];
String f[] = via.split(";");
StringBuilder sb = new StringBuilder();
for(int b=0;b<f.length;b++) {
if (f[b].equals("rport")) {
f[b] = "rport=" + cdsd.port;
}
sb.append(f[b]);
sb.append(";");
}
sb.append("received=" + cdsd.host);
req.append(sb.toString());
req.append("\r\n");
} else {
req.append(cdsd.vialist[a]);
req.append("\r\n");
}
}
}
if (code < 400) {
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 + " " + cd.cmd + "\r\n");
req.append("Allow: INVITE, ACK, CANCEL, BYE, REFER, NOTIFY, OPTIONS\r\n");
req.append("User-Agent: " + useragent + "\r\n");
if (header != null) {
req.append(header);
}
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");
}
if (cdsd.addr == null) {
try {
cdsd.addr = InetAddress.getByName(cdsd.host);
} catch (Exception e) {
JFLog.log(e);
return false;
}
}
return send(cdsd.addr, cdsd.port, req.toString());
}
public String getlocalRTPhost(CallDetails cd) {
return cd.localhost;
}
public boolean register(String user, String pass, String remotehost, int remoteport, int expires, String did, String regcallid) {
//NOTE : There is no dst in a register, it's a one-sided call
CallDetailsServer cd = getCallDetailsServer(regcallid);
cd.user = user;
cd.pass = pass;
cd.pbxsrc.expires = expires;
cd.pbxsrc.to = new String[]{user, user, remotehost + ":" + remoteport, ":"};
cd.pbxsrc.from = new String[]{user, user, remotehost + ":" + remoteport, ":"};
cd.pbxsrc.from = replacetag(cd.pbxsrc.from, generatetag());
cd.pbxsrc.contact = "<sip:" + did + "@" + cd.localhost + ":" + localport + ">";
cd.pbxsrc.uri = "sip:" + remotehost; // + ";rinstance=" + getrinstance();
cd.callid = regcallid;
cd.pbxsrc.branch = getbranch();
cd.pbxsrc.cseq++;
cd.cmd = "REGISTER";
cd.src.host = cd.pbxsrc.host = remotehost;
cd.src.port = cd.pbxsrc.port = remoteport;
cd.authsent = false;
boolean ret = issue(cd, null, false, true);
return ret;
}
//copies "some" fields from src to dest
public void clone(CallDetails.SideDetails src, CallDetails.SideDetails dst) {
dst.host = src.host;
dst.port = src.port;
dst.to = src.to.clone();
dst.from = src.from.clone();
dst.uri = src.uri;
dst.cseq = src.cseq;
dst.branch = src.branch;
dst.contact = src.contact;
dst.vialist = src.vialist;
}
public void packet(String msg[], String remoteip, int remoteport) {
try {
String tmp, req = null, epass;
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;
}
CallDetailsServer cd = getCallDetailsServer(callid);
if (cd.localhost == null && !msg[0].startsWith("SIP/")) {
String f[] = msg[0].split(" "); //REQUEST sip:[ext@]HOST[:port] SIP/2.0
String sip = f[1];
if (sip.startsWith("sip:")) {
sip = sip.substring(4);
}
int idx1 = sip.indexOf("@");
if (idx1 == -1) {
idx1 = 0;
} else {
idx1++;
}
int idx2 = sip.indexOf(":");
String host;
if (idx2 == -1) {
host = sip.substring(idx1);
} else {
host = sip.substring(idx1, idx2);
}
cd.localhost = InetAddress.getByName(host).getHostAddress();
JFLog.log("server address=" + cd.localhost);
localhost = cd.localhost;
}
cd.lastPacket = System.currentTimeMillis();
boolean src = false;
CallDetails.SideDetails cdsd = null;
CallDetails.SideDetails cdpbx = null;
//update CallDetailsServer
synchronized (cd.lock) {
if ((cd.src.host == null) && (cd.dst.host == null)) {
//new call leg (assign this side to src)
src = true;
cdsd = cd.src;
cdpbx = cd.pbxsrc;
} else {
if (cd.src.host != null && resolve(cd.src.host).equals(remoteip) && cd.src.port == remoteport) {
src = true;
cdsd = cd.src;
cdpbx = cd.pbxsrc;
} else if (cd.dst.host != null && resolve(cd.dst.host).equals(remoteip) && cd.dst.port == remoteport) {
src = false;
cdsd = cd.dst;
cdpbx = cd.pbxdst;
} else {
JFLog.log("Ignoring packet from unknown host:" + remoteip + ":" + remoteport);
return; //you were not invited to this party
}
}
cdsd.cseq = getcseq(msg);
cdsd.host = remoteip;
cdsd.port = remoteport;
cdsd.branch = getbranch(msg);
//get cd.to
tmp = getHeader("To:", msg);
if (tmp == null) {
tmp = getHeader("t:", msg);
}
cdsd.to = split(tmp);
//get cd.from
tmp = getHeader("From:", msg);
if (tmp == null) {
tmp = getHeader("f:", msg);
}
cdsd.from = split(tmp);
//extract user from cd.from "display" <sip:user@host>;tag=...
cd.user = cdsd.from[1];
//get via list
cdsd.vialist = getvialist(msg);
//set contact
cdsd.contact = "<sip:" + cd.user + "@" + cd.localhost + ":" + localport + ">";
//get uri (it must equal the Contact field)
cdsd.uri = getHeader("Contact:", msg);
if (cdsd.uri == null) {
cdsd.uri = getHeader("m:", msg);
}
if (cdsd.uri != null) {
cdsd.uri = cdsd.uri.substring(1, cdsd.uri.length() - 1); //remove < > brackets
}
cd.cmd = getcseqcmd(msg);
int type = getResponseType(msg);
if (type != -1) {
JFLog.log("callid:" + callid + "\r\nreply=" + type + " from " + remoteip + ":" + remoteport);
} else {
req = getRequest(msg);
JFLog.log("callid:" + callid + "\r\nrequest=" + req + " from " + remoteip + ":" + remoteport);
}
switch (type) {
case -1:
clone(cdsd, cdpbx);
if (req.equalsIgnoreCase("REGISTER")) {
String resln = getHeader("Authorization:", msg);
if (resln == null) {
//send a 401
cd.nonce = getnonce();
String challenge = "WWW-Authenticate: Digest algorithm=MD5, realm=\"jpbx\", nonce=\"" + cd.nonce + "\"";
if (use_qop) {
challenge += ", qop=\"auth\"";
}
challenge += "\r\n";
reply(cd, 401, "REQ AUTH", challenge, false, src);
break;
}
if (!resln.regionMatches(true, 0, "digest ", 0, 7)) {
break;
}
String tags[] = resln.substring(7).replaceAll(" ", "").replaceAll("\"", "").split(",");
String res = getHeader("response=", tags);
String nonce = getHeader("nonce=", tags);
if ((nonce == null) || (cd.nonce == null) || (!cd.nonce.equals(nonce))) {
//send another 401
cd.nonce = getnonce();
String challenge = "WWW-Authenticate: Digest algorithm=MD5, realm=\"jpbx\", nonce=\"" + cd.nonce + "\"";
if (use_qop) {
challenge += ", qop=\"auth\"";
}
challenge += "\r\n";
reply(cd, 401, "REQ AUTH", challenge, false, src);
break;
}
String test = getResponse(cd.user, iface.getPassword(cd.user), "jpbx", cd.cmd, getHeader("uri=", tags), cd.nonce, getHeader("qop=", tags),
getHeader("nc=", tags), getHeader("cnonce=", tags));
cd.nonce = null; //don't allow value to be reused
if (!res.equalsIgnoreCase(test)) {
reply(cd, 403, "BAD PASSWORD", null, false, src);
setCallDetailsServer(callid, null);
break;
}
//REGISTER OK
iface.onRegister(cd.user, getexpires(msg), remoteip, remoteport);
reply(cd, 200, "OK", null, false, src);
setCallDetailsServer(callid, null);
break;
}
if (req.equalsIgnoreCase("INVITE")) {
//BUG : What if call is from same extension but from another PBX
// this will think the INVITE must auth first
// need to check if dest is on this PBX and bypass auth check
String pass = iface.getPassword(cd.user);
if (pass != null) {
//do auth only if has a password
String resln = getHeader("Proxy-Authorization:", msg);
if ((resln == null) || (cd.nonce == null)) {
//send a 407
cd.nonce = getnonce();
String challenge = "Proxy-Authenticate: Digest algorithm=MD5, realm=\"jpbx\", nonce=\"" + cd.nonce + "\"\r\n";
reply(cd, 407, "REQ AUTH", challenge, false, src);
break;
}
if (!resln.regionMatches(true, 0, "digest ", 0, 7)) {
break;
}
String tags[] = resln.substring(7).replaceAll(" ", "").replaceAll("\"", "").split(",");
String res = getHeader("response=", tags);
String nonce = getHeader("nonce=", tags);
if ((nonce == null) || (!cd.nonce.equals(nonce))) {
//send another 407
cd.nonce = getnonce();
String challenge = "Proxy-Authenticate: Digest algorithm=MD5, realm=\"jpbx\", nonce=\"" + cd.nonce + "\"\r\n";
reply(cd, 407, "REQ AUTH", challenge, false, src);
break;
}
String test = getResponse(cd.user, pass, "jpbx", cd.cmd, getHeader("uri=", tags), cd.nonce, null, null, null);
cd.nonce = null; //don't allow value to be reused
if (!res.equalsIgnoreCase(test)) {
reply(cd, 403, "BAD PASSWORD", null, false, src);
setCallDetailsServer(callid, null);
break;
}
iface.onRegister(cd.user, 3600, remoteip, remoteport); //BUG - this assumes expires is 3600
//no break
}
//get dialed # (if INVITE)
cd.dialed = cdsd.to[1];
//split cd.from into parts
cd.fromname = cdsd.from[0];
cd.fromnumber = cdsd.from[1];
//get SDP details
cdsd.sdp = getSDP(msg);
JFLog.log("src=" + cdsd.sdp);
//get o1/o2
cdsd.o1 = geto(msg, 1);
cdsd.o2 = geto(msg, 2);
cd.authorized = (pass != null);
iface.onInvite(cd, src);
break;
}
if (req.equalsIgnoreCase("CANCEL")) {
iface.onCancel(cd, src);
// setCallDetailsServer(callid, null); //still too soon
break;
}
if (req.equalsIgnoreCase("BYE")) {
//BUG : can't delete calldetails yet (memory leak)
iface.onBye(cd, src);
break;
}
if (req.equalsIgnoreCase("ACK")) {
//TODO : ???
break;
}
if (req.equalsIgnoreCase("REFER")) {
iface.onFeature(cd, req, getHeader("Refer-To:", msg), src);
break;
}
if (req.equalsIgnoreCase("OPTIONS")) {
//send 200 and ignore
reply(cd, 200, "OK", null, false, src);
break;
}
if (req.equalsIgnoreCase("SUBSCRIBE")) {
//send 200 and ignore
reply(cd, 200, "OK", null, false, src);
setCallDetailsServer(callid, null);
break;
}
if (req.equalsIgnoreCase("SHUTDOWN")) {
iface.onFeature(cd, req, remoteip, src);
setCallDetailsServer(callid, null);
break;
}
JFLog.log("Unknown command:" + req);
setCallDetailsServer(callid, null);
break;
case 100:
iface.onTrying(cd, src);
break;
case 180:
case 183:
iface.onRinging(cd, src);
break;
case 200:
if (cd.cmd.equals("INVITE")) {
//update tag
cdsd.to = replacetag(cdsd.to, getHeader("To:", msg));
cdsd.to = replacetag(cdsd.to, getHeader("t:", msg));
cdpbx.to = cdsd.to.clone();
cdsd.sdp = getSDP(msg);
cdsd.o1 = geto(msg, 1);
cdsd.o2 = geto(msg, 2);
}
else if (cd.cmd.equals("BYE")) {
setCallDetailsServer(cd.callid, null);
break;
}
else if (cd.cmd.equals("REGISTER")) {
//send ACK and ignore
cd.cmd = "ACK";
issue(cd, null, false, src);
setCallDetailsServer(callid, null);
break;
}
iface.onSuccess(cd, src);
break;
case 401:
if (cd.cmd.equals("REGISTER")) {
cd.cmd = "ACK";
issue(cd, null, false, src);
cd.cmd = "REGISTER";
if (cd.authsent) {
JFLog.log("Server Error : Double 401");
setCallDetailsServer(callid, null);
break;
}
cd.authstr = getHeader("WWW-Authenticate:", msg);
epass = getAuthResponse(cd, cd.user, cd.pass, cdpbx.host, cd.cmd, "Authorization:");
if (epass == null) {
JFLog.log("err:gen auth failed");
setCallDetailsServer(callid, null);
break;
}
cdsd.cseq++;
cd.authsent = true;
issue(cd, epass, false, src);
}
break;
case 407:
//BUG : Should the cdsd.contact be changed to:
// "<sip:" + did + "@" + getlocalhost(null) + ":" + localport + ">";
//like it is in register() above? Because it wouldn't have been in the initial INVITE.
if (cd.cmd.equals("INVITE")) {
cd.cmd = "ACK";
issue(cd, null, false, src);
if (cd.authsent) {
JFLog.log("Server Error : Double 407");
setCallDetailsServer(callid, null);
break;
}
String reg = iface.getTrunkRegister(remoteip); //user : pass @ host / did
if (reg == null) {
JFLog.log("TRUNK : 407 : no register string for trunk");
setCallDetailsServer(callid, null);
break;
}
int idx1 = reg.indexOf(":");
int idx2 = reg.indexOf("@");
if ((idx1 == -1) || (idx2 == -1)) {
JFLog.log("TRUNK : 407 : invalid register string for trunk");
setCallDetailsServer(callid, null);
break;
}
String trunk_user = reg.substring(0, idx1);
String trunk_pass = reg.substring(idx1 + 1, idx2);
cd.authstr = getHeader("Proxy-Authenticate:", msg);
epass = getAuthResponse(cd, trunk_user, trunk_pass, cdpbx.host, cd.cmd, "Proxy-Authorization:");
if (epass == null) {
JFLog.log("err:gen auth failed");
setCallDetailsServer(callid, null);
break;
}
cdsd.cseq++;
cd.authsent = true;
issue(cd, epass, true, src);
}
break;
default:
iface.onError(cd, type, src);
if (type == 487) {
setCallDetailsServer(cd.callid, null); //call canceled
}
break;
}
} //synchronized
} catch (Exception e) {
JFLog.log(e);
}
}
public Enumeration getCalls() {
return cdlist.elements();
}
}