/**
* $Revision $
* $Date $
*
* Copyright (C) 2005-2010 Jive Software. All rights reserved.
*
* 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.jivesoftware.openfire.plugin.ofswitch;
import java.sql.*;
import java.io.File;
import java.util.*;
import java.net.*;
import java.util.concurrent.*;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.jivesoftware.util.*;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.jivesoftware.openfire.cluster.ClusterEventListener;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.database.DbConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jitsi.util.OSUtils;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.freeswitch.esl.client.manager.*;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.ifsoft.websockets.*;
import org.jivesoftware.openfire.sip.sipaccount.*;
import org.xmpp.packet.*;
import org.dom4j.*;
public class OfSwitchPlugin implements Plugin, ClusterEventListener, IEslEventListener, PropertyEventListener {
private static final Logger Log = LoggerFactory.getLogger(OfSwitchPlugin.class);
private FreeSwitchThread freeSwitchThread;
private ExecutorService executor;
private String freeSwitchExePath = null;
private String freeSwitchHomePath = null;
private final String serviceName = "freeswitch";
private static final ScheduledExecutorService connExec = Executors.newSingleThreadScheduledExecutor();
private ManagerConnection managerConnection;
private Client client;
private ScheduledFuture<ConnectThread> connectTask;
private volatile boolean subscribed = false;
private XMPPServer server;
public static OfSwitchPlugin self;
public String getName() {
return "ofswitch";
}
public String getDescription() {
return "OfSwitch Plugin";
}
public void initializePlugin(final PluginManager manager, final File pluginDirectory)
{
ContextHandlerCollection contexts = HttpBindManager.getInstance().getContexts();
self = this;
server = XMPPServer.getInstance();
try {
ClusterManager.addListener(this);
PropertyEventDispatcher.addListener(this);
Log.info("OfMeet Plugin - Initialize websockets ");
ServletContextHandler context = new ServletContextHandler(contexts, "/sip", ServletContextHandler.SESSIONS);
context.addServlet(new ServletHolder(new XMPPServlet()),"/proxy");
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
Log.info("OfSwitch Plugin - Initialize Webservice");
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers2 = new ArrayList<>();
initializers2.add(new ContainerInitializer(new JettyJasperInitializer(), null));
WebAppContext context2 = new WebAppContext(contexts, pluginDirectory.getPath(), "/ofswitch");
context2.setClassLoader(this.getClass().getClassLoader());
context2.setAttribute("org.eclipse.jetty.containerInitializers", initializers2);
context2.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context2.setWelcomeFiles(new String[]{"index.html"});
Log.info("OfSwitch Plugin - Initialize FreeSwitch");
checkNatives(pluginDirectory);
boolean freeswitchEnabled = JiveGlobals.getBooleanProperty("freeswitch.enabled", true);
if (freeswitchEnabled)
{
String freeswitchServer = JiveGlobals.getProperty("freeswitch.server.hostname", "127.0.0.1");
String freeswitchPassword = JiveGlobals.getProperty("freeswitch.server.password", "ClueCon");
boolean freeswitchInstalled = JiveGlobals.getBooleanProperty("freeswitch.installed", true);
freeSwitchHomePath = JiveGlobals.getProperty("freeswitch.server.homepath", freeSwitchHomePath);
freeSwitchExePath = JiveGlobals.getProperty("freeswitch.server.exepath", freeSwitchExePath);
if (freeswitchInstalled == false)
{
if (freeSwitchExePath != null && !"".equals(freeSwitchExePath) && freeSwitchHomePath != null && !"".equals(freeSwitchHomePath))
{
executor = Executors.newCachedThreadPool();
executor.submit(new Callable<Boolean>()
{
public Boolean call() throws Exception {
try {
Log.info("FreeSwitch executable path " + freeSwitchExePath);
freeSwitchThread = new FreeSwitchThread();
freeSwitchThread.start(freeSwitchExePath + " ", new File(freeSwitchHomePath));
}
catch (Exception e) {
Log.error("FreeSwitch initializePluginn", e);
}
return true;
}
});
} else {
Log.error("FreeSwitch path error server " + freeswitchServer + " " + freeSwitchHomePath);
}
}
managerConnection = new DefaultManagerConnection(freeswitchServer, freeswitchPassword);
Client client = managerConnection.getESLClient();
ConnectThread connector = new ConnectThread();
connectTask = (ScheduledFuture<ConnectThread>) connExec.scheduleAtFixedRate(connector, 30, freeswitchInstalled ? 5 : 0, TimeUnit.SECONDS);
}
} catch (Exception e) {
Log.error("Could NOT start openfire switch", e);
}
}
public void destroyPlugin() {
PropertyEventDispatcher.removeListener(this);
try {
if (freeSwitchThread != null)
{
freeSwitchThread.stop();
}
if (executor != null)
{
executor.shutdown();
}
if (connectTask != null)
{
connectTask.cancel(true);
}
ClusterManager.removeListener(this);
} catch (Exception e) {
}
}
public String getDomain()
{
return server.getServerInfo().getXMPPDomain();
}
public String getHostname()
{
return server.getServerInfo().getHostname();
}
public String getIpAddress()
{
String ourHostname = server.getServerInfo().getHostname();
String ourIpAddress = ourHostname;
try {
ourIpAddress = InetAddress.getByName(ourHostname).getHostAddress();
} catch (Exception e) {
}
return ourIpAddress;
}
//-------------------------------------------------------
//
//
//
//-------------------------------------------------------
private class ConnectThread implements Runnable
{
public void run()
{
try {
client = managerConnection.getESLClient();
if (! client.canSend()) {
Log.info("Attempting to connect to FreeSWITCH ESL");
subscribed = false;
managerConnection.connect();
} else {
if (!subscribed) {
Log.info("Subscribing for ESL events.");
client.cancelEventSubscriptions();
client.addEventListener(self);
client.setEventSubscriptions( "plain", "all" );
client.addEventFilter( "Event-Name", "heartbeat" );
client.addEventFilter( "Event-Name", "custom" );
client.addEventFilter( "Event-Name", "channel_callstate" );
client.addEventFilter( "Event-Name", "presence_in" );
client.addEventFilter( "Event-Name", "background_job" );
subscribed = true;
}
}
} catch (InboundConnectionFailure e) {
Log.error("Failed to connect to ESL", e);
}
}
}
private void checkNatives(File pluginDirectory)
{
try
{
String suffix = null;
if(OSUtils.IS_LINUX32)
{
suffix = "linux-32";
}
else if(OSUtils.IS_LINUX64)
{
suffix = "linux-64";
}
else if(OSUtils.IS_WINDOWS32)
{
suffix = "win-32";
}
else if(OSUtils.IS_WINDOWS64)
{
suffix = "win-64";
}
else if(OSUtils.IS_MAC)
{
suffix = "osx-64";
}
if (suffix != null)
{
freeSwitchHomePath = pluginDirectory.getAbsolutePath() + File.separator + "native" + File.separator + suffix;
try {
freeSwitchExePath = freeSwitchHomePath + File.separator + "FreeSwitchConsole";
File file = new File(freeSwitchExePath);
file.setReadable(true, true);
file.setWritable(true, true);
file.setExecutable(true, true);
Log.info("checkNatives freeSwitch executable path " + freeSwitchExePath);
} catch (Exception e) {
freeSwitchExePath = null;
}
} else {
Log.error("checkNatives unknown OS " + pluginDirectory.getAbsolutePath());
}
}
catch (Exception e)
{
Log.error(e.getMessage(), e);
}
}
//-------------------------------------------------------
//
//
//
//-------------------------------------------------------
@Override
public void joinedCluster()
{
Log.info("OfSwitch Plugin - joinedCluster");
}
@Override
public void joinedCluster(byte[] arg0)
{
}
@Override
public void leftCluster()
{
Log.info("OfSwitch Plugin - leftCluster");
}
@Override
public void leftCluster(byte[] arg0)
{
}
@Override
public void markedAsSeniorClusterMember()
{
Log.info("OfSwitch Plugin - markedAsSeniorClusterMember");
}
//-------------------------------------------------------
//
//
//
//-------------------------------------------------------
public void propertySet(String property, Map params)
{
}
public void propertyDeleted(String property, Map<String, Object> params)
{
}
public void xmlPropertySet(String property, Map<String, Object> params) {
}
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
}
//-------------------------------------------------------
//
//
//
//-------------------------------------------------------
@Override public void eventReceived( EslEvent event )
{
String eventName = event.getEventName();
Map<String, String> headers = event.getEventHeaders();
String eventType = headers.get("Event-Subclass");
Log.info("eventReceived " + eventName + " " + eventType);
for (String key : headers.keySet())
{
String value = headers.get(key);
Log.debug("Generic Event parameter, " + key + "=[" + value + "]");
}
if (eventName.equals("PRESENCE_IN"))
{
final String state = headers.get("presence-call-info-state");
final String direction = headers.get("presence-call-direction");
final String presenceId = headers.get("Channel-Presence-ID");
final String source = headers.get("Caller-Caller-ID-Number");
final String destination = headers.get("Caller-Destination-Number");
if (state != null)
{
Log.info("eventReceived " + state + " " + direction + " " + presenceId + " " + source + " " + destination);
Message message = new Message();
message.setFrom(server.getServerInfo().getXMPPDomain());
Element element = message.addChildElement("sippresence", "http://ignitereatime.org/sippresence");
element.addAttribute("state", state);
element.addAttribute("direction", direction);
element.addAttribute("id", presenceId);
element.addAttribute("source", source);
element.addAttribute("destination", destination);
server.getSessionManager().broadcast(message);
}
}
if (eventName.equals("CHANNEL_CALLSTATE"))
{
String callState = headers.get("Channel-Call-State");
if ("HANGUP".equals(callState))
{
final String source = headers.get("Caller-Caller-ID-Number");
final String destination = headers.get("Caller-Destination-Number");
int tempDuration = 0;
long tempStartTimestamp =0;
try {
tempDuration = (int)((Long.parseLong(headers.get("Caller-Channel-Hangup-Time")) - Long.parseLong( headers.get("Caller-Channel-Answered-Time"))) / 1000000);
tempStartTimestamp = Long.parseLong(headers.get("Caller-Profile-Created-Time")) / 1000;
} catch (Exception ex) {}
final int duration = tempDuration;
final long startTimestamp = tempStartTimestamp;
final String direction = headers.get("Caller-Direction");
ExecutorService executorWriteRecord = Executors.newCachedThreadPool();
executorWriteRecord.submit(new Callable<Boolean>()
{
public Boolean call() throws Exception
{
try {
String username = "";
SipAccount sipAccount = SipAccountDAO.getAccountByExtn(source);
if (sipAccount != null) username = sipAccount.getUsername();
createCallRecord(username, source, destination, startTimestamp, duration, "inbound".equals(direction) ? "received" : "dialed");
}
catch (Exception e) {
Log.error("createCallRecord failed", e);
}
return true;
}
});
}
}
if (eventName.equals("CUSTOM") && eventType != null && (eventType.equals("sofia::register") || eventType.equals("sofia::unregister")))
{
final String extension = headers.get("from-user");
final boolean registered = eventType.equals("sofia::register");
ExecutorService executorWriteRecord = Executors.newCachedThreadPool();
executorWriteRecord.submit(new Callable<Boolean>()
{
public Boolean call() throws Exception
{
SipAccount sipAccount = SipAccountDAO.getAccountByExtn(extension);
if (sipAccount != null)
{
IQ iq = new IQ(IQ.Type.set);
String node = sipAccount.getUsername();
if (node.indexOf("@") > -1) node = JID.escapeNode(sipAccount.getUsername());
iq.setFrom(node + "@sipark." + server.getServerInfo().getXMPPDomain());
iq.setTo("sipark." + server.getServerInfo().getXMPPDomain());
Element child = iq.setChildElement("spark", "http://www.jivesoftware.com/protocol/sipark");
child.addElement("status").setText(registered ? "Registered" : "Unregistered");
server.getIQRouter().route(iq);
}
return true;
}
});
}
}
@Override public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event)
{
Integer memberId = getMemberIdFromEvent(event);
String callerId = getCallerIdFromEvent(event);
String callerIdName = getCallerIdNameFromEvent(event);
String uuid = getChannelCallUUIDFromEvent(event);
int members = getConferenceSizeFromEvent(event);
Map<String, String> headers = event.getEventHeaders();
boolean muted = headers.get("Speak").equals("true") ? false : true;
boolean speaking = headers.get("Talking").equals("true") ? true : false;
EslMessage response;
for (String key : headers.keySet())
{
String value = headers.get(key);
Log.info("User joined voice conference, " + key + "=[" + value + "]");
}
}
@Override public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event)
{
Integer memberId = this.getMemberIdFromEvent(event);
String callerId = getCallerIdFromEvent(event);
String callerIdName = getCallerIdNameFromEvent(event);
String uuid = getChannelCallUUIDFromEvent(event);
int members = getConferenceSizeFromEvent(event);
EslMessage response;
Map<String, String> headers = event.getEventHeaders();
for (String key : headers.keySet())
{
String value = headers.get(key);
Log.info("User left voice conference, " + key + "=[" + value + "]");
}
}
@Override public void conferenceEventMute(String uniqueId, String confName, int confSize, EslEvent event)
{
Integer memberId = this.getMemberIdFromEvent(event);
Log.info("User muted voice conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
}
@Override public void conferenceEventUnMute(String uniqueId, String confName, int confSize, EslEvent event)
{
Integer memberId = this.getMemberIdFromEvent(event);
Log.info("User unmuted voice conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
}
@Override public void conferenceEventAction(String uniqueId, String confName, int confSize, String action, EslEvent event)
{
Integer memberId = this.getMemberIdFromEvent(event);
if (action == null) {
return;
}
if (action.equals("start-talking"))
{
Log.info("User started talking voice conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
} else if (action.equals("stop-talking")) {
Log.info("User stopped talking voice conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
} else {
Log.info("Unknown action " + action + ", user=[" + memberId.toString() + "], conf=[" + confName + "]");
}
}
@Override public void conferenceEventTransfer(String uniqueId, String confName, int confSize, EslEvent event)
{
}
@Override public void conferenceEventThreadRun(String uniqueId, String confName, int confSize, EslEvent event)
{
}
@Override public void conferenceEventRecord(String uniqueId, String confName, int confSize, EslEvent event)
{
String action = event.getEventHeaders().get("Action");
Integer memberId = this.getMemberIdFromEvent(event);
if (action == null) {
return;
}
if (action.equals("start-recording"))
{
Log.info("Recording started, conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
} else if (action.equals("stop-recording")) {
Log.info("Recording stopped,conference, user=[" + memberId.toString() + "], conf=[" + confName + "]");
} else {
Log.info("Unknown record action " + action + ", user=[" + memberId.toString() + "], conf=[" + confName + "]");
}
}
@Override public void conferenceEventPlayFile(String uniqueId, String confName, int confSize, EslEvent event)
{
}
@Override public void backgroundJobResultReceived( EslEvent event )
{
}
@Override public void exceptionCaught(ExceptionEvent e)
{
Log.error("exceptionCaught", e);
}
private Integer getMemberIdFromEvent(EslEvent e) {
return new Integer(e.getEventHeaders().get("Member-ID"));
}
private String getCallerIdFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Caller-ID-Number");
}
private String getCallerIdNameFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Caller-ID-Name");
}
private String getRecordFilenameFromEvent(EslEvent e) {
return e.getEventHeaders().get("Path");
}
private String getRecordTimestampFromEvent(EslEvent e) {
return e.getEventHeaders().get("Event-Date-Timestamp");
}
private String getChannelCallUUIDFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Unique-ID");
}
private int getConferenceSizeFromEvent(EslEvent e) {
int members = -1;
try {
members = Integer.parseInt(e.getEventHeaders().get("Conference-Size"));
} catch(Exception error) {}
return members;
}
public String getSessionVar(String uuid, String var)
{
String value = null;
if (client.canSend())
{
EslMessage response = client.sendSyncApiCommand("uuid_getvar", uuid + " " + var);
if (response != null)
{
value = response.getBodyLines().get(0);
}
}
return value;
}
public String sendAsyncFWCommand(String command)
{
Log.info("sendAsyncFWCommand " + command);
String response = null;
if (client != null)
{
response = client.sendAsyncApiCommand(command, "");
}
return response;
}
public EslMessage sendFWCommand(String command)
{
Log.info("sendFWCommand " + command);
EslMessage response = null;
if (client != null)
{
response = client.sendSyncApiCommand(command, "");
}
return response;
}
public String getDeviceIP(String userId)
{
List<String> regLines = sendFWCommand("sofia status profile internal reg").getBodyLines();
boolean foundUser = false;
String ip = null;
for (String line : regLines)
{
if (foundUser && line.startsWith("IP:"))
{
ip = line.substring(4).trim();
break;
}
if (line.startsWith("User:") && line.indexOf(userId + "@") > -1) foundUser = true;
}
return ip;
}
private void createCallRecord(String username, String addressFrom, String addressTo, long datetime, int duration, String calltype)
{
boolean sipPlugin = XMPPServer.getInstance().getPluginManager().getPlugin("sip") != null;
if (sipPlugin)
{
Log.info("createCallRecord " + username + " " + addressFrom + " " + addressTo + " " + datetime);
String sql = "INSERT INTO ofSipPhoneLog (username, addressFrom, addressTo, datetime, duration, calltype) values (?, ?, ?, ?, ?, ?)";
Connection con = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
psmt = con.prepareStatement(sql);
psmt.setString(1, username);
psmt.setString(2, addressFrom);
psmt.setString(3, addressTo);
psmt.setLong(4, datetime);
psmt.setInt(5, duration);
psmt.setString(6, calltype);
psmt.executeUpdate();
} catch (SQLException e) {
Log.error(e.getMessage(), e);
} finally {
DbConnectionManager.closeConnection(rs, psmt, con);
}
}
}
}