1 /*
2  *             Copyright Andrej Mitrovic 2013.
3  *  Distributed under the Boost Software License, Version 1.0.
4  *     (See accompanying file LICENSE_1_0.txt or copy at
5  *           http://www.boost.org/LICENSE_1_0.txt)
6  */
7 module git.credentials;
8 
9 import std.conv;
10 import std.exception;
11 import std.string;
12 import std.typecons;
13 
14 import git2.transport;
15 
16 import git.exception;
17 import git.util;
18 
19 version (GIT_SSH)
20 {
21     static assert(0, "dlibgit does not support SSH yet.");
22 }
23 
24 /* The base structure for all credential types. */
25 struct GitCred
26 {
27     // internal
28     private this(git_cred* cred)
29     {
30         _data = Data(cred);
31     }
32 
33     /**
34         Return the actual credential type of this credential.
35         Use the $(D get) template to cast this type to the
36         type tagged as $(D credType).
37     */
38     @property GitCredType credType()
39     {
40         return _data._payload.credtype.toDlibGitCredType();
41     }
42 
43     /** Throw if the target credential type is not equal to credType that's stored. */
44     void verifyTypeMatch(GitCredType target)
45     {
46         enforceEx!GitException(credType == target,
47                                format("Tried to cast GitCred of type '%s' to type '%s'",
48                                       credType, target));
49     }
50 
51     /**
52         Cast this credential to the structure type matching
53         the $(D cred) enum tag.
54 
55         If the underlying type does not match the target type,
56         a $(D GitException) is thrown.
57     */
58     auto get(GitCredType cred)()
59     {
60         return get!(_credToType!cred);
61     }
62 
63     /**
64         Cast this credential to a specific credential type $(D T).
65 
66         If the underlying type does not match the target type,
67         a $(D GitException) is thrown.
68     */
69     T get(T)() if (isGitCredential!T)
70     {
71         verifyTypeMatch(T.credType);
72         return getImpl!T;
73     }
74 
75 package:
76     /**
77      * The internal libgit2 handle for this object.
78      *
79      * Care should be taken not to escape the reference outside a scope where
80      * a GitCred encapsulating the handle is kept alive.
81      */
82     @property git_cred* cHandle()
83     {
84         return _data._payload;
85     }
86 
87 private:
88 
89     T getImpl(T : GitCred_PlainText)()
90     {
91         auto cred = cast(T.c_cred_struct*)_data._payload;
92 
93         T result;
94         result.parent = this;
95         result.username = to!string(cred.username);
96         result.password = to!string(cred.password);
97         return result;
98     }
99 
100     version (GIT_SSH)
101     {
102         static assert(0, "dlibgit does not support SSH yet.");
103 
104         T getImpl(T : GitCred_KeyFilePassPhrase)()
105         {
106             auto cred = cast(T.c_cred_struct*)_data._payload;
107 
108             T result;
109             result.parent = this;
110             result.publickey = to!string(cred.publickey);
111             result.privatekey = to!string(cred.privatekey);
112             result.passphrase = to!string(cred.passphrase);
113             return result;
114         }
115 
116         T getImpl(T : GitCred_PublicKey)()
117         {
118             auto cred = cast(T.c_cred_struct*)_data._payload;
119 
120             T result;
121             result.parent = this;
122             result.publickey = cred.publickey[0 .. cred.publickey_len];
123             result.sign_callback = cred.sign_callback;
124             result.sign_data = cred.sign_data;
125             return result;
126         }
127     }
128 
129     /** Payload for the $(D git_cred) object which should be refcounted. */
130     struct Payload
131     {
132         this(git_cred* payload)
133         {
134             _payload = payload;
135         }
136 
137         ~this()
138         {
139             //~ writefln("- %s", __FUNCTION__);
140 
141             if (_payload !is null)
142             {
143                 _payload.free(_payload);
144                 _payload = null;
145             }
146         }
147 
148         /// Should never perform copy
149         @disable this(this);
150 
151         /// Should never perform assign
152         @disable void opAssign(typeof(this));
153 
154         git_cred* _payload;
155     }
156 
157     alias RefCounted!(Payload, RefCountedAutoInitialize.no) Data;
158     Data _data;
159 }
160 
161 
162 ///
163 enum GitCredType
164 {
165     ///
166     plaintext = 1 << 0,
167 
168     ///
169     passphrase = 1 << 1,
170 
171     ///
172     publickey = 1 << 2,
173 
174     ///
175     sshKey = 1 << 3,
176 
177     ///
178     sshCustom = 1 << 4,
179 
180     ///
181     default_ = 1 << 5
182 }
183 
184 /* A plaintext username and password. */
185 struct GitCred_PlainText
186 {
187     ///
188     enum credType = GitCredType.plaintext;
189 
190     ///
191 	GitCred parent;
192 
193     ///
194 	string username;
195 
196     ///
197 	string password;
198 
199     private alias c_cred_struct = git_cred_userpass_plaintext;
200 }
201 
202 version (GIT_SSH)
203 {
204     static assert(0, "dlibgit does not support SSH yet.");
205 
206     /* A ssh key file and passphrase. */
207     struct GitCred_KeyFilePassPhrase
208     {
209         ///
210         enum credType = GitCredType.passphrase;
211 
212         ///
213         GitCred parent;
214 
215         ///
216         string publicKey;
217 
218         ///
219         string privateKey;
220 
221         ///
222         string passPhrase;
223 
224         private alias c_cred_struct = git_cred_ssh_keyfile_passphrase;
225     }
226 
227     /* A ssh public key and authentication callback. */
228     struct GitCred_PublicKey
229     {
230         ///
231         enum credType = GitCredType.publickey;
232 
233         ///
234         GitCred parent;
235 
236         ///
237         ubyte[] publicKey;
238 
239         ///
240         void* signCallback;
241 
242         ///
243         void* signData;
244 
245         private alias c_cred_struct = git_cred_ssh_publickey;
246     }
247 }
248 
249 /** Check if type $(D T) is one of the supported git credential types. */
250 template isGitCredential(T)
251 {
252     version (GIT_SSH)
253     {
254         static assert(0, "dlibgit does not support SSH yet.");
255 
256         enum bool isGitCredential = is(T == GitCred_PlainText) ||
257                                     is(T == GitCred_KeyFilePassPhrase) ||
258                                     is(T == GitCred_PublicKey);
259     }
260     else
261     {
262         enum bool isGitCredential = is(T == GitCred_PlainText);
263     }
264 }
265 
266 // helper
267 private template _credToType(GitCredType credType)
268 {
269     static if (credType == GitCredType.plaintext)
270         alias _credToType = GitCred_PlainText;
271     else static if (targetLibGitVersion >= VerionInfo(0, 20, 0)) {
272         static if (credType == GitCredType.sshKey)
273             alias _credToType = GitCred_SSHKey;
274         else static if (credType == GitCredType.sshCustom)
275             alias _credToType = GitCred_SSHCustom;
276         else static assert(false);
277     } else {
278         version (GIT_SSH)
279         {
280             static assert(0, "dlibgit does not support SSH yet.");
281 
282             static if (credType == GitCredType.passphrase)
283                 alias _credToType = GitCred_KeyFilePassPhrase;
284             else
285             static if (credType == GitCredType.publickey)
286                 alias _credToType = GitCred_PublicKey;
287             else
288             static assert(0);
289         }
290         else
291         static assert(0);
292     }
293 }
294 
295 private GitCredType toDlibGitCredType(git_credtype_t cred_type)
296 {
297     GitCredType ret = cast(GitCredType)0;
298     if (cred_type & GIT_CREDTYPE_USERPASS_PLAINTEXT) ret |= GitCredType.plaintext;
299     if (cred_type & GIT_CREDTYPE_USERPASS_PLAINTEXT) ret |= GitCredType.plaintext;
300     static if (targetLibGitVersion >= VersionInfo(0, 20, 0)) {
301         if (cred_type & GIT_CREDTYPE_SSH_KEY) ret |= GitCredType.sshKey;
302         if (cred_type & GIT_CREDTYPE_SSH_CUSTOM) ret |= GitCredType.sshCustom;
303         if (cred_type & GIT_CREDTYPE_DEFAULT) ret |= GitCredType.default_;
304     } else {
305         if (cred_type & GIT_CREDTYPE_SSH_KEY) ret |= GitCredType.sshKey;
306         if (cred_type & GIT_CREDTYPE_SSH_CUSTOM) ret |= GitCredType.sshCustom;
307     }
308     return ret;
309  }
310 
311 /**
312     Creates a new plain-text username and password credential object.
313     The supplied credential parameter will be internally duplicated.
314 */
315 GitCred getCredPlainText(string username, string password)
316 {
317     git_cred* _git_cred;
318     require(git_cred_userpass_plaintext_new(&_git_cred, username.gitStr, password.gitStr) == 0);
319     return GitCred(_git_cred);
320 }
321 
322 ///
323 unittest
324 {
325     auto cred = getCredPlainText("user", "pass");
326 
327     switch (cred.credType) with (GitCredType)
328     {
329         case plaintext:
330         {
331             version (GIT_SSH)
332             {
333                 static assert(0, "dlibgit does not support SSH yet.");
334 
335                 // throw when trying to cast to an inappropriate type
336                 assertThrown!GitException(cred.get!passphrase);
337 
338                 // ditto
339                 assertThrown!GitException(cred.get!GitCred_KeyFilePassPhrase);
340             }
341 
342             // use enum for the get template
343             auto cred1 = cred.get!plaintext;
344             assert(cred1.username == "user");
345             assert(cred1.password == "pass");
346 
347             // or use a type
348             auto cred2 = cred.get!GitCred_PlainText;
349             assert(cred2.username == "user");
350             assert(cred2.password == "pass");
351 
352             break;
353         }
354 
355         default: assert(0, text(cred.credType));
356     }
357 }
358 
359 version (GIT_SSH)
360 {
361     static assert(0, "dlibgit does not support SSH yet.");
362 
363     /**
364         Creates a new ssh key file and passphrase credential object.
365         The supplied credential parameter will be internally duplicated.
366 
367         Params:
368 
369         - $(D publicKey): The path to the public key of the credential.
370         - $(D privateKey): The path to the private key of the credential.
371         - $(D passPhrase): The passphrase of the credential.
372     */
373     GitCred getCredKeyFilePassPhrase(string publicKey, string privateKey, string passPhrase)
374     {
375         git_cred* _git_cred;
376         require(git_cred_ssh_keyfile_passphrase_new(&_git_cred, publicKey.gitStr, privateKey.gitStr, passPhrase.gitStr) == 0);
377         return GitCred(_git_cred);
378     }
379 
380     ///
381     unittest
382     {
383         auto cred = getCredKeyFilePassPhrase("public", "private", "passphrase");
384 
385         switch (cred.credType) with (GitCredType)
386         {
387             case passphrase:
388             {
389                 // throw when trying to cast to an inappropriate type
390                 assertThrown!GitException(cred.get!plaintext);
391 
392                 // ditto
393                 assertThrown!GitException(cred.get!GitCred_PlainText);
394 
395                 // use enum for the get template
396                 auto cred1 = cred.get!passphrase;
397                 assert(cred1.publicKey == "public");
398                 assert(cred1.privateKey == "private");
399                 assert(cred1.passPhrase == "passphrase");
400 
401                 // or use a type
402                 auto cred2 = cred.get!GitCred_KeyFilePassPhrase;
403                 assert(cred2.publicKey == "public");
404                 assert(cred2.privateKey == "private");
405                 assert(cred2.passPhrase == "passphrase");
406 
407                 break;
408             }
409 
410             default: assert(0, text(cred.credType));
411         }
412     }
413 
414     /**
415         Creates a new ssh public key credential object.
416         The supplied credential parameter will be internally duplicated.
417 
418         Params:
419 
420         - $(D publicKey): The bytes of the public key.
421         - $(D signCallback): The callback method for authenticating.
422         - $(D signData): The abstract data sent to the $(D signCallback) method.
423     */
424     GitCred getCredPublicKey(ubyte[] publicKey, void* signCallback, void* signData)
425     {
426         git_cred* _git_cred;
427         require(git_cred_ssh_publickey_new(&_git_cred, publicKey.ptr, publicKey.length, signCallback, signData) == 0);
428         return GitCred(_git_cred);
429     }
430 
431     ///
432     unittest
433     {
434         auto cred = getCredPublicKey([], null, null);
435 
436         switch (cred.credType) with (GitCredType)
437         {
438             case publickey:
439             {
440                 // throw when trying to cast to an inappropriate type
441                 assertThrown!GitException(cred.get!plaintext);
442 
443                 // ditto
444                 assertThrown!GitException(cred.get!GitCred_PlainText);
445 
446                 // use enum for the get template
447                 auto cred1 = cred.get!publickey;
448                 assert(cred1.publicKey == []);
449                 assert(cred1.signCallback is null);
450                 assert(cred1.signData is null);
451 
452                 // or use a type
453                 auto cred2 = cred.get!GitCred_PublicKey;
454                 assert(cred2.publicKey == []);
455                 assert(cred2.signCallback is null);
456                 assert(cred2.signData is null);
457 
458                 break;
459             }
460 
461             default: assert(0, text(cred.credType));
462         }
463     }
464 }
465 
466 alias GitCredAcquireDelegate = GitCred delegate(
467     in char[] url,
468     in char[] usernameFromURL,
469     uint allowedTypes);