Java tutorial
/** * Copyright 2014 OpenVCX openvcx@gmail.com * * You may not use this file except in compliance with the OpenVCX License. * The OpenVCX License is based on the Apache Version 2.0 License with * additional credit attribution clauses mentioned in section 4 (e) and * 4 (f). * * 4 (e) Redistributions in source or binary form must reproduce the * aforementioned copyright notice, list of conditions and any * disclaimers in the documentation and/or other materials provided * with the distribution. * * (f) All advertising materials mentioning features or use of this * software must display the following acknowledgement: * "This product includes software from OpenVCX". * * You may obtain a copy of the Apache License, Version 2.0 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 com.openvcx.webcall; import com.openvcx.util.*; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.io.PrintWriter; import java.io.InputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileNotFoundException; import java.io.OutputStream; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.SecureRandom; import java.util.Iterator; import java.util.Properties; import java.util.Random; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; //import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; /** * */ public class ConferenceCreateServlet extends HttpServlet { private final Logger logger = Logger.getLogger(getClass()); private static final String CONFIG_CONFERENCE_DEF = "conference.definition.dir"; private static final String CONFIG_CONFERENCE_TEMPLATE = "conference.definition.template"; private static final String CONFIG_CONFERENCE_PROVISION_AUTO = "conference.definition.provision.auto"; private static final String CONFIG_CONFERENCE_TEMP_DIR = "conference.temp.dir"; private static final String CONFIG_MEDIAPORTAL_LISTENER_ADDRESS = "mediaportal.listener.address"; private static final String CONFIG_MEDIAPORTAL_LISTENER_ADDRESS_LEGACY = "mediaportal.listener.address"; private static final String CONFERNCE_CONFIG_DIR = "conferences/"; private static final int CONFERENCE_NUMBER_LENGTH = 8; private static final String CONF_DEF_FILE_SUFFIX = ".conference"; private static final String CONFERENCE_TEMP_DIR = "temp"; private static final String COOKIE_NUMBER_KEY = "sn"; private static final int SECONDS_IN_DAY = 86400; private String m_confDefDir; private String m_confDefDirAuto; private String m_tempDir; private String m_mediaPortalAddress; private boolean m_bAutoProvision = false; private Configuration m_config = null; private SecureRandom m_rand; /** * <i>javax.Servlet</i> overridden initializer method */ @Override public void init(ServletConfig c) throws ServletException { logger.debug("init start"); super.init(c); try { // // Load the config file defined in web.xml 'sip-ngconference.conf' // String configFilePath = getServletContext().getInitParameter("config"); m_config = new Configuration(configFilePath); m_confDefDir = m_config.getString(CONFIG_CONFERENCE_DEF, CONFERNCE_CONFIG_DIR); if (m_confDefDir.charAt(m_confDefDir.length() - 1) != File.separatorChar) { m_confDefDir += File.separatorChar; } m_confDefDirAuto = m_confDefDir + "auto/"; m_tempDir = m_config.getString(CONFIG_CONFERENCE_TEMP_DIR, CONFERENCE_TEMP_DIR); if (m_tempDir.charAt(m_tempDir.length() - 1) != File.separatorChar) { m_tempDir += File.separatorChar; } if (null == (m_mediaPortalAddress = m_config.getString(CONFIG_MEDIAPORTAL_LISTENER_ADDRESS, null))) { // Try to lookup the legacy naming convention if the user has somehow retained // an old sip-ngconference.conf m_mediaPortalAddress = m_config.getString(CONFIG_MEDIAPORTAL_LISTENER_ADDRESS_LEGACY, null); } m_bAutoProvision = m_config.getBoolean(CONFIG_CONFERENCE_PROVISION_AUTO, m_bAutoProvision); logger.info("confDefDirAuto: '" + m_confDefDirAuto + "', tempDir: '" + m_tempDir + "', mediaPortal: '" + m_mediaPortalAddress + ", provision.auto: " + m_bAutoProvision); m_rand = new SecureRandom(); } catch (Exception e) { LogUtil.printError(logger, "init: ", e); throw new ServletException(e); } logger.debug("init done"); } /** * <i>javax.Servlet</i> overridden cleanup method */ @Override public void destroy() { logger.debug("destroy"); super.destroy(); } private void copyFile(String strFrom, String strTo, boolean bAppend) throws FileNotFoundException, IOException { InputStream in = new FileInputStream(new File(strFrom)); OutputStream out = new FileOutputStream(new File(strTo), bAppend); byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } in.close(); out.close(); } private synchronized String createConferenceNumber() { int index = 0; String strResult = ""; while (index < CONFERENCE_NUMBER_LENGTH) { BigInteger bi = new BigInteger(4 * 8, m_rand); String str = bi.toString(); int end = Math.min(CONFERENCE_NUMBER_LENGTH - index, str.length()); str = str.substring(0, end); strResult += str; index += str.length(); } return strResult; } private boolean isConferenceDefinitionExists(String number) { String pathDef = m_confDefDir + number + CONF_DEF_FILE_SUFFIX; String pathDefAuto = m_confDefDirAuto + number + CONF_DEF_FILE_SUFFIX; if (new File(pathDef).canRead() || new File(pathDefAuto).canRead()) { return true; } return false; } /** * <p>Creates a conference definition file according to the <i>strRequestedNumber</i> </p> * <p>If <i>strRequestedNumber</i> is null then a random SIP URI phone number will be automatically created.</p> * <p>A conference definition file will be created for the SIP URI phone number</p> * @param strRequestedNumber The requested SIP URI phone number or null */ private String createConferenceDefinition(String strRequestedNumber) throws IOException { int indexTry = 0; String templateFile = m_config.getString(CONFIG_CONFERENCE_TEMPLATE); String templatePath = m_confDefDir + templateFile; String outputCreatePath = null; String strOutputNumber = null; if (null != strRequestedNumber && strRequestedNumber.length() > 0) { if (strRequestedNumber.length() < CONFERENCE_NUMBER_LENGTH) { // Treat it as an invalid / non-secure requested number logger.warn( "Conference definition requested number length is too short: '" + strRequestedNumber + "'"); return null; } strOutputNumber = strRequestedNumber; if (true == isConferenceDefinitionExists(strRequestedNumber)) { // // The requested number conference definition file already exists // outputCreatePath = null; } else { outputCreatePath = m_confDefDirAuto + strRequestedNumber + CONF_DEF_FILE_SUFFIX; } } if (null == strOutputNumber && null == outputCreatePath) { // // Create a unique pseudo-random SIP URI phone number // do { strOutputNumber = createConferenceNumber(); if (true == isConferenceDefinitionExists(strOutputNumber)) { //TODO: check conflict at root conference/ if (indexTry++ > 20) { throw new IOException("Failed to create unique conference definition file after " + indexTry + "attempts"); } continue; } } while (false); outputCreatePath = m_confDefDirAuto + strOutputNumber + CONF_DEF_FILE_SUFFIX; } if (true == m_bAutoProvision) { if (null != outputCreatePath) { copyFile(templatePath, outputCreatePath, false); logger.debug("Copied auto-provisioned conference template: '" + templatePath + "' -> '" + outputCreatePath + "'"); } else { logger.debug( "Using existing auto-provisioned conference definition number: '" + strOutputNumber + "'"); } } else { logger.debug("Assigned non-provisioned conference number: " + strOutputNumber); } return strOutputNumber; } private void redirect(HttpServletResponse response, String location) throws IOException { response.setStatus(302); response.setHeader("Location", location); response.setHeader("Connection", "close"); PrintWriter out; response.setContentType("text/html"); out = response.getWriter(); out.println("<html><head><meta http-equiv=\"refresh\"content=\"1; URL=" + location + "\"><head>></html>"); out.close(); } private void redirect2(HttpServletResponse response, String location) throws IOException { PrintWriter out; response.setContentType("text/html"); out = response.getWriter(); // Can't embed media web portal link into iframe due to browser cross-site scripting restrictions // per different domain / port. StringBuffer sb = new StringBuffer("<html><head><title>test</title></head><body>" + "<div id=\"mplayerdiv\" style=\"width:100%; height:100%; margin: 0px 0px 0px 0px; " + "padding: 0px 0px 0px 0px;\">" + "<iframe id=\"mplayerframe\" name=\"mplayerframe\" src=\"") .append(location) .append("\" frameborder=\"0\" width=\"100%\" height=\"100%\">" + "<p>Your browser does not support iframes. <a href=\"") .append(location).append("\">").append(location) .append("</a></p></iframe>" + "</div></body></html>"); out.println(sb.toString()); out.close(); } private String getUriDirSegment(String uri, int segmentIndex) { if (null == uri || 0 == uri.length()) { return null; } while ('/' == uri.charAt(0)) { uri = uri.substring(1); } String[] uris = uri.split("/"); if (segmentIndex < uris.length) { return uris[segmentIndex]; } return null; } private String getUriAfterSegment(String uri, int segmentIndex) { StringBuffer remaining = new StringBuffer(); while ('/' == uri.charAt(0)) { uri = uri.substring(1); } String[] uris = uri.split("/"); while (segmentIndex < uris.length) { remaining.append('/' + uris[segmentIndex++]); } return remaining.toString(); } /** * <p>Lookup a client conference number stored in a cookie. If no phone number is provided by the client a random SIP URI phone number is automatically generated.</p> * <p>A conference definition template file is used to create the conference definition for the phone number.</p> * <p>The auto-assigned phone number is then stored in a cookie and returned to the client.</p> * @param out standard output Output writer * @param request The HTTP request object * @param response The HTTP response object */ private boolean doCreateNumber(PrintWriter out, HttpServletRequest request, HttpServletResponse response) throws IOException { String strOutputNumber = null; Cookie[] arrCookies = request.getCookies(); if (null != arrCookies) { for (Cookie cookie : arrCookies) { //logger.debug("cookie name: " + cookie.getName() + ", path: " + cookie.getPath() + ", domain: " + cookie.getDomain() + ", maxAge: " + cookie.getMaxAge() + ", value: " + cookie.getValue()); if (COOKIE_NUMBER_KEY.equals(cookie.getName())) { if (null != (strOutputNumber = cookie.getValue()) && strOutputNumber.length() == 0) { strOutputNumber = null; } logger.debug("Using cookie stored conference output number: '" + strOutputNumber + "'."); break; } } } strOutputNumber = createConferenceDefinition(strOutputNumber); if (null != strOutputNumber) { int cookieAgeDays = 7; Cookie cookie = new Cookie(COOKIE_NUMBER_KEY, strOutputNumber); cookie.setMaxAge(cookieAgeDays * SECONDS_IN_DAY); cookie.setPath("/" + getUriDirSegment(request.getRequestURI(), 0) + "/"); logger.debug("Setting cookie " + COOKIE_NUMBER_KEY + "=" + strOutputNumber); response.addCookie(cookie); out.println("number=" + strOutputNumber); } return true; } private String getLiveAddress(File file) throws IOException, FileNotFoundException { BufferedReader br = new BufferedReader( new InputStreamReader((new FileInputStream(file)), Charset.forName("UTF-8"))); String connectAddress = null; String line; while ((line = br.readLine()) != null) { connectAddress = line; if (connectAddress.length() < 7 || false == "http:".equalsIgnoreCase(connectAddress.substring(0, 5))) { connectAddress = "http://" + connectAddress; } logger.debug("Found conference live address mapping file '" + file.getAbsolutePath() + "' -> '" + connectAddress + "'"); break; } br.close(); return connectAddress; } private String getMediaPortalAddress(String dir, String file) { StringBuffer connectAddress; boolean haveQ = false; connectAddress = new StringBuffer("http://" + m_mediaPortalAddress); if (null != dir && dir.length() > 0) { connectAddress.append((false == haveQ ? "?" : "") + "dir=" + dir + "&"); haveQ = true; } if (null != file && file.length() > 0) { connectAddress.append((false == haveQ ? "?" : "") + "file=" + file); haveQ = true; } return connectAddress.toString(); } private String getRecordAddress(File file) throws IOException, FileNotFoundException { if (null == m_mediaPortalAddress || m_mediaPortalAddress.length() == 0) { logger.warn(CONFIG_MEDIAPORTAL_LISTENER_ADDRESS + " (Media Portal) Address not set in configuration"); return null; } BufferedReader br = new BufferedReader( new InputStreamReader((new FileInputStream(file)), Charset.forName("UTF-8"))); String connectAddress = null; String line; while ((line = br.readLine()) != null) { String recordingDir = getUriDirSegment(line.trim(), 0); // path relative to media portal root dir //String recordingPath = getUriAfterSegment(line, 1); // path relative to media portal root dir String recordingPath = line.trim(); // path relative to media portal root dir connectAddress = getMediaPortalAddress(recordingDir, recordingPath); logger.debug("Found conference static address mapping file '" + file.getAbsolutePath() + "' -> '" + connectAddress + "'"); break; } br.close(); return connectAddress; } private boolean redirectLiveMediaLink(PrintWriter out, String number, HttpServletResponse response) throws FileNotFoundException, IOException { String path = null; try { String connectAddress = null; path = m_tempDir + number + ".live"; File file = new File(path); if (true == file.canRead() && null != (connectAddress = getLiveAddress(file))) { String link = connectAddress + "/live"; link += "?id=" + number; logger.info("Redirecting link request to live '" + link + "' obtained from '" + path + "'"); redirect(response, link); return true; } } catch (FileNotFoundException e) { //LogUtil.printError(logger, "Conference address mapping file path: '" + path + "' not found", e); logger.warn("Conference live address mapping file path '" + path + "' not found."); } return false; } private boolean redirectRecordedMediaLink(PrintWriter out, String number, HttpServletResponse response) throws FileNotFoundException, IOException { String path = null; try { String connectAddress = null; path = m_tempDir + number + ".record"; File file = new File(path); if (file.canRead() && null != (connectAddress = getRecordAddress(file))) { String link = connectAddress; logger.info("Redirecting link request to recorded '" + link + "' obtained from '" + path + "'"); redirect(response, link); return true; } } catch (FileNotFoundException e) { //LogUtil.printError(logger, "Conference address mapping file path: '" + path + "' not found", e); logger.warn("Conference port recorded mapping file path '" + path + "' not found."); } return false; } private boolean redirectRecordedListingLink(PrintWriter out, String number, HttpServletResponse response) throws IOException { String link = getMediaPortalAddress(number, null); logger.info( "Redirecting link request to media listing '" + link + "' obtained for number: '" + number + "'"); redirect(response, link); return true; } /** * <p>Constructs a hyperlink which can be used to view the live or archived media content of the client.</p> * @param out standard output Output writer * @param request The HTTP request object * @param response The HTTP response object */ private void doGetMediaLink(PrintWriter out, String uri, HttpServletResponse response) throws FileNotFoundException, IOException { String number = getUriDirSegment(uri, 0); boolean bProcessed = false; try { if (null != number && number.length() > 0 && false == number.contains("..")) { if (false == (bProcessed = redirectLiveMediaLink(out, number, response)) && false == (bProcessed = redirectRecordedMediaLink(out, number, response))) { logger.debug("Conference live or recorded address mapping file not found for number: '" + number + "'. Redirecting to media listing"); bProcessed = redirectRecordedListingLink(out, number, response); //TODO return media web portal linke regardless if .record is found } } else { logger.error("Conference address mapping request has invalid number: '" + number + "'."); } } catch (FileNotFoundException e) { LogUtil.printError(logger, "Conference address mapping file not found for number: '" + number + "'.", e); } if (false == bProcessed) { response.setStatus(404); response.setContentType("text/html"); response.setHeader("Connection", "close"); logger.error("Conference address mapping file not found for number: '" + number + "', returning 404."); } } /** * * <p>HTTP GET method handler</p> * <p>The handler services the following URLS:</> * <p><i>/createNumber</i> - To create a conference definition file</p> * <p>Lookup a client conference number stored in a cookie. If no phone number is provided by the client a random SIP URI phone number is automatically generated.</p> * <p>A conference definition template file is used to create the conference definition for the phone number.</p> * <p>The auto-assigned phone number is then stored in a cookie and returned to the client.</p> * <p><i>/link</i> or <i>/l</i> - </p> * <p>Constructs a hyperlink which can be used to view the live or archived media content of the client.</p> * @param request The HTTP request object * @param response The HTTP response object */ @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.debug("webcall doGet '" + request.getRequestURI() + "'"); // Write the output html PrintWriter out = response.getWriter(); try { String method = getUriDirSegment(request.getRequestURI(), 2); if ("createNumber".equalsIgnoreCase(method)) { doCreateNumber(out, request, response); } else if ("link".equalsIgnoreCase(method) || "l".equalsIgnoreCase(method)) { doGetMediaLink(out, getUriAfterSegment(request.getRequestURI(), 3), response); } } catch (Exception e) { LogUtil.printError(logger, "doGet: ", e); } out.close(); } }