CVP Call Studio - FTPS_Client Element
This “FTPS_Client” element is based off of the stock FTP_Client element.
FTP often fails security scans - even if its locked down, uses an anonymous user and is only use for uploading Audio files. To get around this issue - FTPS is a similar solution, which can run on the CVP Servers IIS Servers without any 3rd party applications and uses TLS to encrypt the FTP traffic, i.e. FTPS is FTP over TLS. Note - FTPS is NOT the same protocol as SFTP.
Configuration of this custom Call Studio FTPS element is the very similar to the stock FTP element, but does lack some of the functionality such as allowing different usernames, passwords and port at a per host level. This FTPS element allows you to set multiple servers, a username/password the same one for all hosts and it will always TCP Port 990 to connect to the FTPS Server using implicit FTPS. It can also be configured to delete the original file if it has been copied successfully to all servers (just like the FTP_Client element can do).
i.e. in most instances - it can be a simple drop-in replacement of the standard FTP_client element with this custom FTPS_Client element.
Install & Configuration of FTPS Server on IIS
- Install FTP Server Role
- Server Manager → Add Roles → Server Roles → Web Server → FTP Server (check)
- Configure FTPS
- Right Click on Default Web Site and Add FTP Site Publishing
- Select Radio button “Require SSL and Select Certificate” - note you might have to create a Self Signed Cert if one does not exist (or has expired) - Cisco CVP create one - but it only lasts 12 months. Or you can get a Internal CA signed cert. Note if using a Self signed cert - make sure it is not expired.
- Select Authentication “Anonymous” and Allow Access to : “Anonymous users” and Check Permissions “Read” and “Write” (or set up as you require) - if using Anonymous also make sure the FTP server is only accessible from CVP Call Server A and CVP Call Server B (in FTP IP Address and Domain Restrictions).
- After the above is configured - go into “Edit Bindings” in Default Web Site and change the FTP port from 21 to 990
- To allow FTP have the correct rights to upload files to the IIS Web Server - add the user “IUSR” to the folder C:\inetpub\wwwroot and configure it for “Full Control”.
JAR Files
-
- the custom jar file which contains the FTPS_Client
This custom FTPS jar file utilities Java Class from org.apache.commons.net.ftp.FTPSClient and hence requires the commons-net.jar to be loaded - however this JAR already included by default with Cisco CVP and so does NOT need to be added to the VXML server, Here is where it is located by default:
For info, the Apache Commons Net library contains a collection of network utilities and protocol implementations. Supported protocols include Echo, Finger, FTP, NNTP, NTP, POP3(S), SMTP(S), Telnet, and Whois.
Configuration of Call Studio
- purplepi-ftps-v1.jar file must be uploaded to the C:\Cisco\CallStudio\eclipse\plugins\com.audiumcorp.studio.library.common\lib\ folder so that the Call Studio adds this element to the GUI.
- Restart Call Studio
- Where to find the new custom element?
- How to configure the FTPS_Client Element?
Where to place the JAR file
You can upload globally for all CVP VXML applications can use this element, i.e. common library. This means you do NOT need to include these this JAR file for each application that requires it OR you can upload at the Call Studio application level as detailed below. Do one OR the other!
Upload to Common Lib
Upload to C:\Cisco\CVP\VXMLServer\common\lib\
After copying the JAR to this location - you do not need to restart the CVP VXML Server. Simply run the updateCommonClasses.bat
Note on running updateCommonClasses.bat
Below is an extract from the CVP VXML and Call Studio End User Guide. Reference: https://www.cisco.com/c/en/us/td/docs/voice_ip_comm/cust_contact/contact_center/customer_voice_portal/12-6-2/user/guide/ccvp_b_1262-user-guide-for-cisco-unified-cvp-vxml-server-and-call-studio.pdf
extract from CVP document
When performing an application update, all the data and Java classes related to an application will be reloaded. Java classes placed in the common folder of VXML Server are not included in the application update. VXML Server provides a separate administrative function to update the common folder. There are a few items to note about this function:
- The update affects all applications that use classes in the common folder, so running this function could affect applications that have not changed. Therefore, take precaution when running this function.
- The update affects all classes in the common folder, whether they were changed or not. This is usually not a issue unless those classes contain information in them that reloading would reset (such as static variables).
- Due to the fact that this function reloads classes that affect all applications, and those classes may themselves prompt the loading of configuration files from each application that uses those classes, the function may take some time to complete depending on the number of classes in the common folder and the number and complexity of the deployed applications.
- Changes are immediate, and are not done. Because this potentially affects all applications, the administrator must be aware of this.
OR at a Application Level
- If you prefer NOT to have this JAR file in the common folder, include it in the application lib folder of any application that uses this FTPS_Client element.
Source
- FTPS.class
/* * Author: Gerry O'Rourke, similar to Cisco's standard FTP element to uses FTPS */ package ie.purplepi.cisco.cvp.callstudio; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import org.apache.commons.net.ftp.FTPSClient; import com.audium.server.AudiumException; import com.audium.server.session.DecisionElementData; import com.audium.server.session.ElementAPI; import com.audium.server.voiceElement.DecisionElementBase; import com.audium.server.voiceElement.ElementData; import com.audium.server.voiceElement.ElementException; import com.audium.server.voiceElement.ElementInterface; import com.audium.server.voiceElement.ExitState; import com.audium.server.voiceElement.Setting; import com.audium.server.xml.DecisionElementConfig; public class FTPS extends DecisionElementBase implements ElementInterface { public final String doDecision(String name, DecisionElementData data) throws AudiumException { DecisionElementConfig config = data.getDecisionElementConfig(); //String sourceFilePath = data.getSessionData("sourceFilePath").toString(); String filename = config.getSettingValue("filename", (ElementAPI) data); data.addToLog("filename", filename); if (filename == null || filename.isEmpty()) { data.addToLog("filename", "filename not set. Exiting with Error"); return "error"; } String remote_filename = config.getSettingValue("remote_filename", (ElementAPI) data); if (remote_filename != null && !remote_filename.isEmpty()) { data.addToLog("remote_filename_is_empty", "false"); data.addToLog("remote_filename", remote_filename); }else { File file = new File(filename); String sourceFileName = file.getName(); remote_filename = sourceFileName; data.addToLog("remote_filename_is_empty", "true"); //because remote_filename is NOT set - we will use the source filename data.addToLog("remote_filename", remote_filename); } String username = config.getSettingValue("ftps_user", (ElementAPI) data); data.addToLog("username", username); if (username == null || username.isEmpty()) { data.addToLog("username", "username not set. Use anonymous if not using a username. Exiting with Error."); return "error"; } String password = config.getSettingValue("ftps_password", (ElementAPI) data); if (password != null && !password.isEmpty()) { char firstLetterPassword = password.charAt(0); data.addToLog("password", firstLetterPassword + "*******"); }else { data.addToLog("password", "empty password been used"); } String ftps_host = config.getSettingValue("ftps_hosts", (ElementAPI) data); if (ftps_host == null || ftps_host.isEmpty()) { data.addToLog("ftps_host", "FTPS Server not set. At least one FTP Server hostname or IP Address must be set."); return "error"; } data.addToLog("ftps_host", ftps_host); String[] hosts = ftps_host.trim().split("\\s+"); String semicolonSeparated = ""; for (int i = 0; i < hosts.length; i++) { semicolonSeparated += hosts[i]; if (i < hosts.length - 1) { semicolonSeparated += ";"; } } data.addToLog("ftps_host semicolon seperated", semicolonSeparated); String ftps_path = config.getSettingValue("ftps_path", (ElementAPI) data); data.addToLog("ftps_path", ftps_path); String ftpsFilePath = ""; if (ftps_path != null && !ftps_path.isEmpty() && ftps_path.charAt(ftps_path.length() - 1) == '/') { ftpsFilePath = ftps_path + remote_filename; }else if (ftps_path == null || ftps_path.isEmpty()) { ftpsFilePath = remote_filename; }else { ftpsFilePath = ftps_path + "/" + remote_filename; } String delete_file_on_success_string = config.getSettingValue("delete_file_on_success", (ElementAPI) data); boolean delete_file_on_success = Boolean.parseBoolean(delete_file_on_success_string); data.addToLog("delete_file_on_success", Boolean.toString(delete_file_on_success)); // Call Function to upload file to one SFTP server String sftpResponse = ""; int SuccessCount = 0; for (int i = 0; i < hosts.length; i++) { sftpResponse = uploadFileToFTPS(hosts[i], filename, ftpsFilePath, username, password, data); if (sftpResponse.equalsIgnoreCase("Success")) { SuccessCount ++; } } if (SuccessCount == hosts.length) { data.addToLog("Upload Status", "All uploads Successfully"); if (delete_file_on_success ) { File fileToDelete = new File(filename); if (fileToDelete.delete()) { data.addToLog("File Delete", "Successful"); } else { data.addToLog("File Delete", "Failed"); } } return "done"; }else if(SuccessCount == 0) { data.addToLog("Upload Status", "All uploads Failed"); return "error"; }else { data.addToLog("Upload Status", "Only some uploads Successfully"); return "partial_success"; } } /** * This method returns the name the decision element will have in the Element * Pane in the Audium Builder for Studio. */ public String getElementName() { return "FTPS_Client"; } /** * This method returns the name of the folder in which this decision element * resides. Return null if it is to appear directly under the Elements folder. */ public String getDisplayFolderName() { return "Integration"; } /** * This method returns the text of a description of the decision element that * will appear as a popup when the cursor points to the element. */ public String getDescription() { return "Transfers a file to one or more FTPS servers."; } /** * This method returns an array of Setting objects representing all the settings * this decision element expects. Return null if the decision element does not * need any settings. */ public Setting[] getSettings() throws ElementException { Setting[] settings = { new Setting("filename", "Name of file to be transferred", "Full path of file to be transferred.", true, true, true, 3), new Setting("remote_filename", "Remote Filename", "FTPS server target filename. If not specified, the filename used will be the same as the input filename.", false, true, true, 3), new Setting("ftps_hosts", "FTPS Server(s)", "List of FTPS server host names or IP addresses that the file will be transferred to. " + NEW_LINE + "Each FTPS server entry delimited by a space character. Using TCP Port 990 (implicit FTPS) to connect to each FTPS Server.", true, false, true, 3), new Setting("ftps_user", "Default Username", "Username to use when transferring the file." + NEW_LINE + "If left blank, \"anonymous\" will be assumed. This must be set if there is a default password.", false, true, true, 3), new Setting("ftps_password", "Default Password", "Password to use when transferring the file.", false, true, true, 3), new Setting("ftps_path", "FTPS Path", "Directory on FTPS server in which to transfer the file. Use the forward slash as directory delimiter e.g. 'dir/subdir'. Directory will be created if it does not currently exist.", false, true, true, 3), new Setting("delete_file_on_success", "Delete file if successful", "Delete the original file after it has been successfully transferred to all FTP Server(s).", false, true, true, 4) }; // settings[0].setCustomValidator(new WindowsFilePathValidator()) // settings[1].setCustomValidator(new FilenameValidator()); // settings[2].setCustomValidator(new ServerListValidator()); settings[0].setDefaultValue(""); settings[1].setDefaultValue(""); settings[2].setDefaultValue(""); settings[3].setDefaultValue("anonymous"); // settings[3].setCustomValidator(new UsernameValidator()); settings[4].setDefaultValue(""); settings[5].setDefaultValue(""); // settings[5].setCustomValidator(new FTPPathValidator()); settings[6].setDefaultValue("true"); return settings; } /** * This method returns an array of ExitState objects representing all the * possible exit states the decision element can return. There must be at least * one exit state. */ public ExitState[] getExitStates() throws ElementException { return new ExitState[] { new ExitState("done", "done", "This exit state means there the file was successfully transferred to all FTP Server(s)."), new ExitState("partial_success", "partial_success", "This exit state is used when not all ftp transfers were successful."), new ExitState("error", "error", "This exit state is used if an error occurred and the file was not transferred to any FTP Server(s).") }; } public ElementData[] getElementData() throws ElementException { return new ElementData[] { new ElementData("failed_servers", "File was not transferred to these FTP Server(s)."), new ElementData("failed_servers_reasons", "Reason why FTPS transfers failed."), new ElementData("failed_servers_count", "Number of failed FTPS transfers.") }; } public String uploadFileToFTPS(String ftpsServer, String sourceFilePath, String ftpsFilePath, String username, String password, DecisionElementData data) throws AudiumException { FTPSClient ftpsClient = new FTPSClient("TLS", true); FileInputStream fis = null; try { data.addToLog("Connecting to FTPS Server", ftpsServer); // ftpsClient.addProtocolCommandListener(new PrintCommandListener(new // PrintWriter(System.out), true)); // System.out.println("Connecting to FTPS Server: " + ftpsServer); ftpsClient.connect(ftpsServer, 990); ftpsClient.enterLocalPassiveMode(); ftpsClient.login(username, password); // Enable UTF-8 (optional, some servers require it) //ftpsClient.setControlEncoding("UTF-8"); ftpsClient.execPROT("P"); // **Enable encryption for data channel** ftpsClient.execPBSZ(0); // **Set protection buffer size** // Set binary mode for file transfer (match TYPE I) //ftpsClient.setFileType(FTPSClient.BINARY_FILE_TYPE); int replyCode = ftpsClient.getReplyCode(); if (!ftpsClient.isConnected()) { data.addToLog("Failed to connect. Reply code", Integer.toString(replyCode)); return ("Fail"); } File file = new File(sourceFilePath); data.addToLog("sourceFilePath", sourceFilePath); String remoteFilePath = ftpsFilePath; // String fileName = file.getName(); data.addToLog("remoteFilePath", remoteFilePath); fis = new FileInputStream(file); // System.out.println("Uploading file: " + file.getName()); // boolean uploaded = ftpsClient.storeFile(remoteFilePath, fis); boolean uploaded = ftpsClient.storeFile(remoteFilePath, fis); if (uploaded) { data.addToLog("SFTP outcome", "Successfully Uploaded"); return ("Success"); } else { data.addToLog("SFTP outcome", "File upload failed."); return ("Fail"); } } catch (IOException e) { // System.err.println("Error: " + e.getMessage()); data.addToLog("Error: ", e.getMessage()); //throw new AudiumException("Error", e); return ("Fail"); } finally { try { if (fis != null) fis.close(); if (ftpsClient.isConnected()) { ftpsClient.logout(); ftpsClient.disconnect(); } } catch (IOException e) { data.addToLog("Error closing connection", e.getMessage()); return ("Fail"); } } } }




