// // savdicl.cs // // Simple demo to illustrate some features of SSSP // Scans a file or dir using a given TCP/IP port // // To build: // Open savdicli.sln or savdicli.csproj in Microsoft Visual Studio 2005 // and choose the menu option: Build | Build Solution. The binary // savdicli.exe will be output to .\bin\Release. // // Copyright (c) 2007 Sophos Plc, www.sophos.com. // using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Net.Sockets; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Diagnostics; namespace SAVDICLI { /// /// Utility class that provides some global functionality. /// public class Utility { /// /// Regex that picks out any non-alphanumeric characters. /// private static Regex m_toencPattern = new Regex("([^0-9a-zA-Z])"); /// /// Regex that picks out any encoded URL escape sequences ('%' followed /// by 2 hexadecimal digits). /// private static Regex m_encPattern = new Regex("%([0-9a-fA-F]{2})"); /// /// A delegate used to replace characters in a string being URL-encoded. /// /// The Match object passed in /// The replacement string private static string UrlEncodeMatchEvaluator(Match match) { return string.Format("%{0:X2}", Convert.ToByte(match.Groups[1].Value[0])); } /// /// This method encodes a string by replacing any non-alphanumeric /// characters with an escape sequence ('%' followed by 2 hexadecimal digits). /// /// The string to be encoded /// The encoded string public static string UrlEncode(string s) { return m_toencPattern.Replace(s, UrlEncodeMatchEvaluator); } /// /// A delegate used to replace escape sequences in a URL-encoded string with /// corresponding characters. /// /// The match object passed in /// The replacement string private static string UrlDecodeMatchEvaluator(Match match) { return "" + long.Parse(match.Groups[1].Value, NumberStyles.HexNumber); } /// /// This method decodes an encoded string by replacing any escape sequences /// with corresponding non-alphanumeric characters. /// /// The string to be decoded /// The decoded string public static string UrlDecode(string s) { return m_encPattern.Replace(s, UrlDecodeMatchEvaluator); } } /// /// This class provides methods to connect to a SAVDI server instance /// and send and receive data. /// public class Session { /// /// The hostname of the server to connect to. /// string m_hostname; /// /// The port on which to connect to the server. /// uint m_port; /// /// Flag that indicates if a TCP connection has been established with the server. /// bool m_connected; /// /// Flag that indicates if the TCP session is active. /// bool m_active; /// /// The TcpClient object that provides the TCP connection. /// TcpClient m_tcpcli = new TcpClient(); /// /// Flag that controls the amount of debugging information printed out. /// bool m_debug = true; /// /// A delegate that is used to send individual lines received from the server /// on to the caller as soon as they are received. /// /// The line received from the server public delegate void RecvMessageProgress(string line); /// /// A property that can be used to externally control the level of debugging /// information printed out by the Session. /// public bool DebugSocketComms { get { return m_debug; } set { m_debug = value; } } /// /// This read-only property tells the caller how large a chunk should be when /// using SCANDATA to send the contents of a file to the server. Currently /// hardcoded to 4096 bytes. /// public int RecommendedChunkSize { get { return 4096; } } /// /// A property that indicates if a TCP connection to the server has been established. /// public bool Connected { get { return m_connected; } set { m_connected = value; } } /// /// A property that indicates if a TCP session is active. /// public bool Active { get { return m_active; } set { m_active = value; } } /// /// Constructor for a Session object. /// /// The hostname of the server /// The port on which to connect to the server public Session(string hostname, uint port) { m_hostname = hostname; m_port = port; Connected = false; Active = false; } /// /// Sends bytes to the server. /// /// Number of bytes to send /// The bytes to send /// The actual number of bytes sent public int Send(int numbytes, byte[] data) { int sent = 0; if (numbytes > 0) { m_tcpcli.GetStream().Write(data, 0, numbytes); sent = numbytes; } return sent; } /// /// Receives bytes from the server. /// /// The number of bytes to receive /// The destination buffer for the bytes /// The actual number of bytes received public int Recv(int numbytes, out byte[] data) { int read = 0; data = new byte[numbytes]; data.Initialize(); if (numbytes > 0) { read = m_tcpcli.GetStream().Read(data, 0, numbytes); } return read; } /// /// Sends a line to the server. /// /// The line to send /// Indicates whether the send was successful public bool SendLine(string line) { ASCIIEncoding asciienc = new ASCIIEncoding(); byte[] data = asciienc.GetBytes(line + "\n"); int numsent = Send(data.Length, data); if (DebugSocketComms) Console.WriteLine("Sent: " + line); return numsent == data.Length; } /// /// Receives a line from the server. /// /// A variable to collect the line /// Indicates whether the receipt was successful public bool RecvLine(out string line) { bool done = false; line = ""; while (!done) { // Receive only one byte at a time and append to the string byte[] data = new byte[1]; if (1 != Recv(1, out data)) { throw new Exception("Error receiving data from server"); } if (data[0] == '\r') continue; else if (data[0] == '\n') done = true; else line += Convert.ToChar(data[0]); } if (DebugSocketComms) Console.WriteLine("Recv: " + line); return true; } /// /// Receives a message from the server. A message consists of a set of /// zero or more lines, terminated by a blank line. /// /// A container to collect the message /// Indicates whether the receipt was successful public bool RecvMessage(out string[] lines) { // Forward to the actual implementation return RecvMessage(out lines, null); } /// /// Receives a message from the server. A message consists of a set of /// zero or more lines, terminated by a blank line. /// /// A container to collect the message /// A callback (RecvMessageProgress delegate instance) to get individual message lines /// Indicates whether the receipt was successful public bool RecvMessage(out string[] lines, RecvMessageProgress callback) { ArrayList lineslist = new ArrayList(); while (true) { string line = ""; if (!RecvLine(out line) || (line.Length == 0)) { break; } lineslist.Add(line); if (callback != null) { callback(line); } } lines = new string[lineslist.Count]; IEnumerator enumerator = lineslist.GetEnumerator(); int pos = 0; while (enumerator.MoveNext()) { lines.SetValue(enumerator.Current, pos); pos++; } // If we got even one line, it's been a success return lines.Length > 0; } /// /// Connects to the server if not already connected and performs /// a handshake. /// public void Connect() { if (Connected) return; m_tcpcli.Connect(m_hostname, (int)m_port); string line = ""; if (RecvLine(out line)) { if (line.StartsWith("OK SSSP/1.0")) { Connected = true; } } if (!Connected) { throw new Exception("Server failed to initiate handshake"); } line = "SSSP/1.0"; SendLine(line); if (RecvLine(out line)) { if (line.StartsWith("ACC ")) { Active = true; } else if (line.StartsWith("REJ ")) { throw new Exception("The server rejected the connection request"); } else { throw new Exception("Unexpected response from server: " + line); } } } /// /// Disconnects from the server if not already disconnected, but performs /// a goodbye handshake first. /// public void Disconnect() { if (Active) { SendLine("BYE"); string line; RecvLine(out line); Active = false; } if (Connected) { m_tcpcli.Close(); Connected = false; } } } /// /// A helper class that encapsulates various SAVDI server options available /// to the client. Objects of this class are initialised with a dictionary /// containing option name strings as keys, mapped to lists of string values. /// public class Options { private string m_version = ""; private bool m_queryServerAllowed = false; private bool m_querySAVIAllowed = false; private bool m_queryEngineAllowed = false; private bool m_queryOptionsAllowed = false; private bool m_scanDataAllowed = false; private bool m_scanFileAllowed = false; private bool m_scanDirAllowed = false; private bool m_scanDirRecursiveAllowed = false; private long m_maxScanData = 0; private long m_maxMemorySize = 0; private long m_maxClassificationSize = 0; private Dictionary> m_options = null; /// /// This property holds the version string returned by the server. /// public string Version { get { return m_version; } set { m_version = value; } } /// /// This property indicates if the "QUERY SERVER" SSSP command is allowed. /// public bool QueryServerAllowed { get { return m_queryServerAllowed; } set { m_queryServerAllowed = value; } } /// /// This property indicates if the "QUERY SAVI" SSSP command is allowed. /// public bool QuerySAVIAllowed { get { return m_querySAVIAllowed; } set { m_querySAVIAllowed = value; } } /// /// This property indicates if the "QUERY ENGINE" SSSP command is allowed. /// public bool QueryEngineAllowed { get { return m_queryEngineAllowed; } set { m_queryEngineAllowed = value; } } /// /// This property indicates if the "QUERY OPTIONS" SSSP command is allowed. /// public bool QueryOptionsAllowed { get { return m_queryOptionsAllowed; } set { m_queryOptionsAllowed = value; } } /// /// This property indicates if the "SCANDATA" SSSP command is allowed. /// public bool ScanDataAllowed { get { return m_scanDataAllowed; } set { m_scanDataAllowed = value; } } /// /// This property indicates if the "SCANFILE" SSSP command is allowed. /// public bool ScanFileAllowed { get { return m_scanFileAllowed; } set { m_scanFileAllowed = value; } } /// /// This property indicates if the "SCANDIR" SSSP command is allowed. /// public bool ScanDirAllowed { get { return m_scanDirAllowed; } set { m_scanDirAllowed = value; } } /// /// This property indicates if the "SCANDIRR" SSSP command is allowed. /// public bool ScanDirRecursiveAllowed { get { return m_scanDirRecursiveAllowed; } set { m_scanDirRecursiveAllowed = value; } } /// /// This property indicates the maximum amount of data that the server will accept /// for a "SCANDATA" request. Zero means it is unlimited. /// public long MaxScanData { get { return m_maxScanData; } set { m_maxScanData = value; } } /// /// This property indicates the maximum amount of data the server will hold in memory /// before using a temporary file when implementing a "SCANDATA" request. /// public long MaxMemorySize { get { return m_maxMemorySize; } set { m_maxMemorySize = value; } } /// /// This property indicates the maximum amount of data from the start of a file that /// the client needs to send in a "SCANDATA" request when requesting classification only. /// public long MaxClassificationSize { get { return m_maxClassificationSize; } set { m_maxClassificationSize = value; } } /// /// Property that gives read-only raw data access to the options dictionary. /// public Dictionary> RawOptions { get { return m_options; } } /// /// Contructor for Options object. /// /// A dictionary mapping an option name to a list of option values public Options(ref Dictionary> options) { m_options = new Dictionary>(); foreach (string key in options.Keys) { m_options[key] = new List(); List valuelist = options[key]; m_options[key].AddRange(valuelist); switch (key) { case "version": Version = valuelist[0]; break; case "method": foreach (string value in valuelist) { switch (value) { case "QUERY SERVER": QueryServerAllowed = true; break; case "QUERY SAVI": QuerySAVIAllowed = true; break; case "QUERY ENGINE": QueryEngineAllowed = true; break; case "OPTIONS": QueryOptionsAllowed = true; break; case "SCANDATA": ScanDataAllowed = true; break; case "SCANFILE": ScanFileAllowed = true; break; case "SCANDIR": ScanDirAllowed = true; break; case "SCANDIRR": ScanDirRecursiveAllowed = true; break; default: break; } } break; case "maxscandata": MaxScanData = Convert.ToInt64(valuelist[0]); break; case "maxmemorysize": MaxMemorySize = Convert.ToInt64(valuelist[0]); break; case "maxclassificationsize": MaxClassificationSize = Convert.ToInt64(valuelist[0]); break; default: break; } } } } /// /// A class that provides an API to query options available to interact /// with a SAVDI server, to set certain options and to request the server to /// scan files or directories with or without progress information. /// public class Client { /// /// The Session object held by the Client. /// private Session m_session = null; /// /// The Options object held by the Client. /// private Options m_options = null; /// /// Regex pattern to match ACC status lines returned by the server. /// private Regex m_accPattern = new Regex(@"^ACC\s+(.*?)\s*$"); /// /// Regex pattern used to scan for lines of "key: value" /// lines returned by the server when the Client object queries it for supported options. /// private Regex m_optionPattern = new Regex(@"^(\w+):\s*(.*?)\s*$"); /// /// Regex pattern that matches a threat detected message. /// returned by the server /// private Regex m_virusPattern = new Regex(@"^VIRUS\s+(\S+)\s+(.*)"); /// /// Regex pattern that matches a file type indicator message /// returned by the server in response to a "classification" request. /// private Regex m_typePattern = new Regex(@"^TYPE\s+(\S+)\s+(.+)$"); /// /// Regex pattern that matches a "file location" message /// returned by the server in response to a "classification" request. /// private Regex m_filePattern = new Regex(@"^FILE\s+(.+)$"); /// /// Regex pattern that matches DONE status lines returned by the server. /// private Regex m_donePattern = new Regex(@"^DONE\s+(\S+)\s+(\S+)\s+(.+)$"); /// /// Regex pattern that matches OK status lines returned by the server after /// a scan has been successfully completed. /// private Regex m_okPattern = new Regex(@"^OK\s+(.+)$"); /// /// Regex pattern that matches FAIL status lines returned by the server. /// private Regex m_failPattern = new Regex(@"^FAIL\s+(\S+)\s+(.+)$"); /// /// Regex pattern that matches lines returned by the server in case of an event. /// private Regex m_eventPattern = new Regex(@"^EVENT\s+(FILE|DIR|ARCHIVE|ERROR|VIRUS)\s+(.+)$"); /// /// A delegate that is implemented as a callback by users of the Session object /// to receive progress information during a scan. Use of the delegate is optional, /// i.e., it is possible to request scans without specifying a callback. /// /// The name of the event that has occurred /// String data associated with the event public delegate void ScanProgressMonitor(string eventname, string eventdata); /// /// A cached copy of a callback based on the ScanProgressMonitor delegate that may have /// been passed in by a user of the Session object. /// private ScanProgressMonitor m_scanprogcallback = null; /// /// Constructor for Client object. /// /// The hostname of the server /// The port on which to connect to the server public Client(string hostname, uint port) { m_session = new Session(hostname, port); m_session.Connect(); QueryOptions(); } /// /// A method called explicitly to disconnect cleanly from the server. /// public void Disconnect() { if (m_session != null) m_session.Disconnect(); } /// /// Method that queries the server for supported options and intialises the /// Options member object. /// private void QueryOptions() { m_session.SendLine("QUERY SERVER"); string[] lines; if (!m_session.RecvMessage(out lines)) { throw new Exception("Unexpected response from server"); } Dictionary> opts = new Dictionary>(); foreach (string line in lines) { Match match = m_optionPattern.Match(line); if (match.Success && match.Groups.Count == 3) { string key = match.Groups[1].Value; string value = match.Groups[2].Value; if (!opts.ContainsKey(key)) { opts[key] = new List(); } opts[key].Add(value); } } m_options = new Options(ref opts); } /// /// A read-only property that returns the Options object. /// public Options Options { get { return m_options; } } /// /// Sets options on the server. /// /// A Dictionary object mapping string option /// names to lists of string option values. public void SetOptions(ref Dictionary> options) { m_session.SendLine("OPTIONS"); foreach (string optionname in options.Keys) { foreach (string optionvalue in options[optionname]) { m_session.SendLine(optionname + ":" + optionvalue); } } m_session.SendLine(""); string line = ""; m_session.RecvLine(out line); string[] lines; m_session.RecvMessage(out lines); // Message discarded on success if (!m_accPattern.Match(line).Success) { throw new Exception("The server rejected the OPTIONS request. Details:" + String.Join("\n", lines)); } } /// /// Sets a single option on the server. /// /// An option to set on the server /// The value to set the option to public void SetOption(string optionname, string optionvalue) { // Construct a dictionary, add the option name and value to it // and forward to the actual implementation Dictionary> options = new Dictionary>(); options[optionname] = new List(); options[optionname].Add(optionvalue); SetOptions(ref options); } /// /// A Session.RecvMessageProgress delegate instance that is called by /// by the m_session Session object, once per line, when receiving a long message. /// /// A line of text sent back by the Session object private void HandleScanResponse(string line) { Match match = m_eventPattern.Match(line); if (match.Success) { if (m_scanprogcallback != null) { // The cached callback is interested only in events, so we won't // forward every line received from the Sesion object m_scanprogcallback(match.Groups[1].Value, match.Groups[2].Value); } } else { match = m_okPattern.Match(line); if (match.Success) { Console.WriteLine("Handled: " + line); } else { match = m_donePattern.Match(line); if (match.Success) { Console.WriteLine("Handled: " + line); } else { match = m_failPattern.Match(line); if (match.Success) { Console.WriteLine("Handled: " + line); } else { Console.WriteLine("NOT handled: " + line); // Other possible matches: (Included here // only for completeness!) m_virusPattern.Match(line); m_typePattern.Match(line); m_filePattern.Match(line); } } } } } /// /// Method that requests the server to scan a file or a directory. /// /// Path to a file or directory /// The actual name of the request command to send to the server /// A callback (ScanProgressMonitor delegate instance) to monitor the scan progress private void ScanPath(string filepath, string request, ScanProgressMonitor callback) { if (callback != null) { m_scanprogcallback = callback; } string line = ""; string pathtosend = Utility.UrlEncode(filepath); // Note that SCANDIR and SCANDIRR will also scan valid file paths, // not just directories Debug.Assert(request == "SCANFILE" || request == "SCANDIR" || request == "SCANDIRR"); line = request + " " + pathtosend; m_session.SendLine(line); m_session.RecvLine(out line); if (!m_accPattern.IsMatch(line)) { throw new Exception("The server rejected the " + request + " request"); } string[] lines; // Briefly disable the printing out of socket communication by the Session object // m_session.DebugSocketComms = false; // Handle the message in a separate callback function // Session.RecvMessageProgress recvmsgcallback = new Session.RecvMessageProgress(HandleScanResponse); m_session.RecvMessage(out lines, recvmsgcallback); m_session.DebugSocketComms = true; } /// /// Public method to scan a file. Differs from ScanFile as it sends the entire /// contents of the file to the server, instead of just sending the file path. /// /// Path to a file public void ScanData(string filepath) { ScanData(filepath, null); } /// /// Public method to scan a file. Differs from ScanFile as it sends the entire /// contents of the file to the server, instead of just sending the file path. /// /// Path to a file /// A callback function to monitor scan progress public void ScanData(string filepath, ScanProgressMonitor callback) { FileInfo fileinfo = new FileInfo(filepath); if (fileinfo.Length >= Options.MaxScanData) { throw new Exception("File too large to send to server"); } FileStream filestream = new FileStream(filepath, FileMode.Open); BinaryReader filereader = new BinaryReader(filestream); string line = "SCANDATA " + fileinfo.Length; m_session.SendLine(line); int CHUNKSIZE = m_session.RecommendedChunkSize; for (int numchunks = 0; numchunks < fileinfo.Length / CHUNKSIZE; numchunks++) { m_session.Send(CHUNKSIZE, filereader.ReadBytes(CHUNKSIZE)); } int lastchunksize = (int)(fileinfo.Length % CHUNKSIZE); m_session.Send(lastchunksize, filereader.ReadBytes(lastchunksize)); filereader.Close(); filestream.Close(); Console.WriteLine("Sent " + fileinfo.Length + " bytes in " + (fileinfo.Length / CHUNKSIZE + 1) + " chunk(s) of max size " + CHUNKSIZE + " bytes"); line = ""; m_session.RecvLine(out line); if (!m_accPattern.Match(line).Success) { throw new Exception("The server rejected the SCANDATA request"); } string[] lines; m_session.RecvMessage(out lines); } /// /// Public method to scan a file. /// /// Path to a file public void ScanFile(string filepath) { // Forward to the core implementation ScanFile(filepath, null); } /// /// Public method to scan a file. /// /// Path to a file /// A callback function to monitor scan progress public void ScanFile(string filepath, ScanProgressMonitor callback) { // Forward to the core implementation ScanPath(filepath, "SCANFILE", callback); } /// /// Public method to scan the contents of a directory. /// /// Path to a directory public void ScanDir(string filepath) { // Forward to the core implementation ScanDir(filepath, null); } /// /// Public method to scan the contents of a directory. /// /// Path to a directory /// A callback function to monitor scan progress public void ScanDir(string filepath, ScanProgressMonitor callback) { // Forward to the core implementation ScanPath(filepath, "SCANDIR", callback); } /// /// Public method to scan the contents of a directory and all-subdirectories, recursively. /// /// Path to a directory public void ScanDirRecursive(string filepath) { // Forward to the core implementation ScanDirRecursive(filepath, null); } /// /// Public method to scan the contents of a directory and all-subdirectories, recursively. /// /// Path to a directory /// A callback function to monitor scan progress public void ScanDirRecursive(string filepath, ScanProgressMonitor callback) { // Forward to the core implementation ScanPath(filepath, "SCANDIRR", callback); } } /// /// This class contains the entry point as well as methods to parse the /// command-line parameters passed in and to display the usage/help banner. /// public class Program { /// /// Prints the usage/help banner. /// private static void Usage() { Console.WriteLine("Usage: savdicli.exe [OPTIONS] [] [] "); Console.WriteLine("Options: "); Console.WriteLine(" -a : Request the server to scan data from a local "); Console.WriteLine(" -f : Scan a remote path on the server "); Console.WriteLine(" -d : Request the server to scan a remote (DEFAULT) "); Console.WriteLine(" -r : Request the server to scan a remote and all "); Console.WriteLine(" sub-directories recursively "); Console.WriteLine(" : The path to a local or remote file (argument not compatible "); Console.WriteLine(" with the -d or -r flags) "); Console.WriteLine(" : The path to a remote directory on the server (argument not "); Console.WriteLine(" compatible with the -a or -f flags) "); Console.WriteLine(" "); Console.WriteLine(" -h : Shows this help message "); Console.WriteLine(" "); Console.WriteLine("Notes: "); Console.WriteLine(" 1) This program sends the contents of a file or the path to a file or a "); Console.WriteLine(" directory to a local or remote instance of the SAV Dynamic Interface "); Console.WriteLine(" server. The item is scanned via SSSP (Sophos Simple Scanning Protol). "); Console.WriteLine(" 2) The default is 'localhost', and the default 4010. "); Console.WriteLine(" 3) Only one option must be specified. The default is '-d'. "); Console.WriteLine(" 4) If happens to be remote file, it will be scanned as a file, "); Console.WriteLine(" i.e., '-f' will be implicitly assumed. "); } /// /// This method scans the list of command-line parameters supplied to it and returns /// interesting values to the caller. /// /// The list of command-line parameters to scan /// The scanning method that will be used by the server /// The path to the file or directory to be scanned /// The hostname of the server /// The port on which to connect to the server private static void ParseArgs(ref string[] args, out string option, out string filepath, out string hostname, out uint port) { option = ""; filepath = ""; hostname = "localhost"; port = 4010; // Scan the arguments list // if (args.Length < 1 || args.Length > 4) { Usage(); Environment.Exit(2); } int nextargpos = 0; Regex validArgsPattern = new Regex("^-([?ahfdr])$"); Match match = validArgsPattern.Match(args[0]); if (match.Success) { option = match.Groups[1].Value; nextargpos++; } if (option == "h" || option == "?") { Usage(); Environment.Exit(1); } if (args.Length > nextargpos) { filepath = args[nextargpos]; nextargpos++; } if (args.Length > nextargpos) { hostname = args[nextargpos]; nextargpos++; } if (args.Length > nextargpos) { try { port = Convert.ToUInt32(args[nextargpos]); } catch (Exception) { Usage(); Environment.Exit(2); } } // Check that we have all the arguments we need if (filepath.Length == 0 || hostname.Length == 0 || port == 0) { Usage(); Environment.Exit(2); } } private static void PrintRecursiveScandirProgress(string eventname, string eventdata) { Console.WriteLine("(Callback) Event: " + eventname + " " + eventdata); } /// /// Method that performs the core task of connecting to the server, requesting /// a scan and disconnecting. /// /// The scanning method that will be used by the server /// The path to the file or directory to be scanned /// The hostname of the server /// The port on which to connect to the server private static void RunClient(string option, string filepath, string hostname, uint port) { // Construct the client Client client = new Client(hostname, port); try { if (option == "d" || option == "") // By default, assume user wants to scan a directory { if (client.Options.ScanDirAllowed) client.ScanDir(filepath); else throw new Exception("Scanning of directories is not available on the server"); } else if (option == "r") { if (client.Options.ScanDirRecursiveAllowed) { // Demonstration of how to use a callback function to receive scanning events // from the server as they occur. This implementation simply prints out the event. Client.ScanProgressMonitor callback = new Client.ScanProgressMonitor(PrintRecursiveScandirProgress); // How to switch on detailed reporting using SetOptions. // In this case the following events will be reported back by the server: // file : When a file is found // error: When a file/scanning error is encountered // virus: When a virus is found // Dictionary> options = new Dictionary>(); options["report"] = new List(); options["report"].Add("file"); options["report"].Add("error"); options["report"].Add("virus"); client.SetOptions(ref options); client.ScanDirRecursive(filepath, callback); // Switch back to the default options for the rest of this session // client.SetOption("savidefault", ""); } else throw new Exception("Recursive scanning of directories is not available on the server"); } else if (option == "f") { if (client.Options.ScanFileAllowed) client.ScanFile(filepath); else throw new Exception("Scanning of files is disabled on the server"); } else if (option == "a") { // Ensure that the file exists // FileInfo fileinfo = new FileInfo(filepath); if (fileinfo.Exists) { if (client.Options.ScanDataAllowed) client.ScanData(Path.GetFullPath(filepath)); else throw new Exception("Scanning of data via SSSP is disabled on the server"); } else { throw new Exception("File not found: " + filepath); } } } finally { client.Disconnect(); } } /// /// The entry point into the assembly. /// /// The list of string objects, one for each command-line parameter public static void Main(string[] args) { string option = ""; string filepath = ""; string hostname = "localhost"; uint port = 4010; ParseArgs(ref args, out option, out filepath, out hostname, out port); try { RunClient(option, filepath, hostname, port); } catch (Exception e) { Console.WriteLine("FATAL Error: " + e.Message); Environment.Exit(1); } Environment.Exit(0); } } }