#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Text; using Nuclex.Networking.Exceptions; using Nuclex.Support.Tracking; using Nuclex.Support.Scheduling; namespace Nuclex.Audio.Metadata.Requests { /// Establishes a connection to a CDDB compatible server internal class CddbConnectionRequest : Request, IAbortable, IProgressReporter { /// ASCII code for the space character private const char SP = ' '; #region struct ServerGreeting /// Stores the informations returned by the server greeting internal struct ServerGreeting { /// Initializes a new server greeting structure /// Hostname of the connected server /// /// Version number of the CDDB software running on the server /// /// Current server timestamp public ServerGreeting(string hostname, string version, string date) { this.Hostname = hostname; this.Version = version; this.Date = date; } /// Server host name. Example: xyz.fubar.com public string Hostname; /// Version number of server software. Example: v1.0PL0 public string Version; /// Current date and time. Example: Wed Mar 13 00:41:34 1996 public string Date; } #endregion // struct ServerGreeting /// Triggered when the status of the process changes public event EventHandler AsyncProgressChanged; /// Initializes a new CDDB connection request /// /// URL of the server a connection will be established to /// /// Port to which to try to establish the CDDB connection /// Credentials by which to log in to the CDDB server public CddbConnectionRequest( string serverUrl, short port, Cddb.Credentials credentials ) { this.serverUrl = serverUrl; this.port = port; this.credentials = credentials; } /// Begins executing the CDDB connection request in the background public void Start() { this.socket = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); socket.BeginConnect( this.serverUrl, this.port, onConnnected, null ); } /// Aborts the running process. Can be called from any thread. /// /// The receiver should honor the abort request and stop whatever it is /// doing as soon as possible. The method does not impose any requirement /// on the timeliness of the reaction of the running process, but implementers /// are advised to not ignore the abort request and urged to try and design /// their code in such a way that it can be stopped in a reasonable time /// (eg. within 1 second of the abort request). /// public void AsyncAbort() { if(this.socket != null) { this.exception = new AbortedException("Aborted on user request"); closeSocket(); } } /// /// Allows the specific request implementation to re-throw an exception if /// the background process finished unsuccessfully /// protected override void ReraiseExceptions() { if(this.exception != null) { throw this.exception; } } /// /// Allows the specific request implementation to re-throw an exception if /// the background process finished unsuccessfully /// protected override CddbConnection GatherResults() { return this.connection; } /// Triggers the AsyncProgressChanged event /// New progress to report to the event subscribers protected virtual void OnProgressAchieved(float progress) { EventHandler copy = AsyncProgressChanged; if(copy != null) { copy(this, new ProgressReportEventArgs(progress)); } } /// Closes the socket in case an abort request has arrived private void closeSocket() { if(this.socket != null) { Socket theSocket = this.socket; this.socket = null; theSocket.Close(); } } /// Called when a connection to the CDDB server has been established /// Result handle of the asynchronous operation private void onConnnected(IAsyncResult asyncResult) { try { // Check if the callback was issued because the request has been aborted. // If so, set the exception to report to the user and stop right here. if(this.socket == null) { OnAsyncEnded(); return; } // No abort request received, finish the connection this.socket.EndConnect(asyncResult); // A rough estimate. Depending on how long the CDDB server took to reply, // the request may have spent more than 90% of its time waiting for the connection // attempt to finish, but in general, this should be a solid estimate. OnProgressAchieved(0.4f); // Perform the remaining handshaking process asynchronously. If the socket finished // asynchronously, that means we're already executing in a ThreadPool thread which // we can use for the remainder of the handshaking process. if(asyncResult.CompletedSynchronously) { ThreadPool.QueueUserWorkItem(new WaitCallback(performClientServerHandshake)); } else { performClientServerHandshake(null); } } catch(Exception exception) { this.exception = exception; OnAsyncEnded(); return; } } /// /// Performs the client/server handshake upon connecting to a CDDB server /// /// Not used private void performClientServerHandshake(object status) { try { this.protocol = new CddbProtocol(this.socket); // Receive the server greeting string. This is sent by the CDDB server to // any client as soon as it connects and contains the server status and // software running on the server string greetingLine = this.protocol.ReceiveLine(5000); OnProgressAchieved(0.6f); // Obtain the status code returned from the server and convert it to // an exception if it indicates an error int greetingStatusCode = CddbProtocol.GetStatusCode(greetingLine); this.exception = exceptionFromGreetingStatus( greetingStatusCode, greetingLine.Substring((greetingLine.Length >= 4) ? 4 : 3) ); // If no error has occured, decode the server greeting string if(this.exception == null) { ServerGreeting greeting = decodeServerGreeting(greetingLine); // The server greeting was decoded successfully, now let's identify ourselves // to the CDDB server sendHello(); OnProgressAchieved(0.8f); // Receive the reply to our 'hello' from the server receiveHelloReply(); // If everything went fine the CDDB connection is ready to enter normal service if(this.exception == null) { this.connection = new CddbConnection( protocol, greeting.Hostname, greeting.Version, (greetingStatusCode == 201) // Read only connection? ); } } } catch(ObjectDisposedException exception) { if(!(this.exception is AbortedException)) { this.exception = exception; } } catch(Exception exception) { this.exception = exception; } OnAsyncEnded(); } /// /// Generates an exception from the status code in the server greeting, /// if the greeting indicates a problem /// /// Status code provided by the server /// Response returned by the server /// /// The exception for the server status code or null if the status code /// indicates success /// private static Exception exceptionFromGreetingStatus(int statusCode, string message) { switch(statusCode) { case 200: { return null; } case 201: { return null; } case 432: { return new Exceptions.PermissionDeniedException(message); } case 433: { return new Exceptions.TooManyUsersException(message); } case 434: { return new Exceptions.SystemLoadTooHighException(message); } default: { return new BadResponseException( string.Format( "Bad response from CDDB server: invalid status code '{0}', " + "server message is '{1}'", statusCode, message ) ); } } } /// Decodes the greeting received from a CDDB server /// Line containing the server greeting /// A structure with the individual greeting elements private static ServerGreeting decodeServerGreeting(string line) { // Look for the server host name and save its position int hostnameEndIndex = -1; bool hasHostname = (line.Length >= 5) && (line[3] == SP) && ((hostnameEndIndex = line.IndexOf(SP, 4)) != -1); if(!hasHostname) { throw makeBadResponseException("missing hostname in greeting line"); } // Make sure the greeting contains the correct server type bool hasServerType = (line.Length >= hostnameEndIndex + 14) && (line.Substring(hostnameEndIndex + 1, 13) == "CDDBP server "); if(!hasServerType) { throw makeBadResponseException("missing server type in greeting line"); } // Look for the server software version and save its position int versionEndIndex = -1; bool hasVersion = (line.Length > hostnameEndIndex + 15) && ((versionEndIndex = line.IndexOf(SP, hostnameEndIndex + 15)) != -1); if(!hasVersion) { throw makeBadResponseException("missing server version in greeting line"); } // Make sure the greeting contains the ready statement bool hasReadyAt = (line.Length >= versionEndIndex + 9) && (line.Substring(versionEndIndex + 1, 9) == "ready at "); if(!hasReadyAt) { throw makeBadResponseException("missing ready statement in greeting line"); } // Look for the server time and save its position bool hasTime = (line.Length >= versionEndIndex + 10); if(!hasTime) { throw makeBadResponseException("missing server time in greeting line"); } // The greeting line seems to be in order, extract the relevant pieces return new ServerGreeting( line.Substring(4, hostnameEndIndex - 4), line.Substring(hostnameEndIndex + 14, versionEndIndex - hostnameEndIndex - 14), line.Substring(versionEndIndex + 10) ); } /// Constructs a new BadResponseException /// /// Detailed reason to show in the exception message in addition to the standard /// bad response message /// /// The newly constructed exception private static BadResponseException makeBadResponseException(string detailedReason) { return new BadResponseException( string.Format("Malformed greeting from CDDB server ({0})", detailedReason) ); } /// Sends the 'hello' after the server has identified itself private void sendHello() { string hello = string.Format( "cddb hello {0} {1} {2} {3}", this.credentials.User, this.credentials.HostName, this.credentials.ClientName, this.credentials.Version ); this.protocol.SendLine(hello, 5000); } /// Receives the repy to the 'hello' and validates it private void receiveHelloReply() { string replyLine = this.protocol.ReceiveLine(5000); int statusCode = CddbProtocol.GetStatusCode(replyLine); this.exception = exceptionFromHelloResponseStatus( statusCode, replyLine.Substring((replyLine.Length >= 4) ? 4 : 3) ); } /// /// Generates an exception from the status code returned in the response to /// the 'hello' message /// /// Status code provided by the server /// Response returned by the server /// /// The exception for the server status code or null if the status code /// indicates success /// private static Exception exceptionFromHelloResponseStatus( int statusCode, string message ) { switch(statusCode) { case 200: { return null; } case 402: { return new Exceptions.AlreadyShookHandsException(message); } case 431: { return new Exceptions.HandshakeFailedException(message); } default: { return new BadResponseException( string.Format( "Bad response from CDDB server: invalid status code '{0}', " + "server message is '{1}'", statusCode, message ) ); } } } /// URL of the CDDB server to which we connect private string serverUrl; /// Port on the CDDB server to which we connect private short port; /// Credentials by which to log in to the CDDB server private Cddb.Credentials credentials; /// Socket through which we're connected to the server private volatile Socket socket; /// Protocol handler used to communicate with the CDDB server private CddbProtocol protocol; /// /// CDDB connected that's returned to the user upon successfully connecting /// private volatile CddbConnection connection; /// Exception that has occured during asynchronous processing private volatile Exception exception; } } // namespace Nuclex.Audio.Metadata.Requests