1 module mysql.connection;
2 
3 // Publically import rest of package for backwards compatability.
4 // These public imports will eventually be phased out.
5 public import mysql.common;
6 public import mysql.result;
7 public import mysql.protocol.commands;
8 public import mysql.protocol.constants;
9 public import mysql.protocol.extra_types;
10 public import mysql.protocol.packet_helpers;
11 public import mysql.protocol.packets;
12 debug(MYSQL_INTEGRATION_TESTS)
13 {
14     public import mysql.test.common;
15     public import mysql.test.integration;
16     public import mysql.test.regression;
17 }
18 
19 version(Have_vibe_d)
20 {
21     static if(__traits(compiles, (){ import vibe.core.net; } ))
22         import vibe.core.net;
23     else
24         static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'.");
25 }
26 
27 import std.algorithm;
28 import std.conv;
29 import std.datetime;
30 import std.digest.sha;
31 import std.exception;
32 import std.range;
33 import std.socket;
34 import std.stdio;
35 import std.string;
36 import std.traits;
37 import std.variant;
38 
39 immutable SvrCapFlags defaultClientFlags =
40         SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS |
41         SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 |
42         SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS |
43         //SvrCapFlags.MULTI_RESULTS;
44 
45 /**
46  * A struct representing a database connection.
47  *
48  * The Connection is responsible for handshaking with the server to establish
49  * authentication. It then passes client preferences to the server, and
50  * subsequently is the channel for all command packets that are sent, and all
51  * response packets received.
52  *
53  * Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one
54  * byte as a packet number. Connection deals with the headers and ensures that
55  * packet numbers are sequential.
56  *
57  * The initial packet is sent by the server - esentially a 'hello' packet
58  * inviting login. That packet has a sequence number of zero. That sequence
59  * number is the incremented by client and server packets through the handshake
60  * sequence.
61  *
62  * After login all further sequences are initialized by the client sending a
63  * command packet with a zero sequence number, to which the server replies with
64  * zero or more packets with sequential sequence numbers.
65  */
66 class Connection
67 {
68 package:
69     enum OpenState
70     {
71         /// We have not yet connected to the server, or have sent QUIT to the
72         /// server and closed the connection
73         notConnected,
74         /// We have connected to the server and parsed the greeting, but not
75         /// yet authenticated
76         connected,
77         /// We have successfully authenticated against the server, and need to
78         /// send QUIT to the server when closing the connection
79         authenticated
80     }
81     OpenState   _open;
82     MySQLSocket _socket;
83 
84     SvrCapFlags _sCaps, _cCaps;
85     uint    _sThread;
86     ushort  _serverStatus;
87     ubyte   _sCharSet, _protocol;
88     string  _serverVersion;
89 
90     string _host, _user, _pwd, _db;
91     ushort _port;
92 
93     MySQLSocketType _socketType;
94 
95     OpenSocketCallbackPhobos _openSocketPhobos;
96     OpenSocketCallbackVibeD  _openSocketVibeD;
97 
98     // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise
99     // you'll get the dreaded "packet out of order" message. It, and the socket connection are
100     // the reason why most other objects require a connection object for their construction.
101     ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct
102                 /// ordering. First packet should have 0
103     @property ubyte pktNumber()   { return _cpn; }
104     void bumpPacket()       { _cpn++; }
105     void resetPacket()      { _cpn = 0; }
106 
107     version(Have_vibe_d) {} else
108     pure const nothrow invariant()
109     {
110         assert(_socketType != MySQLSocketType.vibed);
111     }
112 
113     ubyte[] getPacket()
114     {
115         scope(failure) kill();
116 
117         ubyte[4] header;
118         _socket.read(header);
119         // number of bytes always set as 24-bit
120         uint numDataBytes = (header[2] << 16) + (header[1] << 8) + header[0];
121         enforceEx!MYXProtocol(header[3] == pktNumber, "Server packet out of order");
122         bumpPacket();
123 
124         ubyte[] packet = new ubyte[numDataBytes];
125         _socket.read(packet);
126         assert(packet.length == numDataBytes, "Wrong number of bytes read");
127         return packet;
128     }
129 
130     void send(ubyte[] packet)
131     in
132     {
133         assert(packet.length > 4); // at least 1 byte more than header
134     }
135     body
136     {
137         _socket.write(packet);
138     }
139 
140     void send(ubyte[] header, ubyte[] data)
141     in
142     {
143         assert(header.length == 4 || header.length == 5/*command type included*/);
144     }
145     body
146     {
147         _socket.write(header);
148         if(data.length)
149             _socket.write(data);
150     }
151 
152     void sendCmd(T)(CommandType cmd, T[] data)
153     in
154     {
155         // Internal thread states. Clients shouldn't use this
156         assert(cmd != CommandType.SLEEP);
157         assert(cmd != CommandType.CONNECT);
158         assert(cmd != CommandType.TIME);
159         assert(cmd != CommandType.DELAYED_INSERT);
160         assert(cmd != CommandType.CONNECT_OUT);
161 
162         // Deprecated
163         assert(cmd != CommandType.CREATE_DB);
164         assert(cmd != CommandType.DROP_DB);
165         assert(cmd != CommandType.TABLE_DUMP);
166 
167         // cannot send more than uint.max bytes. TODO: better error message if we try?
168         assert(data.length <= uint.max);
169     }
170     out
171     {
172         // at this point we should have sent a command
173         assert(pktNumber == 1);
174     }
175     body
176     {
177         if(!_socket.connected)
178         {
179             if(cmd == CommandType.QUIT)
180                 return; // Don't bother reopening connection just to quit
181 
182             _open = OpenState.notConnected;
183             connect(_clientCapabilities);
184         }
185 
186         resetPacket();
187 
188         ubyte[] header;
189         header.length = 4 /*header*/ + 1 /*cmd*/;
190         header.setPacketHeader(pktNumber, cast(uint)data.length +1/*cmd byte*/);
191         header[4] = cmd;
192         bumpPacket();
193 
194         send(header, cast(ubyte[])data);
195     }
196 
197     OKErrorPacket getCmdResponse(bool asString = false)
198     {
199         auto okp = OKErrorPacket(getPacket());
200         enforcePacketOK(okp);
201         _serverStatus = okp.serverStatus;
202         return okp;
203     }
204 
205     ubyte[] buildAuthPacket(ubyte[] token)
206     in
207     {
208         assert(token.length == 20);
209     }
210     body
211     {
212         ubyte[] packet;
213         packet.reserve(4/*header*/ + 4 + 4 + 1 + 23 + _user.length+1 + token.length+1 + _db.length+1);
214         packet.length = 4 + 4 + 4; // create room for the beginning headers that we set rather than append
215 
216         // NOTE: we'll set the header last when we know the size
217 
218         // Set the default capabilities required by the client
219         _cCaps.packInto(packet[4..8]);
220 
221         // Request a conventional maximum packet length.
222         1.packInto(packet[8..12]);
223 
224         packet ~= 33; // Set UTF-8 as default charSet
225 
226         // There's a statutory block of zero bytes here - fill them in.
227         foreach(i; 0 .. 23)
228             packet ~= 0;
229 
230         // Add the user name as a null terminated string
231         foreach(i; 0 .. _user.length)
232             packet ~= _user[i];
233         packet ~= 0; // \0
234 
235         // Add our calculated authentication token as a length prefixed string.
236         assert(token.length <= ubyte.max);
237         if(_pwd.length == 0)  // Omit the token if the account has no password
238             packet ~= 0;
239         else
240         {
241             packet ~= cast(ubyte)token.length;
242             foreach(i; 0 .. token.length)
243                 packet ~= token[i];
244         }
245 
246         if(_db.length)
247         {
248             foreach(i; 0 .. _db.length)
249                 packet ~= _db[i];
250             packet ~= 0; // \0
251         }
252 
253         // The server sent us a greeting with packet number 0, so we send the auth packet
254         // back with the next number.
255         packet.setPacketHeader(pktNumber);
256         bumpPacket();
257         return packet;
258     }
259 
260     void consumeServerInfo(ref ubyte[] packet)
261     {
262         scope(failure) kill();
263 
264         _sCaps = cast(SvrCapFlags)packet.consume!ushort(); // server_capabilities (lower bytes)
265         _sCharSet = packet.consume!ubyte(); // server_language
266         _serverStatus = packet.consume!ushort(); //server_status
267         _sCaps += cast(SvrCapFlags)(packet.consume!ushort() << 16); // server_capabilities (upper bytes)
268         _sCaps |= SvrCapFlags.OLD_LONG_PASSWORD; // Assumed to be set since v4.1.1, according to spec
269 
270         enforceEx!MYX(_sCaps & SvrCapFlags.PROTOCOL41, "Server doesn't support protocol v4.1");
271         enforceEx!MYX(_sCaps & SvrCapFlags.SECURE_CONNECTION, "Server doesn't support protocol v4.1 connection");
272     }
273 
274     ubyte[] parseGreeting()
275     {
276         scope(failure) kill();
277 
278         ubyte[] packet = getPacket();
279 
280         _protocol = packet.consume!ubyte();
281 
282         _serverVersion = packet.consume!string(packet.countUntil(0));
283         packet.skip(1); // \0 terminated _serverVersion
284 
285         _sThread = packet.consume!uint();
286 
287         // read first part of scramble buf
288         ubyte[] authBuf;
289         authBuf.length = 255;
290         authBuf[0..8] = packet.consume(8)[]; // scramble_buff
291 
292         enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "filler should always be 0");
293 
294         consumeServerInfo(packet);
295 
296         packet.skip(1); // this byte supposed to be scramble length, but is actually zero
297         packet.skip(10); // filler of \0
298 
299         // rest of the scramble
300         auto len = packet.countUntil(0);
301         enforceEx!MYXProtocol(len >= 12, "second part of scramble buffer should be at least 12 bytes");
302         enforce(authBuf.length > 8+len);
303         authBuf[8..8+len] = packet.consume(len)[];
304         authBuf.length = 8+len; // cut to correct size
305         enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "Excepted \\0 terminating scramble buf");
306 
307         return authBuf;
308     }
309 
310     static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port)
311     {
312         auto s = new PlainPhobosSocket();
313         s.connect(new InternetAddress(host, port));
314         return s;
315     }
316 
317     static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port)
318     {
319         version(Have_vibe_d)
320             return vibe.core.net.connectTCP(host, port);
321         else
322             assert(0);
323     }
324 
325     void initConnection()
326     {
327         resetPacket();
328         final switch(_socketType)
329         {
330             case MySQLSocketType.phobos:
331                 _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port));
332                 break;
333 
334             case MySQLSocketType.vibed:
335                 version(Have_vibe_d) {
336                     _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port));
337                     break;
338                 } else assert(0, "Unsupported socket type. Need version Have_vibe_d.");
339         }
340     }
341 
342     ubyte[] makeToken(ubyte[] authBuf)
343     {
344         auto pass1 = sha1Of(cast(const(ubyte)[])_pwd);
345         auto pass2 = sha1Of(pass1);
346 
347         SHA1 sha1;
348         sha1.start();
349         sha1.put(authBuf);
350         sha1.put(pass2);
351         auto result = sha1.finish();
352         foreach (size_t i; 0..20)
353             result[i] = result[i] ^ pass1[i];
354         return result.dup;
355     }
356 
357     SvrCapFlags getCommonCapabilities(SvrCapFlags server, SvrCapFlags client) pure
358     {
359         SvrCapFlags common;
360         uint filter = 1;
361         foreach (size_t i; 0..uint.sizeof)
362         {
363             bool serverSupport = (server & filter) != 0; // can the server do this capability?
364             bool clientSupport = (client & filter) != 0; // can we support it?
365             if(serverSupport && clientSupport)
366                 common |= filter;
367             filter <<= 1; // check next flag
368         }
369         return common;
370     }
371 
372     void setClientFlags(SvrCapFlags capFlags)
373     {
374         _cCaps = getCommonCapabilities(_sCaps, capFlags);
375 
376         // We cannot operate in <4.1 protocol, so we'll force it even if the user
377         // didn't supply it
378         _cCaps |= SvrCapFlags.PROTOCOL41;
379         _cCaps |= SvrCapFlags.SECURE_CONNECTION;
380     }
381 
382     void authenticate(ubyte[] greeting)
383     in
384     {
385         assert(_open == OpenState.connected);
386     }
387     out
388     {
389         assert(_open == OpenState.authenticated);
390     }
391     body
392     {
393         auto token = makeToken(greeting);
394         auto authPacket = buildAuthPacket(token);
395         send(authPacket);
396 
397         auto packet = getPacket();
398         auto okp = OKErrorPacket(packet);
399         enforceEx!MYX(!okp.error, "Authentication failure: " ~ cast(string) okp.message);
400         _open = OpenState.authenticated;
401     }
402 
403     SvrCapFlags _clientCapabilities;
404 
405     void connect(SvrCapFlags clientCapabilities)
406     in
407     {
408         assert(closed);
409     }
410     out
411     {
412         assert(_open == OpenState.authenticated);
413     }
414     body
415     {
416         initConnection();
417         auto greeting = parseGreeting();
418         _open = OpenState.connected;
419 
420         _clientCapabilities = clientCapabilities;
421         setClientFlags(clientCapabilities);
422         authenticate(greeting);
423     }
424     
425     // Forcefully close the socket without sending the quit command.
426     // Needed in case an error leaves communatations in an undefined or non-recoverable state.
427     void kill()
428     {
429         if(_socket.connected)
430             _socket.close();
431         _open = OpenState.notConnected;
432     }
433     
434 public:
435 
436     /**
437      * Construct opened connection.
438      *
439      * After the connection is created, and the initial invitation is received from the server
440      * client preferences can be set, and authentication can then be attempted.
441      *
442      * Parameters:
443      *    socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos,
444      *                 unless -version=Have_vibe_d is used.
445      *    openSocket = Optional callback which should return a newly-opened Phobos
446      *                 or Vibe.d TCP socket. This allows custom sockets to be used,
447      *                 subclassed from Phobos's or Vibe.d's sockets.
448      *    host = An IP address in numeric dotted form, or as a host  name.
449      *    user = The user name to authenticate.
450      *    password = Users password.
451      *    db = Desired initial database.
452      *    capFlags = The set of flag bits from the server's capabilities that the client requires
453      */
454     this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags)
455     {
456         version(Have_vibe_d)
457             enum defaultSocketType = MySQLSocketType.vibed;
458         else
459             enum defaultSocketType = MySQLSocketType.phobos;
460 
461         this(defaultSocketType, host, user, pwd, db, port, capFlags);
462     }
463 
464     ///ditto
465     this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags)
466     {
467         version(Have_vibe_d) {} else
468             enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d");
469 
470         this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD,
471             host, user, pwd, db, port, capFlags);
472     }
473 
474     ///ditto
475     this(OpenSocketCallbackPhobos openSocket,
476         string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags)
477     {
478         this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags);
479     }
480 
481     version(Have_vibe_d)
482     ///ditto
483     this(OpenSocketCallbackVibeD openSocket,
484         string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags)
485     {
486         this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags);
487     }
488 
489     private this(MySQLSocketType socketType,
490         OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD,
491         string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags)
492     in
493     {
494         final switch(socketType)
495         {
496             case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break;
497             case MySQLSocketType.vibed:  assert(openSocketVibeD  !is null); break;
498         }
499     }
500     body
501     {
502         enforceEx!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1");
503         enforceEx!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection");
504         version(Have_vibe_d) {} else
505             enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d");
506 
507         _socketType = socketType;
508         _host = host;
509         _user = user;
510         _pwd = pwd;
511         _db = db;
512         _port = port;
513 
514         _openSocketPhobos = openSocketPhobos;
515         _openSocketVibeD  = openSocketVibeD;
516 
517         connect(capFlags);
518     }
519 
520     /**
521      * Construct opened connection.
522      *
523      * After the connection is created, and the initial invitation is received from
524      * the server client preferences are set, and authentication can then be attempted.
525      *
526      * TBD The connection string needs work to allow for semicolons in its parts!
527      *
528      * Parameters:
529      *    socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos
530      *                 unless -version=Have_vibe_d is used.
531      *    openSocket = Optional callback which should return a newly-opened Phobos
532      *                 or Vibe.d TCP socket. This allows custom sockets to be used,
533      *                 subclassed from Phobos's or Vibe.d's sockets.
534      *    cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld"
535      *    capFlags = The set of flag bits from the server's capabilities that the client requires
536      */
537     this(string cs, SvrCapFlags capFlags = defaultClientFlags)
538     {
539         string[] a = parseConnectionString(cs);
540         this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags);
541     }
542 
543     ///ditto
544     this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags)
545     {
546         string[] a = parseConnectionString(cs);
547         this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags);
548     }
549 
550     ///ditto
551     this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags)
552     {
553         string[] a = parseConnectionString(cs);
554         this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags);
555     }
556 
557     version(Have_vibe_d)
558     ///ditto
559     this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags)
560     {
561         string[] a = parseConnectionString(cs);
562         this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags);
563     }
564 
565     @property bool closed()
566     {
567         return _open == OpenState.notConnected || !_socket.connected;
568     }
569 
570     version(Have_vibe_d)
571     {
572         void acquire() { if( _socket ) _socket.acquire(); }
573         void release() { if( _socket ) _socket.release(); }
574         bool isOwner() { return _socket ? _socket.isOwner() : false; }
575         bool amOwner() { return _socket ? _socket.isOwner() : false; }
576     }
577     else
578     {
579         void acquire() { /+ Do nothing +/ }
580         void release() { /+ Do nothing +/ }
581         bool isOwner() { return !!_socket; }
582         bool amOwner() { return !!_socket; }
583     }
584 
585     /**
586      * Explicitly close the connection.
587      *
588      * This is a two-stage process. First tell the server we are quitting this
589      * connection, and then close the socket.
590      *
591      * Idiomatic use as follows is suggested:
592      * ------------------
593      * {
594      *     auto con = Connection("localhost:user:password:mysqld");
595      *     scope(exit) con.close();
596      *     // Use the connection
597      *     ...
598      * }
599      * ------------------
600      */
601     void close()
602     {
603         if (_open == OpenState.authenticated && _socket.connected)
604             quit();
605 
606         if (_open == OpenState.connected)
607             kill();
608         resetPacket();
609     }
610 
611     void reconnect()
612     {
613         reconnect(_clientCapabilities);
614     }
615 
616     void reconnect(SvrCapFlags clientCapabilities)
617     {
618         bool sameCaps = clientCapabilities == _clientCapabilities;
619         if(!closed)
620         {
621             // Same caps as before?
622             if(clientCapabilities == _clientCapabilities)
623                 return; // Nothing to do, just keep current connection
624 
625             close();
626         }
627 
628         connect(clientCapabilities);
629     }
630 
631     private void quit()
632     in
633     {
634         assert(_open == OpenState.authenticated);
635     }
636     body
637     {
638         sendCmd(CommandType.QUIT, []);
639         // No response is sent for a quit packet
640         _open = OpenState.connected;
641     }
642 
643     static string[] parseConnectionString(string cs)
644     {
645         string[] rv;
646         rv.length = 5;
647         rv[4] = "3306"; // Default port
648         string[] a = split(cs, ";");
649         foreach (s; a)
650         {
651             string[] a2 = split(s, "=");
652             enforceEx!MYX(a2.length == 2, "Bad connection string: " ~ cs);
653             string name = strip(a2[0]);
654             string val = strip(a2[1]);
655             switch (name)
656             {
657                 case "host":
658                     rv[0] = val;
659                     break;
660                 case "user":
661                     rv[1] = val;
662                     break;
663                 case "pwd":
664                     rv[2] = val;
665                     break;
666                 case "db":
667                     rv[3] = val;
668                     break;
669                 case "port":
670                     rv[4] = val;
671                     break;
672                 default:
673                     throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__);
674             }
675         }
676         return rv;
677     }
678 
679     /**
680      * Select a current database.
681      *
682      * Params: dbName = Name of the requested database
683      * Throws: MySQLException
684      */
685     void selectDB(string dbName)
686     {
687         sendCmd(CommandType.INIT_DB, dbName);
688         getCmdResponse();
689         _db = dbName;
690     }
691 
692     /**
693      * Check the server status
694      *
695      * Returns: An OKErrorPacket from which server status can be determined
696      * Throws: MySQLException
697      */
698     OKErrorPacket pingServer()
699     {
700         sendCmd(CommandType.PING, []);
701         return getCmdResponse();
702     }
703 
704     /**
705      * Refresh some feature(s) of the server.
706      *
707      * Returns: An OKErrorPacket from which server status can be determined
708      * Throws: MySQLException
709      */
710     OKErrorPacket refreshServer(RefreshFlags flags)
711     {
712         sendCmd(CommandType.REFRESH, [flags]);
713         return getCmdResponse();
714     }
715 
716     /**
717      * Get a textual report on the server status.
718      *
719      * (COM_STATISTICS)
720      */
721     string serverStats()
722     {
723         sendCmd(CommandType.STATISTICS, []);
724         return cast(string) getPacket();
725     }
726 
727     /**
728      * Enable multiple statement commands
729      *
730      * This can be used later if this feature was not requested in the client capability flags.
731      *
732      * Params: on = Boolean value to turn the capability on or off.
733      */
734     void enableMultiStatements(bool on)
735     {
736         scope(failure) kill();
737 
738         ubyte[] t;
739         t.length = 2;
740         t[0] = on ? 0 : 1;
741         t[1] = 0;
742         sendCmd(CommandType.STMT_OPTION, cast(string) t);
743 
744         // For some reason this command gets an EOF packet as response
745         auto packet = getPacket();
746         enforceEx!MYXProtocol(packet[0] == 254 && packet.length == 5, "Unexpected response to SET_OPTION command");
747     }
748 
749     /// Return the in-force protocol number
750     @property ubyte protocol() pure const nothrow { return _protocol; }
751     /// Server version
752     @property string serverVersion() pure const nothrow { return _serverVersion; }
753     /// Server capability flags
754     @property uint serverCapabilities() pure const nothrow { return _sCaps; }
755     /// Server status
756     @property ushort serverStatus() pure const nothrow { return _serverStatus; }
757     /// Current character set
758     @property ubyte charSet() pure const nothrow { return _sCharSet; }
759     /// Current database
760     @property string currentDB() pure const nothrow { return _db; }
761     /// Socket type being used
762     @property MySQLSocketType socketType() pure const nothrow { return _socketType; }
763 }