using System; using System.Collections.Concurrent; using System.Net; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using RethinkDb.Driver.Ast; using RethinkDb.Driver.Model; using RethinkDb.Driver.Proto; using RethinkDb.Driver.Utils; namespace RethinkDb.Driver.Net { /// /// Represents a single connection to a RethinkDB Server. /// public class Connection : IConnection { private long nextToken = 0; private string dbname; private readonly TimeSpan? connectTimeout; private readonly Handshake handshake; internal SocketWrapper Socket { get; private set; } private readonly ConcurrentDictionary cursorCache = new ConcurrentDictionary(); private SslContext sslContext; /// /// Raised when the underlying network connection has thrown an exception. /// public event EventHandler ConnectionError; internal Connection(Builder builder) { dbname = builder.dbname; if( builder.authKey.IsNotNullOrEmpty() && builder.user.IsNotNullOrEmpty() ) { throw new ReqlDriverError("Either `authKey` or `user` can be used, but not both."); } var user = builder.user ?? "admin"; var password = builder.password ?? builder.authKey ?? ""; this.handshake = new Handshake(user, password); this.Hostname = builder.hostname ?? "localhost"; this.Port = builder.port ?? RethinkDBConstants.DefaultPort; connectTimeout = builder.timeout; this.sslContext = builder.sslContext; } /// /// Hostname assigned to the connection. /// public string Hostname { get; } /// /// TCP port number assigned to the connection. /// public int Port { get; } /// /// Current default database for queries on the connection. To change default database, /// public virtual string Db => dbname; /// /// Changes the default database on the connection. /// public virtual void Use(string db) { dbname = db; } /// /// Returns the connection timeout setting. /// public virtual TimeSpan? Timeout => connectTimeout; /// /// Reconnects the underlying connection to the server. /// /// /// The timeout value before throwing exception public virtual void Reconnect(bool noreplyWait = false, TimeSpan? timeout = null) { ReconnectAsync(noreplyWait, timeout).WaitSync(); } /// /// Asynchronously reconnects the underlying connection to the server. /// /// /// The timeout value before throwing exception public virtual async Task ReconnectAsync(bool noreplyWait = false, TimeSpan? timeout = null) { Close(noreplyWait); this.Socket = new SocketWrapper(this.Hostname, this.Port, timeout ?? connectTimeout, this.sslContext, OnSocketErrorCallback); await this.Socket.ConnectAsync(handshake).ConfigureAwait(false); } /// /// Gets the connection state of the Socket. This property will return the latest /// known state of the Socket. When it returns false, the Socket was either never connected /// or it is not connected anymore. When it returns true, though, there's no guarantee that the Socket /// is still connected, but only that it was connected at the time of the last IO operation. /// public virtual bool Open => this.Socket?.Open ?? false; /// /// Retrieves the client-side local endpoint used to connect to the RethinkDB server. /// public virtual IPEndPoint ClientEndPoint => this.Socket?.ClientEndPoint; /// /// Retrieves the server-side endpoint of the connection to the RethinkDB server. /// public virtual IPEndPoint RemoteEndPoint => this.Socket?.RemoteEndPoint; /// /// Flag to check if the underlying socket has some kind of error. /// public virtual bool HasError => this.Socket?.HasError ?? false; /// /// An exception throwing method to check the state of the /// underlying socket. /// /// Throws when the underlying socket is closed. public virtual void CheckOpen() { if( !this.Socket?.Open ?? true ) { throw new ReqlDriverError("Connection is closed."); } } /// /// Called when the underlying encounters an error. /// protected void OnSocketErrorCallback(Exception e) { CleanUpCursorCache(e.Message); //raise event defensively. in case anyone else is subscribed //we don't stop processing the rest of the subscribers. this.ConnectionError.FireEvent(this, e); } /// /// Called when cleanup is needed. Usually when the connection was closed /// and can no longer be used. The is in a state /// where it must be "reconnected" before it can be used again. /// protected void CleanUpCursorCache(string message) { foreach( var cursor in this.cursorCache.Values ) { cursor.SetError(message); } cursorCache.Clear(); } /// /// Closes the connection. /// public void Dispose() { this.Close(false); } /// /// Closes the connection. /// /// public virtual void Close(bool shouldNoReplyWait = true) { if( this.Socket != null ) { try { if( shouldNoReplyWait ) { NoReplyWait(); } } finally { nextToken = 0; this.Socket.Close(); this.Socket = null; } } CleanUpCursorCache("The connection is closed."); } /// /// Ensure that previous queries executed with NoReplyWait have been processed /// by the server. Note that this guarantee only apples to queries run on the /// same connection. /// public virtual void NoReplyWait() { NoReplyWaitAsync().WaitSync(); } /// /// Asynchronously ensures that previous queries executed with NoReplyWait have /// been processed by the server. Note that this guarantee only apples to queries /// run on the same connection. /// public virtual Task NoReplyWaitAsync(CancellationToken cancelToken = default) { return RunQueryWaitAsync(Query.NoReplyWait(NewToken()), cancelToken); } /// /// Return the server name and server UUID being used by a connection. /// public virtual async Task ServerAsync(CancellationToken cancelToken = default) { var response = await SendQuery(Query.ServerInfo(NewToken()), cancelToken, awaitResponse: true).ConfigureAwait(false); if( response.Type == ResponseType.SERVER_INFO ) { return response.Data[0].ToObject(Converter.Serializer); } throw new ReqlDriverError("Did not receive a SERVER_INFO response."); } /// /// Return the server name and server UUID being used by a connection. /// public virtual Server Server() { return ServerAsync().WaitSync(); } private long NewToken() { return Interlocked.Increment(ref nextToken); } Task SendQueryReply(Query query) { return SendQuery(query, CancellationToken.None, awaitResponse: true); } void SendQueryNoReply(Query query) { SendQuery(query, CancellationToken.None, awaitResponse: false); } /// /// Fast SUCCESS_SEQUENCE or SUCCESS_PARTIAL conversion without DLR dynamic. /// protected virtual async Task> RunQueryCursorAsync(Query query, CancellationToken cancelToken) { var res = await SendQuery(query, cancelToken, awaitResponse: true).ConfigureAwait(false); if( res.IsPartial || res.IsSequence ) { return new Cursor(this, query, res); } if (res.IsError) { throw res.MakeError(query); } throw new ReqlDriverError( $"The query response cannot be converted to a Cursor. The run helper " + $"works with SUCCESS_SEQUENCE or SUCCESS_PARTIAL results. The server " + $"response was {res.Type}. If the server response can be handled by " + $"this run method check T. Otherwise, if the server response cannot " + $"be handled by this run helper use `.RunAtom` or `.RunResult`."); } /// /// Fast SUCCESS_ATOM conversion without the DLR dynamic /// protected virtual async Task RunQueryAtomAsync(Query query, CancellationToken cancelToken) { var res = await SendQuery(query, cancelToken, awaitResponse: true).ConfigureAwait(false); if( res.IsAtom ) { try { if( typeof(T).IsJToken() ) { if( res.Data[0].Type == JTokenType.Null ) return (T)(object)null; var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); Converter.ConvertPseudoTypes(res.Data[0], fmt); return (T)(object)res.Data[0]; //ugh ugly. find a better way to do this. //return res.Data[0].ToObject(); } return res.Data[0].ToObject(Converter.Serializer); } catch( IndexOutOfRangeException ex ) { throw new ReqlDriverError("Atom response was empty!", ex); } } if( res.IsError ) { throw res.MakeError(query); } throw new ReqlDriverError( $"The query response cannot be converted to an object of T or List. This run helper works with SUCCESS_ATOM results. The server response was {res.Type}. If the server response can be handled by this run method try converting to T or List. Otherwise, if the server response cannot be handled by this run helper use another run helper like `.RunCursor` or `.RunResult`."); } /// /// Fast SUCCESS_ATOM or SUCCESS_SEQUENCE conversion without the DLR dynamic /// private async Task RunQueryResultAsync(Query query, CancellationToken cancelToken) { var res = await SendQuery(query, cancelToken, awaitResponse: true).ConfigureAwait(false); if( res.IsAtom ) { try { if( typeof(T).IsJToken() ) { if( res.Data[0].Type == JTokenType.Null ) return (T)(object)null; var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); Converter.ConvertPseudoTypes(res.Data[0], fmt); return (T)(object)res.Data[0]; //ugh ugly. find a better way to do this. } return res.Data[0].ToObject(Converter.Serializer); } catch ( IndexOutOfRangeException ex ) { throw new ReqlDriverError("Atom response was empty!", ex); } } if( res.IsSequence ) { if( typeof(T).IsJToken() ) { var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); Converter.ConvertPseudoTypes(res.Data, fmt); return (T)(object)res.Data; //ugh ugly. find a better way to do this. } return res.Data.ToObject(Converter.Serializer); } if (res.IsError) { throw res.MakeError(query); } throw new ReqlDriverError( $"The query response cannot be converted to an object of T or List " + $"because the server response was {res.Type}. The `.RunResult` helper " + $"only works with SUCCESS_ATOM or SUCCESS_SEQUENCE responses. When the query " + $"response grows larger (over 100K), the response type from the server " + $"can change from SUCCESS_SEQUENCE to SUCCESS_PARTIAL; in which case, you'll " + $"need to use `.RunCursor` that handles both SUCCESS_SEQUENCE and SUCCESS_PARTIAL " + $"response types. The `.RunResult` run helper is only meant to be a " + $"convenience method for relatively quick and smaller responses."); } /// /// Asynchronously ensures that previous queries executed with NoReplyWait have /// been processed by the server. Note that this guarantee only apples to queries /// run on the same connection. /// protected virtual async Task RunQueryWaitAsync(Query query, CancellationToken cancelToken) { var res = await SendQuery(query, cancelToken, awaitResponse: true).ConfigureAwait(false); if( res.IsWaitComplete ) { return; } if (res.IsError) { throw res.MakeError(query); } throw new ReqlDriverError( $"The query response is not WAIT_COMPLETE. The returned query is {res.Type}. You need to call the appropriate run method that handles the response type for your query."); } /// /// Run the query but it's return type is standard dynamic. /// protected async Task RunQueryAsync(Query query, CancellationToken cancelToken) { //If you need to continue after an await, **while inside the driver**, //as a library writer, you must use ConfigureAwait(false) on *your* //await to tell the compiler NOT to resume //on synchronization context (if one is present). // //The top most await (your user) will capture the correct synchronization context //(if any) when they await on a query's run. // // https://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Async-library-methods-should-consider-using-Task-ConfigureAwait-false- // http://blogs.msdn.com/b/lucian/archive/2013/11/23/talk-mvp-summit-async-best-practices.aspx // var res = await SendQuery(query, cancelToken, awaitResponse: true).ConfigureAwait(false); if( res.IsAtom ) { try { if( typeof(T).IsJToken() ) { if( res.Data[0].Type == JTokenType.Null ) return null; var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); Converter.ConvertPseudoTypes(res.Data[0], fmt); return res.Data[0]; } return res.Data[0].ToObject(typeof(T), Converter.Serializer); } catch ( IndexOutOfRangeException ex ) { throw new ReqlDriverError("Atom response was empty!", ex); } } else if( res.IsPartial || res.IsSequence ) { return new Cursor(this, query, res); } else if( res.IsWaitComplete ) { return null; } else { throw res.MakeError(query); } } /// /// Sends the query over to the underlying socket for sending. /// protected Task SendQuery(Query query, CancellationToken cancelToken, bool awaitResponse) { if( this.Socket == null ) throw new ReqlDriverError("No socket open."); return this.Socket.SendQuery(query.Token, query.Serialize(), awaitResponse, cancelToken); } /// /// Prepares the query by setting the default DB if it doesn't exist. /// protected Query PrepareQuery(ReqlAst term, OptArgs globalOpts) { SetDefaultDb(globalOpts); Query q = Query.Start(NewToken(), term, globalOpts); if( globalOpts?.ContainsKey("noreply") == true ) { throw new ReqlDriverError("Don't provide the noreply option as an optarg. Use `.RunNoReply` instead of `.Run`"); } return q; } /// /// Sets the database if it's not already set. /// protected void SetDefaultDb(OptArgs globalOpts) { if( globalOpts?.ContainsKey("db") == false && this.dbname != null ) { // Only override the db global arg if the user hasn't // specified one already and one is specified on the connection globalOpts.With("db", this.dbname); } if( globalOpts?.ContainsKey("db") == true ) { // The db arg must be wrapped in a db ast object globalOpts.With("db", new Db(Arguments.Make(globalOpts["db"]))); } } #region REQL AST RUNNERS //Typically called by the surface API of ReqlAst. Task IConnection.RunAsync(ReqlAst term, object globalOpts, CancellationToken cancelToken) { Query q = PrepareQuery(term, OptArgs.FromAnonType(globalOpts)); return RunQueryAsync(q, cancelToken); } Task> IConnection.RunCursorAsync(ReqlAst term, object globalOpts, CancellationToken cancelToken) { Query q = PrepareQuery(term, OptArgs.FromAnonType(globalOpts)); return RunQueryCursorAsync(q, cancelToken); } Task IConnection.RunAtomAsync(ReqlAst term, object globalOpts, CancellationToken cancelToken) { Query q = PrepareQuery(term, OptArgs.FromAnonType(globalOpts)); return RunQueryAtomAsync(q, cancelToken); } Task IConnection.RunResultAsync(ReqlAst term, object globalOpts, CancellationToken cancelToken) { Query q = PrepareQuery(term, OptArgs.FromAnonType(globalOpts)); return RunQueryResultAsync(q, cancelToken); } void IConnection.RunNoReply(ReqlAst term, object globalOpts) { var opts = OptArgs.FromAnonType(globalOpts); SetDefaultDb(opts); opts.With("noreply", true); SendQueryNoReply(Query.Start(NewToken(), term, opts)); } Task IConnection.RunUnsafeAsync(ReqlAst term, object globalOpts, CancellationToken cancelToken) { Query q = PrepareQuery(term, OptArgs.FromAnonType(globalOpts)); return SendQuery(q, cancelToken, awaitResponse: true); } #endregion #region CURSOR SUPPORT internal virtual Task Continue(ICursor cursor) { return SendQueryReply(Query.Continue(cursor.Token)); } internal virtual void Stop(ICursor cursor) { /* neumino: The END query itself doesn't come back with a response cowboy: ..... a Query[token,STOP], is like sending a very last CONTINUE, r:[] would contain the last bits of the finished seq neumino: Yes a STOP is like a very last CONTINUE neumino: If you have a pending CONTINUE, and send a STOP, you should get back two SUCCESS_SEQUENCE */ //this.Socket?.CancelAwaiter(cursor.Token); SendQueryNoReply(Query.Stop(cursor.Token)); } internal void AddToCache(long token, ICursor cursor) { if( this.Socket == null ) throw new ReqlDriverError("Can't add to cache when not connected."); cursorCache[token] = cursor; } internal void RemoveFromCache(long token) { ICursor removed; if( !cursorCache.TryRemove(token, out removed) ) { Log.Trace($"Could not remove cursor token {token} from cursorCache."); } } #endregion internal static Builder Build() { return new Builder(); } /// /// The Connection builder. /// public class Builder : IConnectionBuilder { internal string hostname = null; internal int? port = null; internal string dbname = null; internal TimeSpan? timeout = null; internal string authKey = null; internal string user = null; internal string password = null; internal SslContext sslContext = null; /// /// The hostname or IP address of the server. /// /// Hostname or IP address public virtual Builder Hostname(string hostnameOrIp) { this.hostname = hostnameOrIp; return this; } /// /// The TCP port to connect with. /// public virtual Builder Port(int driverPort) { this.port = driverPort; return this; } /// /// The default DB for queries. /// public virtual Builder Db(string database) { this.dbname = database; return this; } /// /// The authorization key to the server. /// public virtual Builder AuthKey(string key) { this.authKey = key; return this; } /// /// The user account and password to connect as (default "admin", ""). /// public virtual Builder User(string user, string password) { this.user = user; this.password = password; return this; } /// /// Note: Timeout is not used when using connectAsync(); /// /// /// public virtual Builder Timeout(int seconds) { this.timeout = TimeSpan.FromSeconds(seconds); return this; } /// /// Creates and establishes the connection using the specified settings. /// public virtual Connection Connect() { return ConnectAsync().WaitSync(); } /// /// Asynchronously creates and establishes the connection using the specified settings. /// public virtual async Task ConnectAsync() { var conn = new Connection(this); await conn.ReconnectAsync().ConfigureAwait(false); return conn; } /// /// Enables SSL over the driver port. /// /// Context settings for the SSL stream. public virtual Builder EnableSsl(SslContext context, string licenseTo, string licenseKey) { this.sslContext = context; #if !DEBUG if( !LicenseVerifier.VerifyLicense(licenseTo, licenseKey) ) { throw new ReqlDriverError("The SSL/TLS usage license is invalid. Please check your license that you copied all the characters in your license. If you still have trouble, please contact support@bitarmory.com."); } #endif return this; } } } internal sealed class LicenseVerifier { public static bool VerifyLicense(string licenseTo, string licenseKey) { AssertKeyIsNotBanned(licenseKey); const string modulusString = "tccosXHxEfxzsO1NgsAbGT0X+WuQMUYcj7r2UJCHDqqq3TbR7UoAgzjm3MV1k/eJxRnURypw2wj98eafyajeprk3lK6BnWa5tG8S7p3QU5kkUo7TnmTj8rRkTk9RwArIGGjKfCVZToE06UQudsJmw1UK3mku1qF0/hD909cSiCM="; const string exponentString = "AQAB"; var data = Encoding.UTF8.GetBytes(licenseTo); var rsaParameters = new RSAParameters { Modulus = Convert.FromBase64String(modulusString), Exponent = Convert.FromBase64String(exponentString) }; #if STANDARD using( var rsa = System.Security.Cryptography.RSA.Create() ) #else using( var rsa = new System.Security.Cryptography.RSACryptoServiceProvider() ) #endif { var licenseData = Convert.FromBase64String(licenseKey); rsa.ImportParameters(rsaParameters); bool verified = false; #if STANDARD verified = rsa.VerifyData(data, licenseData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); #else verified = rsa.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), licenseData); #endif return verified; } } internal static void AssertKeyIsNotBanned(string licenseKey) { var isBanned = licenseKey.StartsWith( "fuIIq8Pre2NzXVi0otn54PCx22NNAbNReAsk/ylDIV/ZrWeC60B+C76oj3/Ptb8b02vxPYdN6nR2nz3IgYG/O6Zy5TKoYl2UnR2aNq8sKxjv9siwsjMS82EZB8pxs0UwPoz+xmrKY40sqiIz+thDI2EH1MlGoZd+KfJImJp7fvI=") || licenseKey.StartsWith( "tE4z+qpOuKWP4XfmAbnyepzI6m/qx2DaI+aDkMes94ujERmA7O6bb0100+LiClLymVLXYXNvkRBg7ot6NGlfyli/8x1h3IgL+HD8gFoWdTAN4oG8wE8ZyrFugnqmAHUDAy4h/KrOqB8VUXwGQh8Y/0ZxOBQb0KOaZJC/MUMbve8="); if( isBanned ) throw new UnauthorizedAccessException("The license key has expired."); } } }