// created on 10/5/2002 at 23:01 // Npgsql.NpgsqlConnection.cs // // Author: // Francisco Jr. (fxjrlists@yahoo.com.br) // // Copyright (C) 2002 The Npgsql Development Team // npgsql-general@gborg.postgresql.org // http://gborg.postgresql.org/project/npgsql/projdisplay.php // // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 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 GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.ComponentModel; using System.Data; using System.Net; using System.Net.Sockets; using System.IO; using System.Text; using System.Collections; using System.Collections.Specialized; using System.Security.Cryptography.X509Certificates; using Mono.Security.Protocol.Tls; using NpgsqlTypes; using Npgsql.Design; namespace Npgsql { /// /// Represents the method that handles the Notification events. /// /// The source of the event. /// A NpgsqlNotificationEventArgs that contains the event data. public delegate void NotificationEventHandler(Object sender, NpgsqlNotificationEventArgs e); /// /// This class represents a connection to a /// PostgreSQL server. /// [System.Drawing.ToolboxBitmapAttribute(typeof(NpgsqlConnection))] public sealed class NpgsqlConnection : Component, IDbConnection, ICloneable { //Changed the Name of this event because events usually don't start with 'On' in the .Net-Framework // (but their handlers do ;-) /// /// Occurs on NotificationResponses from the PostgreSQL backend. /// public event NotificationEventHandler Notification; // Public properties for ssl callbacks public CertificateValidationCallback CertificateValidationCallback; public CertificateSelectionCallback CertificateSelectionCallback; public PrivateKeySelectionCallback PrivateKeySelectionCallback; private NpgsqlState state; private ConnectionState connection_state; private String connection_string; internal ListDictionary connection_string_values; // some of the following constants are needed // for designtime support so I made them 'internal' // as I didn't want to add another interface for internal access // --brar // In the connection string internal readonly Char CONN_DELIM = ';'; // Delimeter internal readonly Char CONN_ASSIGN = '='; internal readonly String CONN_SERVER = "SERVER"; internal readonly String CONN_USERID = "USER ID"; internal readonly String CONN_PASSWORD = "PASSWORD"; internal readonly String CONN_DATABASE = "DATABASE"; internal readonly String CONN_PORT = "PORT"; internal readonly String SSL_ENABLED = "SSL"; // Postgres default port internal readonly String PG_PORT = "5432"; // These are for ODBC connection string compatibility internal readonly String ODBC_USERID = "UID"; internal readonly String ODBC_PASSWORD = "PWD"; // These are for the connection pool internal readonly String MIN_POOL_SIZE = "MINPOOLSIZE"; internal readonly String MAX_POOL_SIZE = "MAXPOOLSIZE"; internal readonly String CONN_ENCODING = "ENCODING"; internal readonly String CONN_TIMEOUT = "TIMEOUT"; // Values for possible CancelRequest messages. private NpgsqlBackEndKeyData backend_keydata; // Flag for transaction status. private Boolean _inTransaction = false; // Mediator which will hold data generated from backend private NpgsqlMediator _mediator; // Logging related values private readonly String CLASSNAME = "NpgsqlConnection"; private Stream stream; private Connector _connector; private Encoding connection_encoding; private Boolean _supportsPrepare = false; private String _serverVersion; // Contains string returned from select version(); private Hashtable _oidToNameMapping; private System.Resources.ResourceManager resman; private Int32 _backendProtocolVersion; private Int32 _connectionTimeout; /// /// Initializes a new instance of the /// NpgsqlConnection class. /// public NpgsqlConnection() : this(String.Empty) {} /// /// Initializes a new instance of the /// NpgsqlConnection class /// and sets the ConnectionString. /// /// The connection used to open the PostgreSQL database. public NpgsqlConnection(String ConnectionString) { resman = new System.Resources.ResourceManager(this.GetType()); NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, CLASSNAME, ConnectionString); connection_state = ConnectionState.Closed; state = NpgsqlClosedState.Instance; connection_string = ConnectionString; connection_string_values = new ListDictionary(); connection_encoding = Encoding.Default; _backendProtocolVersion = ProtocolVersion.Version3; _mediator = new NpgsqlMediator(); _oidToNameMapping = new Hashtable(); _connectionTimeout = 15; CertificateValidationCallback = new CertificateValidationCallback(DefaultCertificateValidationCallback); if (connection_string != String.Empty) ParseConnectionString(); } /// /// Gets or sets the string used to open a SQL Server database. /// /// The connection string that includes the server name, /// the database name, and other parameters needed to establish /// the initial connection. The default value is an empty string. /// [RefreshProperties(RefreshProperties.All), DefaultValue(""), RecommendedAsConfigurable(true)] [NpgsqlSysDescription("Description_ConnectionString", typeof(NpgsqlConnection)), Category("Data")] [Editor(typeof(ConnectionStringEditor), typeof(System.Drawing.Design.UITypeEditor))] public String ConnectionString { get { return connection_string; } set { NpgsqlEventLog.LogPropertySet(LogLevel.Debug, CLASSNAME, "ConnectionString", value); connection_string = value; if (connection_string != String.Empty) ParseConnectionString(); } } /// /// Gets the time to wait while trying to establish a connection /// before terminating the attempt and generating an error. /// /// The time (in seconds) to wait for a connection to open. The default value is 15 seconds. [NpgsqlSysDescription("Description_ConnectionTimeout", typeof(NpgsqlConnection))] public Int32 ConnectionTimeout { get { return _connectionTimeout; } } /// /// Gets the name of the current database or the database to be used after a connection is opened. /// /// The name of the current database or the name of the database to be /// used after a connection is opened. The default value is an empty string. [NpgsqlSysDescription("Description_Database", typeof(NpgsqlConnection))] public String Database { get { return DatabaseName; } } /// /// Gets the current state of the connection. /// /// A bitwise combination of the ConnectionState values. The default is Closed. [Browsable(false)] public ConnectionState State { get { return connection_state; } } /// /// Begins a database transaction. /// /// An IDbTransaction /// object representing the new transaction. /// /// Currently there's no support for nested transactions. /// IDbTransaction IDbConnection.BeginTransaction() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "IDbConnection.BeginTransaction"); //throw new NotImplementedException(); return BeginTransaction(); } /// /// Begins a database transaction with the specified isolation level. /// /// The isolation level under which the transaction should run. /// An IDbTransaction /// object representing the new transaction. /// /// Currently the IsolationLevel ReadCommitted and Serializable are supported by the PostgreSQL backend. /// There's no support for nested transactions. /// IDbTransaction IDbConnection.BeginTransaction(IsolationLevel level) { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "IDbConnection.BeginTransaction", level); //throw new NotImplementedException(); return BeginTransaction(level); } // I had to rename this Method from Notification to Notify due to the renaming of OnNotification to Notification /// /// Creates a Notification event /// /// The NpgsqlNotificationEventArgs that contains the event data. internal void Notify(NpgsqlNotificationEventArgs e) { if (Notification != null) Notification(this, e); } /// /// Begins a database transaction. /// /// A NpgsqlTransaction /// object representing the new transaction. /// /// Currently there's no support for nested transactions. /// public NpgsqlTransaction BeginTransaction() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "BeginTransaction"); return this.BeginTransaction(IsolationLevel.ReadCommitted); } /// /// Begins a database transaction with the specified isolation level. /// /// The isolation level under which the transaction should run. /// A NpgsqlTransaction /// object representing the new transaction. /// /// Currently the IsolationLevel ReadCommitted and Serializable are supported by the PostgreSQL backend. /// There's no support for nested transactions. /// public NpgsqlTransaction BeginTransaction(IsolationLevel level) { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "BeginTransaction", level); if (_inTransaction) throw new InvalidOperationException(resman.GetString("Exception_NoNestedTransactions")); return new NpgsqlTransaction(this, level); } /// /// This method changes the current database by disconnecting from the actual /// database and connecting to the specified. /// /// The name of the database to use in place of the current database. public void ChangeDatabase(String dbName) { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ChangeDatabase", dbName); //throw new NotImplementedException(); if (dbName == null) throw new ArgumentNullException("dbName"); if (dbName == String.Empty) throw new ArgumentException(String.Format(resman.GetString("Exception_InvalidDbName"), dbName), "dbName"); if(this.connection_state != ConnectionState.Open) throw new InvalidOperationException(resman.GetString("Exception_ChangeDatabaseOnOpenConn")); String oldDatabaseName = (String)connection_string_values[CONN_DATABASE]; Close(); connection_string_values[CONN_DATABASE] = dbName; Open(); } /// /// Opens a database connection with the property settings specified by the /// ConnectionString. /// public void Open() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "Open"); // I moved this here from ParseConnectionString as there is no need to validate the // ConnectionString before we open the connection. // See: http://gborg.postgresql.org/pipermail/npgsql-hackers/2003-March/000019.html // In fact it makes it possible to parse incomplete ConnectionStrings for designtime support // -- brar // // Now check if there is any missing argument. if (connection_string == String.Empty) throw new InvalidOperationException(resman.GetString("Exception_ConnStrEmpty")); if (connection_string_values[CONN_SERVER] == null) throw new ArgumentException(resman.GetString("Exception_MissingConnStrArg"), CONN_SERVER); if ((connection_string_values[CONN_USERID] == null) & (connection_string_values[ODBC_USERID] == null)) throw new ArgumentException(resman.GetString("Exception_MissingConnStrArg"), CONN_USERID); if ((connection_string_values[CONN_PASSWORD] == null) & (connection_string_values[ODBC_PASSWORD] == null)) throw new ArgumentException(resman.GetString("Exception_MissingConnStrArg"), CONN_PASSWORD); if (connection_string_values[CONN_DATABASE] == null) // Database is optional. "[...] defaults to the user name if empty" connection_string_values[CONN_DATABASE] = connection_string_values[CONN_USERID]; if (connection_string_values[CONN_PORT] == null) // Port is optional. Defaults to PG_PORT. connection_string_values[CONN_PORT] = PG_PORT; if (connection_string_values[SSL_ENABLED] == null) connection_string_values[SSL_ENABLED] = "no"; if (connection_string_values[MIN_POOL_SIZE] == null) connection_string_values[MIN_POOL_SIZE] = "1"; if (connection_string_values[MAX_POOL_SIZE] == null) connection_string_values[MAX_POOL_SIZE] = "20"; if (connection_string_values[CONN_ENCODING] == null) connection_string_values[CONN_ENCODING] = "SQL_ASCII"; if (connection_string_values[CONN_TIMEOUT] == null) connection_string_values[CONN_TIMEOUT] = "15"; try { // Check if the connection is already open. if (connection_state == ConnectionState.Open) throw new NpgsqlException(resman.GetString("Exception_ConnOpen")); lock(ConnectorPool.ConnectorPoolMgr) { Connector = ConnectorPool.ConnectorPoolMgr.RequestConnector(ConnectionString, Int32.Parse((String)connection_string_values[MAX_POOL_SIZE]), Int32.Parse((String)connection_string_values[CONN_TIMEOUT]), false); Connector.InUse = true; } if (!Connector.IsInitialized) { // Reset state to initialize new connector in pool. CurrentState = NpgsqlClosedState.Instance; // Try first connect using the 3.0 protocol... CurrentState.Open(this); // Change the state of connection to open. connection_state = ConnectionState.Open; // Check if there were any errors. if (_mediator.Errors.Count > 0) { // Check if there is an error of protocol not supported... // As the message can be localized, just check the initial unlocalized part of the // message. If it is an error other than protocol error, when connecting using // version 2.0 we shall catch the error again. if (((String)_mediator.Errors[0]).StartsWith("FATAL")) { // Try using the 2.0 protocol. _mediator.Reset(); CurrentState = NpgsqlClosedState.Instance; BackendProtocolVersion = ProtocolVersion.Version2; CurrentState.Open(this); } // Keep checking for errors... if(_mediator.Errors.Count > 0) { StringWriter sw = new StringWriter(); sw.WriteLine(resman.GetString("Exception_OpenError")); uint i = 1; foreach(string error in _mediator.Errors) { sw.WriteLine("{0}. {1}", i++, error); } CurrentState = NpgsqlClosedState.Instance; _mediator.Reset(); throw new NpgsqlException(sw.ToString()); } } backend_keydata = _mediator.GetBackEndKeyData(); // Get version information to enable/disable server version features. // Only for protocol 2.0. if (BackendProtocolVersion == ProtocolVersion.Version2) { NpgsqlCommand command = new NpgsqlCommand("select version();set DATESTYLE TO ISO;", this); _serverVersion = (String) command.ExecuteScalar(); } // Adjust client encoding. //NpgsqlCommand commandEncoding = new NpgsqlCommand("show client_encoding", this); //String clientEncoding = (String)commandEncoding.ExecuteScalar(); if (connection_string_values[CONN_ENCODING].Equals("UNICODE")) connection_encoding = Encoding.UTF8; Connector.ServerVersion = ServerVersion; Connector.BackendProtocolVersion = BackendProtocolVersion; Connector.Encoding = connection_encoding; } // Connector was obtained from pool. // Do a mini initialization in the state machine. connection_state = ConnectionState.Open; ServerVersion = Connector.ServerVersion; BackendProtocolVersion = Connector.BackendProtocolVersion; Encoding = Connector.Encoding; CurrentState = NpgsqlReadyState.Instance; ProcessServerVersion(); _oidToNameMapping = NpgsqlTypesHelper.LoadTypesMapping(this); } catch(IOException e) { // This exception was thrown by StartupPacket handling functions. // So, close the connection and throw the exception. // [TODO] Better exception handling. :) Close(); throw new NpgsqlException(resman.GetString("Exception_OpenError"), e); } } /// /// Closes the connection to the database. /// public void Close() { Dispose(true); } /// /// Creates and returns a IDbCommand /// object associated with the IDbConnection. /// /// A IDbCommand object. IDbCommand IDbConnection.CreateCommand() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "IDbConnection.CreateCommand"); return (NpgsqlCommand) CreateCommand(); } /// /// Creates and returns a NpgsqlCommand /// object associated with the NpgsqlConnection. /// /// A NpgsqlCommand object. public NpgsqlCommand CreateCommand() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "CreateCommand"); return new NpgsqlCommand("", this); } /// /// Releases the unmanaged resources used by the /// NpgsqlConnection /// and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; /// false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (disposing) { // Only if explicitly calling Close or dispose we still have access to // managed resources. NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "Dispose", disposing); try { if ((connection_state == ConnectionState.Open)) { CurrentState.Close(this); } } catch (IOException e) { throw new NpgsqlException(resman.GetString("Exception_CloseError"), e); } finally { // Even if an exception occurs, let object in a consistent state. /*if (stream != null) stream.Close();*/ connection_state = ConnectionState.Closed; } } base.Dispose (disposing); } public Object Clone() { return new NpgsqlConnection(ConnectionString); } // Private util methods /// /// This method parses the connection string. /// It translates it to a list of key-value pairs. /// Valid values are: /// Server - Address/Name of Postgresql Server /// Port - Port to connect to. /// Database - Database name. Defaults to user name if not specified /// User - User name /// Password - Password for clear text authentication /// MinPoolSize - Min size of connection pool /// MaxPoolSize - Max size of connection pool /// Encoding - Encoding to be used /// Timeout - Time to wait for connection open. In seconds. /// private void ParseConnectionString() { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ParseConnectionString"); connection_string_values.Clear(); // Get the key-value pairs delimited by CONN_DELIM String[] pairs = connection_string.Split(new Char[] {CONN_DELIM}); String[] keyvalue; // Now, for each pair, get its key-value. foreach(String s in pairs) { // This happen when there are trailling/empty CONN_DELIMs // Just ignore them. if (s == "") continue; keyvalue = s.Split(new Char[] {CONN_ASSIGN}); // Check if there is a key-value pair. if (keyvalue.Length != 2) throw new ArgumentException(resman.GetString("Exception_WrongKeyVal"), connection_string); // Shift the key to upper case, and substitute ODBC style keys keyvalue[0] = keyvalue[0].ToUpper(); if (keyvalue[0] == ODBC_USERID) keyvalue[0] = CONN_USERID; if (keyvalue[0] == ODBC_PASSWORD) keyvalue[0] = CONN_PASSWORD; // Add the pair to the dictionary. The key is shifted to upper // case for case insensitivity. NpgsqlEventLog.LogMsg(resman, "Log_ConnectionStringValues", LogLevel.Debug, keyvalue[0], keyvalue[1]); connection_string_values.Add(keyvalue[0], keyvalue[1]); } } /// /// This method is required to set all the version dependent features flags. /// SupportsPrepare means the server can use prepared query plans (7.3+) /// private void ProcessServerVersion () { NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ProcessServerVersion"); // FIXME: Do a better parsing of server version to avoid // hardcode version numbers. if (BackendProtocolVersion == ProtocolVersion.Version2) { // With protocol version 2, only 7.3 version supports prepare. SupportsPrepare = (_serverVersion.IndexOf("PostgreSQL 7.3") != -1); } else { // 3.0+ version is set by ParameterStatus message. // On protocol 3.0, 7.4 and above support it. SupportsPrepare = (_serverVersion.IndexOf("7.4") != -1) || (_serverVersion.IndexOf("7.5") != -1); } } internal Stream Stream { get { return _connector.Stream; } set { stream = value; } } internal Connector Connector { get { return _connector; } set { _connector = value; } } /* public bool useSSL() { if (SSL_ENABLED=="yes") return true; return false; } */ // State internal void Query (NpgsqlCommand queryCommand) { CurrentState.Query(this, queryCommand ); } internal void Authenticate (string password) { CurrentState.Authenticate(this, password ); } internal void Startup () { CurrentState.Startup(this); } internal void Parse (NpgsqlParse parse) { CurrentState.Parse(this, parse); } internal void Flush () { CurrentState.Flush(this); } internal void Sync () { CurrentState.Sync(this); } internal void Bind (NpgsqlBind bind) { CurrentState.Bind(this, bind); } internal void Execute (NpgsqlExecute execute) { CurrentState.Execute(this, execute); } // Default SSL Callbacks implementation. private Boolean DefaultCertificateValidationCallback( X509Certificate certificate, int[] certificateErrors) { return true; } internal NpgsqlState CurrentState { get { return state; } set { state = value; } } internal NpgsqlBackEndKeyData BackEndKeyData { get { return backend_keydata; } set { backend_keydata = value; } } internal String ServerName { get { return (String)connection_string_values[CONN_SERVER]; } } internal String ServerPort { get { return (String)connection_string_values[CONN_PORT]; } } internal String DatabaseName { get { return (String)connection_string_values[CONN_DATABASE]; } } internal String UserName { get { return (String)connection_string_values[CONN_USERID]; } } internal String ServerPassword { get { return (String)connection_string_values[CONN_PASSWORD]; } } internal String SSL { get { return (String)connection_string_values[SSL_ENABLED]; } } internal Encoding Encoding { get { return connection_encoding; } set { connection_encoding = value; } } internal NpgsqlMediator Mediator { get { return _mediator; } } internal Boolean InTransaction { get { return _inTransaction; } set { _inTransaction = value; } } internal Boolean SupportsPrepare { get { return _supportsPrepare; } set { _supportsPrepare = value; } } internal String ServerVersion { get { return _serverVersion; } set { _serverVersion = value; } } internal Hashtable OidToNameMapping { get { return _oidToNameMapping; } set { _oidToNameMapping = value; } } internal Int32 BackendProtocolVersion { get { return _backendProtocolVersion; } set { _backendProtocolVersion = value; } } internal Int32 MinPoolSize { get { return Int32.Parse((String)connection_string_values[MIN_POOL_SIZE]); } } internal Int32 MaxPoolSize { get { return Int32.Parse((String)connection_string_values[MAX_POOL_SIZE]); } } } }