package javaforce.voip;
import java.net.*;
import java.util.*;
import javaforce.*;
/**
* Base class for SIP communications. Opens the UDP port and passes any received
* packets thru the SIPInterface.
* Direct Known subclasses : SIPClient, SIPServer.
* RFC 3261 (2543) - SIP
* See also:
* http://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml#sip-parameters-2
*/
public abstract class SIP {
public static class Packet {
public byte data[];
public int length;
public int port;
public String host;
}
public enum Transport {UDP, TCP, TLS};
private Worker worker;
private SIPInterface iface;
private boolean active = true;
private String rinstance;
private String tupleid;
private Random r = new Random();
private boolean server;
protected SIPTransport transport;
protected static String useragent = "JavaForce/" + JF.getVersion();
/**
* Opens the transport and sets the SIPInterface callback.
*/
protected boolean init(int port, SIPInterface iface, boolean server, Transport type) throws Exception {
rinstance = null;
this.iface = iface;
this.server = server;
switch (type) {
case UDP:
transport = new SIPUDPTransport();
break;
case TCP:
if (server)
transport = new SIPTCPTransportServer();
else
transport = new SIPTCPTransportClient();
break;
case TLS:
if (server)
transport = new SIPTLSTransportServer();
else
transport = new SIPTLSTransportClient();
break;
}
if (!transport.open(port)) return false;
worker = new Worker();
worker.start();
return true;
}
/**
* Closes the UDP port and frees resources.
*/
protected void uninit() {
if (transport == null) {
return;
}
active = false;
transport.close();
try {
worker.join();
} catch (Exception e) {
}
transport = null;
worker = null;
}
/**
* Sends a packet out on the UDP port.
*/
protected boolean send(InetAddress remote, int remoteport, String datastr) {
byte data[] = datastr.getBytes();
return transport.send(data, 0, data.length, remote, remoteport);
}
/**
* Splits a To: or From: field in a SIP message into parts.
*/
public static String[] split(String x) {
//x = "display name" <sip:user@host ;... > ;...
//return: [0] [1] [2] [flgs1][:][flgs2]
if (x == null) {
return new String[] {"null", "null", "null"};
}
ArrayList<String> parts = new ArrayList<String>();
int i1, i2;
String x1, x2;
i1 = x.indexOf('<');
if (i1 == -1) {
parts.add("");
x1 = x;
x2 = "";
} else {
if (i1 == 0) {
parts.add("Unknown Name");
} else {
parts.add(x.substring(0, i1).trim().replaceAll("\"", ""));
}
i1++;
i2 = x.substring(i1).indexOf('>');
if (i2 == -1) {
return null;
}
x1 = x.substring(i1, i1 + i2);
x2 = x.substring(i1 + i2 + 1).trim();
}
i1 = x1.indexOf(':');
if (i1 == -1) {
return null;
}
x1 = x1.substring(i1 + 1); //remove sip:
i1 = x1.indexOf('@');
if (i1 == -1) {
parts.add(""); //no user
} else {
parts.add(x1.substring(0, i1).trim()); //userid
x1 = x1.substring(i1 + 1).trim();
}
if ((x1.length() > 0) && (x1.charAt(0) == ';')) {
x1 = x1.substring(1);
}
do {
i1 = x1.indexOf(';');
if (i1 == -1) {
x1 = x1.trim();
if (x1.length() > 0) {
parts.add(x1);
}
break;
}
parts.add(x1.substring(0, i1).trim());
x1 = x1.substring(i1 + 1).trim();
} while (true);
if (parts.size() == 2) {
parts.add(""); //no host ???
}
parts.add(":"); //this seperates fields outside of <>
if ((x2.length() > 0) && (x2.charAt(0) == ';')) {
x2 = x2.substring(1);
}
do {
i1 = x2.indexOf(';');
if (i1 == -1) {
x2 = x2.trim();
if (x2.length() > 0) {
parts.add(x2);
}
break;
}
parts.add(x2.substring(0, i1).trim());
x2 = x2.substring(i1 + 1).trim();
} while (true);
String ret[] = new String[parts.size()];
for (int a = 0; a < parts.size(); a++) {
ret[a] = parts.get(a);
}
return ret;
}
/**
* Joins a To: or From: field after it was split into parts.
*/
public static String join(String x[]) {
//x = "display name" <sip:user@host ;... > ;...
//return: [0] [1] [2] [...][:][...]
if (x == null) {
return "\"null\"<sip:null@null>";
}
StringBuffer buf = new StringBuffer();
if (x[0].length() > 0) {
buf.append('\"');
buf.append(x[0]);
buf.append('\"');
buf.append('<');
}
buf.append("sip:");
if (x[1].length() > 0) {
buf.append(x[1]);
buf.append('@');
}
buf.append(x[2]);
int i = 3;
for (; (i < x.length) && (!x[i].equals(":")); i++) {
buf.append(';');
buf.append(x[i]);
}
i++; //skip ':' seperator
if (x[0].length() > 0) buf.append('>');
for (; i < x.length; i++) {
buf.append(';');
buf.append(x[i]);
}
return buf.toString();
}
/**
* Returns a flag in a To: From: field.
*/
public static String getFlag2(String fields[], String flg) {
flg += "=";
int i;
for (i = 0; i < fields.length; i++) {
if (fields[i].equals(":")) {
break;
}
}
if (i == fields.length) {
return "";
}
i++;
for (; i < fields.length; i++) {
if (fields[i].startsWith(flg)) {
return fields[i].substring(flg.length());
}
}
return ""; //do not return null
}
/**
* Sets/adds a flag in a To: From: field.
*/
public static String[] setFlag2(String fields[], String flg, String value) {
flg += "=";
boolean seperator = false;
for (int i = 3; i < fields.length; i++) {
if (!seperator) {
if (fields[i].equals(":")) {
seperator = true;
}
continue;
}
if (fields[i].startsWith(flg)) {
fields[i] = flg + value;
return fields;
}
}
//need to add an element to fields and append "flg=value"
String newfields[] = new String[fields.length + 1];
for (int j = 0; j < fields.length; j++) {
newfields[j] = fields[j];
}
newfields[fields.length] = flg + value;
return newfields;
}
/**
* Returns a random SIP branch id.
* TODO : Implement RFC 3261 section 8.1.1.7 (z9hG4bK)
*/
public String getbranch() {
return String.format("z123456-y12345-%x%x-1--d12345-", r.nextInt(), r.nextInt());
}
/** Returns branch in first Via line */
protected String getbranch(String msg[]) {
String vias[] = getvialist(msg);
if (vias == null || vias.length == 0) return null;
//Via: SDP/2.0/UDP host:port;branch=...;rport;...
String f[] = vias[0].split(";");
for(int a=0;a<f.length;a++) {
if (f[a].startsWith("branch=")) {
return f[a].substring(7);
}
}
return null;
}
/**
* Determines if a SIP message is on hold.
*/
protected boolean ishold(String msg[]) {
//does msg contain "a=sendonly"?
for (int a = 0; a < msg.length; a++) {
if (msg[a].equalsIgnoreCase("a=sendonly")) {
return true;
}
}
return false;
}
/**
* Returns the Via: list in a SIP message as an array.
*/
protected String[] getvialist(String msg[]) {
ArrayList<String> vialist = new ArrayList<String>();
for (int a = 0; a < msg.length; a++) {
String ln = msg[a];
if (ln.regionMatches(true, 0, "Via:", 0, 4)) {
vialist.add(ln);
continue;
}
if (ln.regionMatches(true, 0, "v:", 0, 2)) {
vialist.add(ln);
continue;
}
}
return vialist.toArray(new String[0]);
}
/**
* Returns the Record-Route: list in a SIP message as an array.
*/
protected String[] getroutelist(String msg[]) {
ArrayList<String> routelist = new ArrayList<String>();
for (int a = 0; a < msg.length; a++) {
String ln = msg[a];
if (ln.regionMatches(true, 0, "Record-Route:", 0, 13)) {
routelist.add("Route:" + ln.substring(13));
continue;
}
}
return routelist.toArray(new String[0]);
}
/**
* Returns a random generated rinstance id.
*/
protected String getrinstance() {
if (rinstance != null) {
return rinstance;
}
rinstance = String.format("%x%x", r.nextInt(), r.nextInt());
return rinstance;
}
/**
* Returns a random generated tuple id.
*/
protected String gettupleid() {
if (tupleid != null) {
return tupleid;
}
tupleid = String.format("%08x", r.nextInt());
return tupleid;
}
/**
* Returns the URI part of a SIP message.
*/
protected String geturi(String msg[]) {
//cmd uri SIP/2.0\r\n
int idx1 = msg[0].indexOf(' ');
if (idx1 == -1) {
return null;
}
int idx2 = msg[0].substring(idx1 + 1).indexOf(' ');
if (idx2 == -1) {
return null;
}
return msg[0].substring(idx1 + 1).substring(0, idx2);
}
/**
* Returns a random generated tag for the To: or From: parts of a SIP message.
* This function is used by replacetag() so it must resemble a To: or From:
* field.
*/
public static String generatetag() {
Random r = new Random();
return String.format("null<sip:null@null>;tag=%x%x", r.nextInt(), r.nextInt());
}
/**
* Replaces the 'tag' field from 'newfield' into 'fields'.
*/
public static String[] replacetag(String fields[], String newfield) {
//x = "display name" <sip:user@host;tag=...>;tag=...
// [0] [1] [2] [...] [:][...]
if (newfield == null) {
return fields;
}
String newfields[] = split(newfield);
int oldtagidx = -1;
boolean seperator = false;
for (int i = 3; i < fields.length; i++) {
if (!seperator) {
if (fields[i].equals(":")) {
seperator = true;
}
continue;
}
if (fields[i].startsWith("tag=")) {
oldtagidx = i;
break;
}
}
seperator = false;
for (int i = 3; i < newfields.length; i++) {
if (!seperator) {
if (newfields[i].equals(":")) {
seperator = true;
}
continue;
}
if (newfields[i].startsWith("tag=")) {
if (oldtagidx != -1) {
fields[oldtagidx] = newfields[i];
return fields;
} else {
//need to add an element to fields and append newfields[i]
String retfields[] = new String[fields.length + 1];
for (int j = 0; j < fields.length; j++) {
retfields[j] = fields[j];
}
retfields[fields.length] = newfields[i];
return retfields;
}
}
}
return fields;
}
/**
* Removes the 'tag' field from 'fields'.
*/
public static String[] removetag(String fields[]) {
boolean seperator = false;
for (int i = 3; i < fields.length; i++) {
if (!seperator) {
if (fields[i].equals(":")) {
seperator = true;
}
continue;
}
if (fields[i].startsWith("tag=")) {
//remove fields[i]
String newfields[] = new String[fields.length - 1];
for (int j = 0; j < i; j++) {
newfields[j] = fields[j];
}
for (int j = i + 1; j < fields.length; j++) {
newfields[j - 1] = fields[j];
}
return newfields;
}
}
return fields; //no tag found
}
/**
* Returns the 'tag' field from 'fields'.
*/
public static String gettag(String fields[]) {
boolean seperator = false;
for (int i = 3; i < fields.length; i++) {
if (!seperator) {
if (fields[i].equals(":")) {
seperator = true;
}
continue;
}
if (fields[i].startsWith("tag=")) {
return fields[i].substring(4);
}
}
return null; //no tag found
}
/**
* Returns a random callid for a SIP message (a unique id for each call, not
* to be confused with caller id).
*/
public String getcallid() {
return String.format("%x%x", r.nextInt(), System.currentTimeMillis());
}
/**
* Returns current time in seconds.
*/
protected long getNow() {
return System.currentTimeMillis() / 1000;
}
/**
* Returns a random nonce variable used in SIP authorization.
*/
protected String getnonce() {
return String.format("%x%x%x%x", r.nextInt(), r.nextInt(), System.currentTimeMillis(), r.nextInt());
}
/**
* Returns string name of codec based on payload id (except dynamic ids 96-127).
*/
private static String getCodecName(int id) {
switch (id) {
case 0:
return "PCMU";
case 8:
return "PCMA";
case 9:
return "G722";
case 26:
return "JPEG";
case 18:
return "G729";
case 34:
return "H263";
}
return "?";
}
/**
* Parses the SDP content.
*/
public static SDP getSDP(String msg[]) {
String type = getHeader("Content-Type:", msg);
if (type == null) type = getHeader("c:", msg); //short form
if (type == null || type.indexOf("application/sdp") == -1) return null;
SDP sdp = new SDP();
SDP.Stream stream = null;
int idx;
int start = -1;
for(int a=0;a<msg.length;a++) {
if (msg[a].length() == 0) {start = a+1; break;}
}
if (start == -1) {
JFLog.log("SIP.getSDP() : No SDP found");
return null;
}
int acnt = 1;
int vcnt = 1;
for(int a=start;a<msg.length;a++) {
String ln = msg[a];
if (ln.startsWith("c=")) {
//c=IN IP4 1.2.3.4
idx = ln.indexOf("IP4 ");
if (idx == -1) {JFLog.log("SIP.getSDP() : Unsupported c field:" + ln); continue;}
String ip = ln.substring(idx+4);
if (stream == null) {
sdp.ip = ip;
} else {
stream.ip = ip;
}
} else if (ln.startsWith("m=")) {
//m=audio <port> RTP/<profile> <codecs>
if (stream != null) {
if (stream.content == null) {
switch (stream.type) {
case audio: stream.content = "audio" + (acnt++); break;
case video: stream.content = "video" + (vcnt++); break;
}
}
}
if (ln.startsWith("m=audio")) {
stream = sdp.addStream(SDP.Type.audio);
} else if (ln.startsWith("m=video")) {
stream = sdp.addStream(SDP.Type.video);
} else {
JFLog.log("SIP.getSDP() : Unsupported m field:" + ln);
stream = sdp.addStream(SDP.Type.other);
continue;
}
//parse static codecs
String f[] = ln.split(" ");
String p[] = f[2].split("/");
if (p[1].equals("AVP")) {
stream.profile = SDP.Profile.AVP;
} else if (p[1].equals("AVPF")) {
stream.profile = SDP.Profile.AVPF;
} else if (p[1].equals("SAVP")) {
stream.profile = SDP.Profile.SAVP;
} else if (p[1].equals("SAVPF")) {
stream.profile = SDP.Profile.SAVPF;
} else {
stream.profile = SDP.Profile.UNKNOWN;
JFLog.log("SIP.getSDP() : Unsupported profile:" + p[1]);
}
stream.port = JF.atoi(f[1]);
for(int b=3;b<f.length;b++) {
int id = JF.atoi(f[b]);
if (id < 96) {
stream.addCodec(new Codec(getCodecName(id), id));
}
}
} else if (ln.startsWith("a=")) {
if (ln.startsWith("a=rtpmap:")) {
//a=rtpmap:<id> <name>/<bitrate>
String f[] = ln.substring(9).split(" ");
int id = JF.atoi(f[0]);
String n[] = f[1].split("/");
if (id >= 96) {
stream.addCodec(new Codec(n[0], id));
}
}
else if (ln.startsWith("a=sendrecv")) {
if (stream != null) {
stream.mode = SDP.Mode.sendrecv;
}
}
else if (ln.startsWith("a=sendonly")) {
if (stream != null) {
stream.mode = SDP.Mode.sendonly;
}
}
else if (ln.startsWith("a=recvonly")) {
if (stream != null) {
stream.mode = SDP.Mode.sendonly;
}
}
else if (ln.startsWith("a=inactive")) {
if (stream != null) {
stream.mode = SDP.Mode.inactive;
}
}
else if (ln.startsWith("a=content:")) {
stream.content = ln.substring(10);
}
else if (ln.startsWith("a=candidate:")) {
// 0 1 2 3 4 5 6 7 8 9 10 11
//a=candidate:0 1 UDP 2128609535 10.1.1.100 60225 typ host
//a=candidate:1 1 UDP 1692467199 x.x.x.x 60225 typ srflx raddr 10.1.1.100 rport 60225
//a=candidate:0 2 UDP 2128609534 10.1.1.100 60226 typ host
//a=candidate:1 2 UDP 1692467198 x.x.x.x 60226 typ srflx raddr 10.1.1.100 rport 60226
String f[] = ln.substring(12).split(" ");
if (stream != null && f.length >= 8 && f[0].equals("0") && f[1].equals("1")) {
//override ip
stream.ip = f[4];
}
}
else if (ln.startsWith("a=ice-ufrag:")) {
sdp.iceufrag = ln.substring(12);
}
else if (ln.startsWith("a=ice-pwd:")) {
sdp.icepwd = ln.substring(10);
}
else if (ln.startsWith("a=fingerprint:sha-256 ")) {
sdp.fingerprint = ln.substring(22);
}
else if (ln.startsWith("a=crypto:")) {
//SRTP Keys (replaced by DTLS method)
//a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
// # crypto base64_key_salt life mki
stream.keyExchange = SDP.KeyExchange.SDP;
String f[] = ln.split(" ");
if (!f[2].startsWith("inline:")) {
JFLog.log("a=crypto:bad keys(1)");
continue;
}
String base64 = f[2].substring(7);
int pipe = base64.indexOf("|");
if (pipe != -1) {
base64 = base64.substring(0, pipe);
}
byte keys[] = javaforce.Base64.decode(base64.getBytes());
if (keys == null || keys.length != 30) {
JFLog.log("a=crypto:bad keys(2)");
continue;
}
byte key[] = Arrays.copyOfRange(keys, 0, 16);
byte salt[] = Arrays.copyOfRange(keys, 16, 16 + 14);
stream.addKey(f[1], key, salt);
}
}
}
if ((stream != null) && (stream.content == null)) {
switch (stream.type) {
case audio: stream.content = "audio" + (acnt++); break;
case video: stream.content = "video" + (vcnt++); break;
}
}
return sdp;
}
/**
* Determines if codecs[] contains codec.
* NOTE:This checks the name field, not the id which could by dynamic.
*/
public static boolean hasCodec(Codec codecs[], Codec codec) {
for (int a = 0; a < codecs.length; a++) {
if (codecs[a].name.equals(codec.name)) {
return true;
}
}
return false;
}
/**
* Adds a codec to a list of codecs.
*/
public static Codec[] addCodec(Codec codecs[], Codec codec) {
Codec newCodecs[] = new Codec[codecs.length + 1];
for (int a = 0; a < codecs.length; a++) {
newCodecs[a] = codecs[a];
}
newCodecs[codecs.length] = codec;
return newCodecs;
}
/**
* Removes a codec from a list of codecs.
*/
public static Codec[] delCodec(Codec codecs[], Codec codec) {
if (!hasCodec(codecs, codec)) {
return codecs;
}
Codec newCodecs[] = new Codec[codecs.length - 1];
int pos = 0;
for (int a = 0; a < codecs.length; a++) {
if (codecs[a].name.equals(codec.name)) {
continue;
}
newCodecs[pos++] = codecs[a];
}
return newCodecs;
}
/**
* Returns a codec from a list of codecs. Comparison is done by name only. The
* returned codec 'id' may be different than provided codec.
*/
public static Codec getCodec(Codec codecs[], Codec codec) {
for (int a = 0; a < codecs.length; a++) {
if (codecs[a].name.equals(codec.name)) {
return codecs[a];
}
}
return null;
}
/**
* Returns the requested operation of a SIP message. (INVITE, BYE, etc.)
*/
protected String getRequest(String msg[]) {
int idx = msg[0].indexOf(" ");
if (idx == -1) {
return null;
}
return msg[0].substring(0, idx);
}
/**
* Returns the response number from a SIP reply message. (100, 200, 401, etc.)
*/
protected int getResponseType(String msg[]) {
if (msg[0].length() < 11) {
return -1; //bad msg
}
if (!msg[0].regionMatches(true, 0, "SIP/2.0 ", 0, 8)) {
return -1; //not a response
} //SIP/2.0 ### ...
return Integer.valueOf(msg[0].substring(8, 11));
}
/**
* Returns a specific header (field) from a SIP message.
*/
public static String getHeader(String header, String msg[]) {
int sl = header.length();
for (int a = 0; a < msg.length; a++) {
String ln = msg[a];
if (ln.length() < sl) {
continue;
}
if (ln.substring(0, sl).equalsIgnoreCase(header)) {
return ln.substring(sl).trim().replaceAll("\"", "");
}
}
return null;
}
/**
* Returns a set of specific headers (fields) from a SIP message.
*/
public static String[] getHeaders(String header, String msg[]) {
ArrayList<String> lst = new ArrayList<String>();
int sl = header.length();
for (int a = 0; a < msg.length; a++) {
String ln = msg[a];
if (ln.length() < sl) {
continue;
}
if (ln.substring(0, sl).equalsIgnoreCase(header)) {
lst.add(ln.substring(sl).trim().replaceAll("\"", ""));
}
}
return lst.toArray(new String[0]);
}
/**
* Returns the cseq of a SIP message.
*/
protected int getcseq(String msg[]) {
String cseqstr = getHeader("CSeq:", msg);
if (cseqstr == null) {
return -1;
}
String parts[] = cseqstr.split(" ");
return Integer.valueOf(parts[0]);
}
/**
* Returns the command at the end of the cseq header in a SIP message.
*/
protected String getcseqcmd(String msg[]) {
String cseqstr = getHeader("CSeq:", msg);
if (cseqstr == null) {
return null;
}
String parts[] = cseqstr.split(" ");
if (parts.length < 2) {
return null;
}
return parts[1];
}
/**
* Generates a response to a SIP authorization challenge.
*/
protected String getResponse(String user, String pass, String realm, String cmd, String uri, String nonce, String qop, String nc, String cnonce) {
MD5 md5 = new MD5();
String H1 = user + ":" + realm + ":" + pass;
md5.init();
md5.add(H1.getBytes(), 0, H1.length());
H1 = new String(md5.byte2char(md5.done()));
String H2 = cmd + ":" + uri;
md5.init();
md5.add(H2.getBytes(), 0, H2.length());
H2 = new String(md5.byte2char(md5.done()));
String H3 = H1 + ":" + nonce + ":";
if ((qop != null) && (qop.length() > 0)) {
H3 += nc + ":" + cnonce + ":" + qop + ":";
}
H3 += H2;
md5.init();
md5.add(H3.getBytes(), 0, H3.length());
return new String(md5.byte2char(md5.done()));
}
/** Split an Authenticate line into parts. */
private String[] split(String in, char delimit) {
ArrayList<String> strs = new ArrayList<String>();
boolean inquote = false;
char ca[] = in.toCharArray();
int p1 = 0, p2 = 0;
for(int a=0;a<ca.length;a++) {
char ch = ca[a];
if (ch == delimit && !inquote) {
strs.add(in.substring(p1,p2).trim());
p2++;
p1 = p2;
continue;
} else if (ch == '\"') {
inquote = !inquote;
}
p2++;
}
if (p2 > p1) {
strs.add(in.substring(p1, p2).trim());
}
/*
System.out.println("auth=" + in);
for(int a=0;a<strs.size();a++) {
System.out.println("str[]=" + strs.get(a));
}
*/
return strs.toArray(new String[strs.size()]);
}
/**
* Generates a complete header response to a SIP authorization challenge.
*/
protected String getAuthResponse(CallDetails cd, String user, String pass, String remote, String cmd, String header) {
//request = ' Digest algorithm=MD5, realm="asterisk", nonce="value", etc.'
String request = cd.authstr;
if (!request.regionMatches(true, 0, "Digest ", 0, 7)) {
JFLog.log("err:no digest");
return null;
}
String tags[] = split(request.substring(7), ',');
String auth, nonce = null, qop = null, cnonce = null, nc = null,stale = null;
String realm = null;
auth = getHeader("algorithm=", tags);
if (auth != null) {
if (!auth.equalsIgnoreCase("MD5")) {
JFLog.log("err:only MD5 auth supported");
return null;
} //unsupported auth type
}
realm = getHeader("realm=", tags);
nonce = getHeader("nonce=", tags);
qop = getHeader("qop=", tags); //auth or auth-int
stale = getHeader("stale=", tags); //true|false ???
if (nonce == null) {
JFLog.log("err:no nonce");
return null;
} //no nonce found
if (realm == null) {
JFLog.log("err:no realm");
return null;
} //no realm found
if (qop != null) {
String qops[] = qop.split(","); //server could provide multiple options
qop = null;
for (int a = 0; a < qops.length; a++) {
if (qops[a].trim().equals("auth")) {
qop = "auth";
break;
}
}
if (qop != null) {
//generate cnonce and nc
cnonce = getnonce();
if (cd.nonce != null && cd.nonce.equals(nonce)) {
cd.nonceCount++;
} else {
cd.nonceCount = 1;
}
nc = String.format("%08x", cd.nonceCount);
}
}
cd.nonce = nonce;
String response = getResponse(user, pass, realm, cmd, "sip:" + remote, nonce, qop, nc, cnonce);
StringBuffer ret = new StringBuffer();
ret.append(header);
ret.append(" Digest username=\"" + user + "\", realm=\"" + realm + "\", uri=\"sip:" + remote + "\", nonce=\"" + nonce + "\"");
if (cnonce != null) {
ret.append(", cnonce=\"" + cnonce + "\"");
}
//NOTE:Do NOT quote qop or nc
if (qop != null) {
ret.append(", nc=" + nc);
ret.append(", qop=" + qop);
}
ret.append(", response=\"" + response + "\"");
ret.append(", algorithm=MD5\r\n");
return ret.toString();
}
/**
* Returns the remote RTP host in a SIP/SDP packet.
*/
protected String getremotertphost(String msg[]) {
String c = getHeader("c=", msg);
if (c == null) {
return null;
}
int idx = c.indexOf("IP4 ");
if (idx == -1) {
return null;
}
return c.substring(idx + 4);
}
/**
* Returns the remote RTP port in a SIP/SDP packet.
*/
protected int getremotertpport(String msg[]) {
// m=audio PORT RTP/AVP ...
String m = getHeader("m=audio ", msg);
if (m == null) {
return -1;
}
int idx = m.indexOf(' ');
if (idx == -1) {
return -1;
}
return Integer.valueOf(m.substring(0, idx));
}
/**
* Returns the remote Video RTP port in a SIP/SDP packet.
*/
protected int getremoteVrtpport(String msg[]) {
// m=video PORT RTP/AVP ...
String m = getHeader("m=video ", msg);
if (m == null) {
return -1;
}
int idx = m.indexOf(' ');
if (idx == -1) {
return -1;
}
return Integer.valueOf(m.substring(0, idx));
}
/**
* Returns the 'o' counts in a SIP/SDP packet. idx can be 1 or 2.
*/
protected long geto(String msg[], int idx) {
//o=blah o1 o2 ...
String o = getHeader("o=", msg);
if (o == null) {
return 0;
}
String os[] = o.split(" ");
return Long.valueOf(os[idx]);
}
/**
* Returns "expires" field from SIP headers.
*/
public int getexpires(String msg[]) {
//check Expires field
String expires = getHeader("Expires:", msg);
if (expires != null) {
return JF.atoi(expires);
}
//check Contact field
String contact = getHeader("Contact:", msg);
if (contact == null) {
contact = getHeader("c:", msg);
}
if (contact == null) {
return -1;
}
String tags[] = split(contact);
expires = getHeader("expires=", tags);
if (expires == null) {
return -1;
}
return JF.atoi(expires);
}
public abstract String getlocalRTPhost(CallDetails cd);
/**
* Builds SDP packet. (RFC 2327)
*/
public void buildsdp(CallDetails cd, CallDetails.SideDetails cdsd) {
//build SDP content
SDP sdp = cdsd.sdp;
String ip = sdp.ip;
if (ip == null) {
ip = getlocalRTPhost(cd);
}
StringBuffer content = new StringBuffer();
content.append("v=0\r\n");
content.append("o=- " + cdsd.o1 + " " + cdsd.o2 + " IN IP4 " + cd.localhost + "\r\n");
content.append("s=" + useragent + "\r\n");
content.append("c=IN IP4 " + ip + "\r\n");
content.append("t=0 0\r\n");
if (sdp.iceufrag != null) content.append("a=ice-ufrag:" + sdp.iceufrag + "\r\n");
if (sdp.icepwd != null) content.append("a=ice-pwd:" + sdp.icepwd + "\r\n");
if (sdp.fingerprint != null) content.append("a=fingerprint:sha-256 " + sdp.fingerprint + "\r\n");
for(int a=0;a<sdp.streams.length;a++) {
SDP.Stream stream = sdp.streams[a];
if (stream.codecs.length == 0) continue;
Codec rfc2833 = getCodec(stream.codecs, RTP.CODEC_RFC2833);
content.append("m=" + stream.getType() + " " + stream.port + " RTP/" + stream.profile);
for(int b=0;b<stream.codecs.length;b++) {
content.append(" " + stream.codecs[b].id);
}
if (stream.type == SDP.Type.audio && rfc2833 == null) {
rfc2833 = RTP.CODEC_RFC2833;
content.append(" " + rfc2833.id);
}
content.append("\r\n");
if (stream.keyExchange == SDP.KeyExchange.SDP && stream.keys != null) {
for(int c=0;c<stream.keys.length;c++) {
SDP.Key keys = stream.keys[c];
byte key_salt[] = new byte[16 + 14];
System.arraycopy(keys.key, 0, key_salt, 0, 16);
System.arraycopy(keys.salt, 0, key_salt, 16, 14);
String keystr = new String(javaforce.Base64.encode(key_salt));
//keys | lifetime | mki:length
String ln = keys.crypto + " inline:" + keystr; // + "|2^20"; // + "|1:32";
content.append("a=crypto:" + (c+1) + " ");
content.append(ln);
content.append("\r\n");
}
}
if (stream.content != null) {
content.append("a=content:" + stream.content + "\r\n");
}
content.append("a=" + stream.getMode() + "\r\n");
if (stream.ip != null) {
content.append("c=IN IP4 " + stream.ip + "\r\n");
}
content.append("a=ptime:20\r\n");
if (hasCodec(stream.codecs, RTP.CODEC_G711u)) {
content.append("a=rtpmap:0 PCMU/8000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_G711a)) {
content.append("a=rtpmap:8 PCMA/8000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_G722)) {
content.append("a=rtpmap:9 G722/8000\r\n"); //NOTE:It's really 16000 but an error in RFC claims it as 8000
}
if (hasCodec(stream.codecs, RTP.CODEC_G729a)) {
content.append("a=rtpmap:18 G729/8000\r\n");
content.append("a=fmtp:18 annexb=no\r\n");
content.append("a=silenceSupp:off - - - -\r\n");
}
if (stream.type == SDP.Type.audio) {
content.append("a=rtpmap:" + rfc2833.id + " telephone-event/8000\r\n");
content.append("a=fmtp:" + rfc2833.id + " 0-15\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_JPEG)) {
content.append("a=rtpmap:26 JPEG/90000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_H263)) {
content.append("a=rtpmap:34 H263/90000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_H263_1998)) {
content.append("a=rtpmap:" + getCodec(stream.codecs, RTP.CODEC_H263_1998).id + " H263-1998/90000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_H263_2000)) {
content.append("a=rtpmap:" + getCodec(stream.codecs, RTP.CODEC_H263_2000).id + " H263-2000/90000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_H264)) {
content.append("a=rtpmap:" + getCodec(stream.codecs, RTP.CODEC_H264).id + " H264/90000\r\n");
}
if (hasCodec(stream.codecs, RTP.CODEC_VP8)) {
content.append("a=rtpmap:" + getCodec(stream.codecs, RTP.CODEC_VP8).id + " VP8/90000\r\n");
}
if (stream.keyExchange == SDP.KeyExchange.DTLS) {
content.append("a=rtcp-mux"); //http://tools.ietf.org/html/rfc5761
}
}
cd.sdp = content.toString();
}
private static HashMap<String, String> dnsCache = new HashMap<String, String>();
/**
* Resolve hostname to IP address. Keeps a cache to improve performance.
*/
public static String resolve(String host) {
//uses a small DNS cache
//TODO : age and delete old entries (SIP servers should always have static IPs so this is not critical)
String ip = dnsCache.get(host);
if (ip != null) {
return ip;
}
try {
ip = InetAddress.getByName(host).getHostAddress();
} catch (Exception e) {
JFLog.log(e);
return null;
}
JFLog.log("dns:" + host + "=" + ip);
dnsCache.put(host, ip);
return ip;
}
private final int mtu = 1460; //max size of packet
/**
* This thread handles reading incoming SIP packets and dispatches them thru
* SIPInterface.
*/
private class Worker extends Thread {
public void run() {
while (active) {
try {
byte data[] = new byte[mtu];
Packet pack = new Packet();
pack.data = data;
if (!transport.receive(pack)) continue;
if (pack.length <= 4) {
continue; //keep alive
}
String msg[] = new String(data, 0, pack.length).replaceAll("\r", "").split("\n", -1);
if (server) {
WorkerPacket wp = new WorkerPacket(msg, pack.host, pack.port);
wp.start();
} else {
iface.packet(msg, pack.host, pack.port);
}
} catch (Exception e) {
JFLog.log(e);
}
}
}
}
/**
* This thread dispatches SIP packets in a separate thread for server mode.
*/
private class WorkerPacket extends Thread {
String x1[];
String x2;
int x3;
public WorkerPacket(String x1[], String x2, int x3) {
this.x1 = x1;
this.x2 = x2;
this.x3 = x3;
}
public void run() {
iface.packet(x1, x2, x3);
}
}
}