1 /** 2 * Store database credentials outside the sourcecode 3 * 4 * This module makes it easy to load credentials from external files and use it for Database Clients. 5 * This module was build with MySQL/MariaDB credentials in mind - but it can surely be used for similar credential structures. 6 * 7 * Authors: Martin Brzenska 8 ' Licence: MIT 9 * 10 */ 11 module credexil; 12 13 import std.typecons : Nullable; 14 15 /** 16 * Holds credentials used for a login 17 * 18 * Authors: Martin Brzenska 19 ' Licence: MIT 20 */ 21 struct Credential 22 { 23 private string __host; 24 private Nullable!ushort __port; 25 private Nullable!string __user; 26 private Nullable!string __pwd; 27 private Nullable!string __db; 28 private string __connectionName; 29 30 public: 31 /// 32 @property Credential host(string host) 33 { 34 this.__host = host; 35 return this; 36 } 37 /// 38 @property string host() { return this.__host; } 39 40 /// 41 @property Credential port(ushort port) 42 { 43 import std.conv : to; 44 this.__port = port; 45 return this; 46 } 47 Nullable!ushort port() @property 48 { return this.__port; } 49 /// 50 @property Credential user(string user) 51 { 52 this.__user = user; 53 return this; 54 } 55 /// 56 @property Nullable!string user() { return this.__user; } 57 58 /// 59 @property Credential pwd(string pwd) 60 { 61 this.__pwd = pwd; 62 return this; 63 } 64 /// 65 @property Nullable!string pwd() { return this.__pwd; } 66 67 /// 68 @property Credential db(string db) 69 { 70 this.__db = db; 71 return this; 72 } 73 /// 74 @property Nullable!string db() { return this.__db; } 75 76 /// 77 @property Credential connectionName(string connectionName) 78 { 79 this.__connectionName = connectionName; 80 return this; 81 } 82 /// 83 const string connectionName() @property { return this.__connectionName; } 84 85 /// 86 @property string connectionString() 87 { 88 import std.conv : to; 89 import std.string : chop; 90 return 91 chop( 92 "host="~this.host~";" 93 ~ (this.port.isNull ? "" : "port="~this.port.to!string~";") 94 ~ (this.user.isNull ? "" : "user="~this.user~";") 95 ~ (this.pwd.isNull ? "" : "pwd="~this.pwd~";") 96 ~ (this.db.isNull ? "" : "db="~this.db~";") 97 ); 98 } 99 /// 100 unittest 101 { 102 auto cred = Credential().host("localhost"); 103 assert(cred.connectionString == "host=localhost"); 104 105 cred = Credential().host("127.0.0.1").port(3306); 106 assert(cred.connectionString == "host=127.0.0.1;port=3306"); 107 108 cred = Credential().host("localhost").user("testuser").db("SchemaXY"); 109 assert(cred.connectionString == "host=localhost;user=testuser;db=SchemaXY"); 110 assert(cred.host == "localhost"); 111 112 } 113 114 string toString() 115 { 116 return this.connectionString; 117 } 118 } 119 120 /** 121 * Loads credentials from the given file 122 * 123 * The file to load must have the appropriate format (see example below). 124 * One file can hold multiple credential entities, each consisting of a name and a set of variables. 125 * 126 * Examples: 127 * ----------------- 128 * [connection_name] 129 * host = 127.0.0.1 130 * port = 3306 131 * user = MyUser 132 * pwd = MyPassword 133 * db = MyDatabase 134 * ----------------- 135 * 136 * Throws: CredentialException if file is not formated as expected 137 * Throws: ErrnoException if file could not be opened. 138 * 139 * Authors: Martin Brzenska 140 ' Licence: MIT 141 * 142 */ 143 public Credential[] load(string file) 144 { 145 import std.algorithm.searching : maxElement , startsWith; 146 import std.array : array; 147 import std.format : format; 148 import std.regex; 149 import std.stdio : File; 150 import std.string : strip; 151 import std.uni : toLower; 152 import std.conv : to , ConvOverflowException , ConvException; 153 154 auto connectionName = ctRegex!(`^[\s]*\[([^\]]+)\][\s]*$`); 155 auto variable = ctRegex!(`^[\s]*(host|port|user|pwd|db)[\s]*=[\s]*(.*)[\s]*$`); 156 157 immutable string FERR_OBLIGATORY_PARAMS = "'host' variable is missing for Credential '%s'"; 158 159 Credential[] credentials; 160 161 /* 162 * is temporarily used to build up a Credential Entity 163 */ 164 Credential credential; 165 166 foreach( file_lineno , line ; File(file).byLineCopy.array ) 167 { 168 auto nameCapture = line.matchFirst(connectionName); 169 if( ! nameCapture.empty ) 170 { 171 /* 172 * This is the beginning of a new connection 173 */ 174 175 if(credential.connectionName != credential.connectionName.init) 176 { 177 /* 178 * If there was a connection before, make sure that all obligatory variables from the previous credentialEntity are set. 179 * If ok, add the previous entity to the result array. 180 */ 181 if(credential.host == credential.host.init) 182 { 183 throw new CredentialException(format(FERR_OBLIGATORY_PARAMS , credential.connectionName )); 184 } 185 else 186 { 187 credentials ~= credential; 188 } 189 } 190 191 /* 192 * Check if we already had a connection with this name 193 */ 194 foreach( cred ; credentials ) 195 { 196 if( cred.connectionName == nameCapture[1].toLower ) 197 { 198 throw new CredentialException( format( "The Credential Entity Name must be unique ('%s')" , nameCapture[1].toLower ) ); 199 } 200 } 201 202 credential = Credential(); 203 credential.connectionName = nameCapture[1].toLower; 204 } 205 206 auto variableCapture = line.matchFirst(variable); 207 if( ! variableCapture.empty ) 208 { 209 switch(variableCapture[1].toLower) 210 { 211 case "host": 212 if(credential.host.length) 213 throw new CredentialException(format("Multiple definitions for '%s' in '%s'" , variableCapture[1].toLower, credential.connectionName) ); 214 if( ! variableCapture[2].length) 215 throw new CredentialException(format("variable '%s' cannot be empty at line '%s:%d'" , variableCapture[1].toLower , file , 1+file_lineno) ); 216 217 credential.host = variableCapture[2]; 218 break; 219 220 case "port": 221 if( ! credential.port.isNull) 222 throw new CredentialException(format("Multiple definitions for port in '%s'" , credential.connectionName) ); 223 if( ! variableCapture[2].length) 224 throw new CredentialException(format("if '%s' is given, it cannot be empty at line '%s:%d'" , variableCapture[1].toLower , file , 1+file_lineno) ); 225 226 try { 227 credential.port = variableCapture[2].to!ushort; 228 } 229 catch( ConvException e ) 230 { 231 throw new CredentialException(format( "port must have a value between %d and %d in '%s:%d'" , credential.port.min , credential.port.max , file , 1+file_lineno )); 232 } 233 break; 234 235 case "user": 236 if( ! credential.user.isNull) 237 throw new CredentialException(format("Multiple definitions for user in '%s'" , credential.connectionName) ); 238 if( ! variableCapture[2].length) 239 throw new CredentialException(format("apply a non empty value to '%s' or leave it completely in '%s:%d'" , variableCapture[1].toLower , file , 1+file_lineno) ); 240 241 credential.user = variableCapture[2]; 242 break; 243 244 case "pwd": 245 if( ! credential.pwd.isNull ) 246 throw new CredentialException(format("Multiple definitions for pwd in '%s'" , credential.connectionName) ); 247 if( ! variableCapture[2].length) 248 throw new CredentialException(format("apply a non empty value to '%s' or leave it completely in '%s:%d'" , variableCapture[1].toLower , file , 1+file_lineno) ); 249 250 credential.pwd = variableCapture[2]; 251 break; 252 253 case "db": 254 if( ! credential.db.isNull ) 255 throw new CredentialException(format("Multiple definitions for db in '%s'" , credential.connectionName) ); 256 credential.db = variableCapture[2]; 257 break; 258 259 default: 260 throw new CredentialException(format("unknown variable in '%s:%d'" , file , 1+file_lineno) ); 261 } 262 } 263 264 if( 265 nameCapture.empty 266 && variableCapture.empty 267 && false == line.startsWith("#",";") 268 && line.strip.length 269 ) { 270 throw new CredentialException(format("Syntax Error at line %d" , 1+file_lineno) ); 271 } 272 } 273 274 /* 275 * Handle the last Credential Entity 276 * Check if all obligatory parameters are given 277 */ 278 if( 279 credential.connectionName != credential.connectionName.init 280 && credential.host == credential.host.init 281 ) { 282 throw new CredentialException(format(FERR_OBLIGATORY_PARAMS , credential.connectionName )); 283 } 284 else 285 { 286 credentials ~= credential; 287 } 288 289 return credentials; 290 } 291 /// 292 unittest { 293 Credential[] credentials = .load("./source/test.cred"); 294 assert(credentials.length == 3); 295 296 assert(credentials.get("connection_name").connectionName == "connection_name"); 297 assert(credentials.get("connection_name").host == "127.0.0.1"); 298 assert(credentials.get("connection_name").port == 3306); 299 assert(credentials.get("connection_name").user == "MyUser"); 300 assert(credentials.get("connection_name").pwd == "MyPassword"); 301 assert(credentials.get("connection_name").db == "MyDatabase"); 302 assert(credentials.get("connection_name").connectionString == "host=127.0.0.1;port=3306;user=MyUser;pwd=MyPassword;db=MyDatabase"); 303 304 } 305 unittest 306 { 307 Credential _2ndTestCred = .load("./source/test.cred").get("2nd test connection"); 308 309 assert(_2ndTestCred.connectionName == "2nd test connection" ); 310 assert(_2ndTestCred.connectionString == "host=localhost;user=some_user;pwd=1234" ); 311 assert(_2ndTestCred.host == "localhost" ); 312 assert(_2ndTestCred.user == "some_user" ); 313 assert(_2ndTestCred.pwd == "1234" ); 314 assert(_2ndTestCred.port.isNull ); 315 assert(_2ndTestCred.db.isNull ); 316 317 } 318 319 /** 320 * Gets the Credential entity with the given name 321 * 322 * Throws: core.exception.RangeError if name cannot be found 323 * 324 * Authors: Martin Brzenska 325 ' Licence: MIT 326 */ 327 Credential get(in Credential[] credentials , string name ) 328 { 329 import std.algorithm.searching : countUntil; 330 return credentials[ credentials.countUntil!(cred => cred.connectionName == name) ]; 331 } 332 unittest 333 { 334 auto credentials = .load("./source/test.cred"); 335 assert(credentials.get("connection_name").connectionName == "connection_name"); 336 } 337 338 class CredentialException : Exception 339 { 340 this(string msg) { super(msg); } 341 }