/[openpgpmdrv]/trunk/OpenPGPminidriver/PublicDataOperations.c
ViewVC logotype

Annotation of /trunk/OpenPGPminidriver/PublicDataOperations.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 10 - (hide annotations)
Mon Mar 15 18:23:17 2010 UTC (15 years, 1 month ago) by vletoux
File MIME type: text/plain
File size: 14638 byte(s)
first beta version
1 vletoux 1 /* OpenPGP Smart Card Mini Driver
2     Copyright (C) 2009 Vincent Le Toux
3    
4     This library is Free software; you can redistribute it and/or
5     modify it under the terms of the GNU Lesser General Public
6     License version 2.1 as published by the Free Software Foundation.
7    
8     This library is distributed in the hope that it will be useful,
9     but WITHOUT ANY WARRANTY; without even the implied warranty of
10     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11     Lesser General Public License for more details.
12    
13     You should have received a copy of the GNU Lesser General Public
14     License along with this library; if not, write to the Free Software
15     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16     */
17    
18     #include <windows.h>
19     #include <stdio.h>
20     #include <cardmod.h>
21     #include "Tracing.h"
22     #include "Context.h"
23     #include "SmartCard.h"
24     #include "PublicDataOperations.h"
25     #include "CryptoOperations.h"
26 vletoux 6 #include "tlv.h"
27 vletoux 1
28     typedef enum _OPENPGP_FILE_TYPE
29     {
30     StoredOnSmartCard,
31     Virtual,
32     } OPENPGP_FILE_TYPE;
33    
34 vletoux 9 #define OPENPGP_FILE_OPTIONAL 1
35     #define OPENPGP_FILE_WRITE_ONLY 2
36     #define OPENPGP_FILE_NULL_LENGHT_EQUALS_MISSING 4
37 vletoux 10 #define OPENPGP_FILE_CONF_IS_AUTH 8
38 vletoux 9
39 vletoux 1 typedef struct _OPENPGP_FILE
40     {
41     PCHAR szDirectory;
42     PCHAR szFile;
43     OPENPGP_FILE_TYPE dwFileType;
44 vletoux 6 DWORD dwTag;
45     DWORD dwTlv;
46 vletoux 1 CARD_FILE_ACCESS_CONDITION dwAccess;
47 vletoux 9 DWORD dwFlag;
48 vletoux 1 } OPENPGP_FILE, *POPENPGP_FILE;
49    
50    
51     #define szCARD_APPLICATION_FILE "cardapps"
52    
53     OPENPGP_FILE Files[] =
54     {
55 vletoux 6 {szOpenPGPDir, szOpenPGPFingerprint, StoredOnSmartCard, 0x6E, 0xC5, EveryoneReadAdminWriteAc},
56     {szOpenPGPDir, szOpenPGPStatus, StoredOnSmartCard, 0xC4, 0, EveryoneReadAdminWriteAc},
57     {szOpenPGPDir, szOpenPGPApplicationIdentifier, StoredOnSmartCard, 0x4F, 0, UnknownAc},
58     {szOpenPGPDir, szOpenPGPLogin, StoredOnSmartCard, 0x5E, 0, EveryoneReadAdminWriteAc},
59     {szOpenPGPDir, szOpenPGPName, StoredOnSmartCard, 0x65, 0x5B, EveryoneReadAdminWriteAc},
60     {szOpenPGPDir, szOpenPGPLanguage, StoredOnSmartCard, 0x65, 0x5F2D, EveryoneReadAdminWriteAc},
61     {szOpenPGPDir, szOpenPGPSex, StoredOnSmartCard, 0x65, 0x5F35,EveryoneReadAdminWriteAc},
62     {szOpenPGPDir, szOpenPGPUrl, StoredOnSmartCard, 0x5F50, 0, EveryoneReadAdminWriteAc},
63     {szOpenPGPDir, szOpenPGPHistoricalBytes, StoredOnSmartCard, 0x5F52, 0, UnknownAc},
64 vletoux 9 {szOpenPGPDir, szOpenPGPCertificate, StoredOnSmartCard, 0x7F21, 0, EveryoneReadAdminWriteAc, OPENPGP_FILE_NULL_LENGHT_EQUALS_MISSING},
65 vletoux 6 {szOpenPGPDir, szOpenPGPExtendedCap, StoredOnSmartCard, 0x6E, 0xC0, UnknownAc},
66     {szOpenPGPDir, szOpenPGPAlgoAttributesSignature, StoredOnSmartCard, 0x6E, 0xC1, UnknownAc},
67     {szOpenPGPDir, szOpenPGPAlgoAttributesDecryption, StoredOnSmartCard, 0x6E, 0xC2,UnknownAc},
68     {szOpenPGPDir, szOpenPGPAlgoAttributesAuthentication, StoredOnSmartCard, 0x6E, 0xC3, UnknownAc },
69 vletoux 9 {szOpenPGPDir, szOpenPGPPUK, StoredOnSmartCard, 0xD3, 0, UnknownAc, OPENPGP_FILE_WRITE_ONLY },
70 vletoux 6 {NULL, szCARD_IDENTIFIER_FILE, StoredOnSmartCard, 0x4F, 0, EveryoneReadAdminWriteAc},
71 vletoux 1 {NULL, szCARD_APPLICATION_FILE, Virtual, 0, 0, EveryoneReadAdminWriteAc},
72     {NULL, szCACHE_FILE, Virtual, 0, 0, EveryoneReadUserWriteAc},
73     {szBASE_CSP_DIR, szCONTAINER_MAP_FILE, Virtual, 0, 0, EveryoneReadUserWriteAc},
74 vletoux 10 {szBASE_CSP_DIR, "ksc1", StoredOnSmartCard, 0x7F21, 0, EveryoneReadAdminWriteAc, OPENPGP_FILE_NULL_LENGHT_EQUALS_MISSING | OPENPGP_FILE_CONF_IS_AUTH},
75     {szBASE_CSP_DIR, "ksc2", StoredOnSmartCard, 0x7F21, 0, EveryoneReadAdminWriteAc, OPENPGP_FILE_NULL_LENGHT_EQUALS_MISSING},
76 vletoux 1
77     };
78    
79     DWORD dwFileCount = ARRAYSIZE(Files);
80    
81 vletoux 8 DWORD OCardDirectoryList(__in PCARD_DATA pCardData,
82 vletoux 1 __in PBYTE* pbResponse, __in_opt PDWORD pdwResponseSize)
83     {
84     // hardcoded
85     *pdwResponseSize = 16;
86     *pbResponse = pCardData->pfnCspAlloc(*pdwResponseSize);
87     if (!*pbResponse)
88     {
89     return SCARD_E_NO_MEMORY;
90     }
91     memcpy(*pbResponse, "openpgp\0mscp\0\0\0\0", *pdwResponseSize);
92     return 0;
93     }
94    
95 vletoux 8
96 vletoux 1 // read file
97 vletoux 8 DWORD OCardReadFile(__in PCARD_DATA pCardData,
98 vletoux 1 __in_opt PSTR szDirectory, __in PSTR szFile,
99 vletoux 6 __in PBYTE* ppbResponse, __in PDWORD pdwResponseSize)
100 vletoux 1 {
101     DWORD dwI;
102     DWORD dwReturn = 0;
103     BOOL fDirectoryFound = FALSE;
104     BOOL fFileFound = FALSE;
105     BYTE pbCmd[] = {0x00, 0xCA, 0x00, 0x00, 0x00};
106     DWORD dwCmdSize = ARRAYSIZE(pbCmd);
107     POPENPGP_CONTEXT pContext = (POPENPGP_CONTEXT) pCardData->pvVendorSpecific;
108 vletoux 6 PBYTE pbData = NULL;
109 vletoux 1 __try
110     {
111 vletoux 6 *pdwResponseSize = 0;
112 vletoux 1 for(dwI = 0; dwI < dwFileCount; dwI++)
113     {
114     BOOL fMatch = FALSE;
115     if (szDirectory == NULL)
116     {
117     if (!Files[dwI].szDirectory) fMatch = TRUE;
118     }
119     else
120     {
121 vletoux 8 if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
122 vletoux 1 }
123     if (fMatch)
124     {
125     fDirectoryFound = TRUE;
126 vletoux 8 if (_stricmp(szFile, Files[dwI].szFile) == 0)
127 vletoux 1 {
128     fFileFound = TRUE;
129     break;
130     }
131     }
132     }
133     if (!fFileFound)
134     {
135     if (fDirectoryFound)
136     {
137     dwReturn = SCARD_E_FILE_NOT_FOUND;
138     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_FILE_NOT_FOUND %S",szFile);
139     }
140     else
141     {
142     dwReturn = SCARD_E_DIR_NOT_FOUND;
143     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_DIR_NOT_FOUND %S",szDirectory);
144     }
145     __leave;
146     }
147     if (Files[dwI].dwFileType == StoredOnSmartCard)
148     {
149 vletoux 6 pbCmd[2] = (BYTE) (Files[dwI].dwTag / 0x100);
150     pbCmd[3] = (BYTE) (Files[dwI].dwTag % 0x100);
151 vletoux 8 dwReturn = OCardGetData(pCardData, pbCmd, dwCmdSize, &pbData, pdwResponseSize);
152 vletoux 6 if (dwReturn)
153     {
154     __leave;
155     }
156     if (Files[dwI].dwTlv)
157     {
158     PBYTE pbPointer;
159     //TraceDump(0,pbData,*pdwResponseSize);
160     if (find_tlv(pbData, Files[dwI].dwTlv, *pdwResponseSize, &pbPointer, pdwResponseSize))
161     {
162     *ppbResponse = pCardData->pfnCspAlloc(*pdwResponseSize);
163     if (!*ppbResponse )
164     {
165     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_NO_MEMORY");
166     dwReturn = SCARD_E_NO_MEMORY;
167     }
168     memcpy(*ppbResponse, pbPointer, *pdwResponseSize);
169     }
170     else
171     {
172     dwReturn = SCARD_E_FILE_NOT_FOUND;
173     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_FILE_NOT_FOUND %S",szFile);
174     }
175     }
176     else
177     {
178     *ppbResponse = pbData;
179     // do not free the data !
180     pbData = NULL;
181     }
182 vletoux 1 }
183     else
184     {
185     if (szDirectory == NULL)
186     {
187 vletoux 8 if (_stricmp(szFile, szCARD_APPLICATION_FILE) == 0)
188 vletoux 1 {
189 vletoux 8 dwReturn = OCardDirectoryList(pCardData, ppbResponse, pdwResponseSize);
190 vletoux 1 }
191 vletoux 8 else if (_stricmp(szFile, szCACHE_FILE) == 0)
192 vletoux 1 {
193     *pdwResponseSize = sizeof(CARD_CACHE_FILE_FORMAT);
194 vletoux 6 *ppbResponse = pCardData->pfnCspAlloc(*pdwResponseSize);
195     memset(*ppbResponse,0,*pdwResponseSize);
196 vletoux 1 }
197     else
198     {
199     dwReturn = SCARD_E_FILE_NOT_FOUND;
200     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_FILE_NOT_FOUND %S",szFile);
201     }
202     }
203 vletoux 8 else if (_stricmp(szDirectory,szBASE_CSP_DIR) == 0)
204 vletoux 1 {
205 vletoux 8 if (_stricmp(szFile, szCONTAINER_MAP_FILE) == 0)
206 vletoux 1 {
207 vletoux 8 dwReturn = OCardReadContainerMapFile(pCardData, ppbResponse, pdwResponseSize);
208 vletoux 1 }
209     else
210     {
211     dwReturn = SCARD_E_FILE_NOT_FOUND;
212     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_FILE_NOT_FOUND %S",szFile);
213     }
214     }
215     else
216     {
217     dwReturn = SCARD_E_DIR_NOT_FOUND;
218     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_DIR_NOT_FOUND %S",szDirectory);
219     }
220     }
221     if (dwReturn)
222     {
223     __leave;
224     }
225     // add to the cache
226     dwReturn = 0;
227    
228     }
229     __finally
230     {
231 vletoux 6 if( pbData)
232     pCardData->pfnCspFree(pbData);
233 vletoux 1 }
234 vletoux 6 Trace(WINEVENT_LEVEL_VERBOSE, L"%S\\%S dwReturn = 0x%08X size = %d",szDirectory, szFile, dwReturn, *pdwResponseSize);
235 vletoux 1 return dwReturn;
236     }
237    
238 vletoux 8 DWORD OCardEnumFile(__in PCARD_DATA pCardData,
239 vletoux 1 __in_opt PSTR szDirectory,
240     __in PBYTE* pbResponse, __in PDWORD pdwResponseSize)
241     {
242 vletoux 10 DWORD dwReturn = 0, dwTempReturn;
243 vletoux 1 DWORD dwI, dwSize;
244     BOOL fDirectoryFound = FALSE;
245 vletoux 9 BOOL fAddToOuput;
246 vletoux 1 __try
247     {
248     *pbResponse = NULL;
249     *pdwResponseSize = 0;
250 vletoux 9
251     // compute the max size of the buffer
252     dwSize = 0;
253 vletoux 1 for(dwI = 0; dwI < dwFileCount; dwI++)
254     {
255     BOOL fMatch = FALSE;
256     if (szDirectory == NULL)
257     {
258     if (!Files[dwI].szDirectory) fMatch = TRUE;
259     }
260     else
261     {
262 vletoux 8 if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
263 vletoux 1 }
264 vletoux 9 if (fMatch && !(Files[dwI].dwFileType & OPENPGP_FILE_WRITE_ONLY))
265     {
266     dwSize += (DWORD) strlen( Files[dwI].szFile) + 1;
267     }
268     }
269     dwSize += 1;
270     *pbResponse = pCardData->pfnCspAlloc(dwSize);
271     if (!*pbResponse)
272     {
273     dwReturn = SCARD_E_NO_MEMORY;
274     __leave;
275     }
276     for(dwI = 0; dwI < dwFileCount; dwI++)
277     {
278     BOOL fMatch = FALSE;
279     if (szDirectory == NULL)
280     {
281     if (!Files[dwI].szDirectory) fMatch = TRUE;
282     }
283     else
284     {
285     if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
286     }
287 vletoux 1 if (fMatch)
288     {
289     fDirectoryFound = TRUE;
290 vletoux 9 fAddToOuput = TRUE;
291     if (Files[dwI].dwFlag & OPENPGP_FILE_WRITE_ONLY)
292 vletoux 1 {
293 vletoux 9 fAddToOuput = FALSE;
294     }
295     if (fAddToOuput && (Files[dwI].dwFlag & OPENPGP_FILE_NULL_LENGHT_EQUALS_MISSING))
296     {
297 vletoux 1 PBYTE pbData = NULL;
298     DWORD dwSize;
299 vletoux 9 fAddToOuput = FALSE;
300 vletoux 1 // check if the file exists and be read
301 vletoux 10 dwTempReturn = OCardReadFile(pCardData, szDirectory, Files[dwI].szFile, &pbData, &dwSize);
302     if (!dwTempReturn)
303 vletoux 1 {
304     pCardData->pfnCspFree(pbData);
305 vletoux 9 if (dwSize > 0)
306     {
307     fAddToOuput = TRUE;
308     }
309 vletoux 1 }
310     }
311 vletoux 10 if (fAddToOuput && (Files[dwI].dwFlag & OPENPGP_FILE_CONF_IS_AUTH))
312     {
313     dwTempReturn = OCardIsConfidentialityKeyTheSameThanAuthentication(pCardData);
314     if (dwReturn)
315     {
316     fAddToOuput = FALSE;
317     }
318     }
319 vletoux 9 if (fAddToOuput)
320 vletoux 1 {
321     dwSize = (DWORD) strlen( Files[dwI].szFile) + 1;
322     memcpy(*pbResponse + *pdwResponseSize, Files[dwI].szFile, dwSize);
323     *pdwResponseSize += dwSize;
324     }
325     }
326     }
327     if (!fDirectoryFound)
328     {
329     dwReturn = SCARD_E_DIR_NOT_FOUND;
330     __leave;
331     }
332     (*pbResponse)[*pdwResponseSize] = '\0';
333     *pdwResponseSize += 1;
334     dwReturn = 0;
335     }
336     __finally
337     {
338     }
339     Trace(WINEVENT_LEVEL_VERBOSE, L"dwReturn = 0x%08X",dwReturn);
340     return dwReturn;
341     }
342    
343     // read file
344 vletoux 8 DWORD OCardGetFileInfo(__in PCARD_DATA pCardData,
345 vletoux 1 __in_opt PSTR szDirectory, __in PSTR szFile,
346     __inout PCARD_FILE_INFO pCardFileInfo)
347     {
348     DWORD dwReturn = 0;
349     PBYTE pbData = NULL;
350     DWORD dwSize, dwI;
351     __try
352     {
353 vletoux 8 dwReturn = OCardReadFile(pCardData, szDirectory, szFile, &pbData, &dwSize);
354 vletoux 1 if (dwReturn)
355     {
356     __leave;
357     }
358     pCardData->pfnCspFree(pbData);
359     pCardFileInfo->cbFileSize = dwSize;
360     pCardFileInfo->AccessCondition = InvalidAc;
361     for(dwI = 0; dwI < dwFileCount; dwI++)
362     {
363 vletoux 8 BOOL fMatch = FALSE;
364     if (szDirectory == NULL)
365 vletoux 1 {
366 vletoux 8 if (!Files[dwI].szDirectory) fMatch = TRUE;
367     }
368     else
369     {
370     if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
371     }
372     if (fMatch)
373     {
374     if (_stricmp(szFile, Files[dwI].szFile) == 0)
375 vletoux 1 {
376     pCardFileInfo->AccessCondition = Files[dwI].dwAccess;
377     break;
378     }
379     }
380     }
381     dwReturn = 0;
382     }
383     __finally
384     {
385     }
386     Trace(WINEVENT_LEVEL_VERBOSE, L"dwReturn = 0x%08X",dwReturn);
387     return dwReturn;
388     }
389    
390 vletoux 8 DWORD OCardWriteFile(__in PCARD_DATA pCardData,
391 vletoux 1 __in_opt PSTR szDirectory, __in PSTR szFile,
392     __in PBYTE pbData, __in DWORD dwSize)
393     {
394     DWORD dwI;
395     DWORD dwReturn = 0;
396     BOOL fDirectoryFound = FALSE;
397     BOOL fFileFound = FALSE;
398     BYTE pbCmd[5 + 256] = {0x00, 0xDA, 0x00, 0x00, 0x00};
399     DWORD dwCmdSize = 0;
400     __try
401     {
402     if (dwSize > 255)
403     {
404     dwReturn = SCARD_E_INVALID_PARAMETER;
405     Trace(WINEVENT_LEVEL_ERROR, L"dwSize %d",dwSize);
406     __leave;
407     }
408    
409    
410     for(dwI = 0; dwI < dwFileCount; dwI++)
411     {
412     BOOL fMatch = FALSE;
413     if (szDirectory == NULL)
414     {
415     if (!Files[dwI].szDirectory) fMatch = TRUE;
416     }
417     else
418     {
419 vletoux 8 if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
420 vletoux 1 }
421     if (fMatch)
422     {
423     fDirectoryFound = TRUE;
424 vletoux 8 if (_stricmp(szFile, Files[dwI].szFile) == 0)
425 vletoux 1 {
426     fFileFound = TRUE;
427     break;
428     }
429     }
430     }
431     if (!fFileFound)
432     {
433     if (fDirectoryFound)
434     {
435     dwReturn = SCARD_E_FILE_NOT_FOUND;
436     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_FILE_NOT_FOUND %S",szFile);
437     }
438     else
439     {
440     dwReturn = SCARD_E_DIR_NOT_FOUND;
441     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_DIR_NOT_FOUND %S",szDirectory);
442     }
443     __leave;
444     }
445     if (Files[dwI].dwFileType == StoredOnSmartCard)
446     {
447 vletoux 6 if (Files[dwI].dwTlv > 0)
448 vletoux 1 {
449 vletoux 6 pbCmd[2] = (BYTE) (Files[dwI].dwTlv / 0x100);
450     pbCmd[3] = (BYTE) (Files[dwI].dwTlv % 0x100);
451 vletoux 1 }
452 vletoux 6 else
453     {
454     pbCmd[2] = (BYTE) (Files[dwI].dwTag / 0x100);
455     pbCmd[3] = (BYTE) (Files[dwI].dwTag % 0x100);
456     }
457 vletoux 1 pbCmd[4] = (BYTE) dwSize;
458     if (dwSize)
459     {
460     memcpy(pbCmd + 5, pbData, dwSize);
461     }
462     dwCmdSize = dwSize + 5;
463 vletoux 8 dwReturn = OCardSendCommand(pCardData, pbCmd, dwCmdSize);
464 vletoux 1 if (dwReturn)
465     {
466     __leave;
467     }
468     }
469     else
470     {
471     dwReturn = SCARD_W_SECURITY_VIOLATION;
472     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_W_SECURITY_VIOLATION %S",szFile);
473     __leave;
474     }
475    
476    
477     }
478     __finally
479     {
480     }
481     Trace(WINEVENT_LEVEL_VERBOSE, L"dwReturn = 0x%08X",dwReturn);
482     return dwReturn;
483     }
484    
485 vletoux 8 DWORD OCardDeleteFile(__in PCARD_DATA pCardData,
486 vletoux 1 __in_opt PSTR szDirectory, __in PSTR szFile)
487     {
488 vletoux 8 return OCardWriteFile(pCardData, szDirectory, szFile, NULL, 0);
489 vletoux 1 }
490    
491     // just change the flag in Files
492 vletoux 8 DWORD OCardCreateFile(__in PCARD_DATA pCardData,
493 vletoux 1 __in_opt PSTR szDirectory, __in PSTR szFile)
494     {
495     DWORD dwI;
496     DWORD dwReturn = 0;
497     BOOL fDirectoryFound = FALSE;
498     BOOL fFileFound = FALSE;
499     __try
500     {
501     for(dwI = 0; dwI < dwFileCount; dwI++)
502     {
503     BOOL fMatch = FALSE;
504     if (szDirectory == NULL)
505     {
506     if (!Files[dwI].szDirectory) fMatch = TRUE;
507     }
508     else
509     {
510 vletoux 8 if (Files[dwI].szDirectory && _stricmp(szDirectory, Files[dwI].szDirectory) == 0) fMatch = TRUE;
511 vletoux 1 }
512     if (fMatch)
513     {
514     fDirectoryFound = TRUE;
515 vletoux 8 if (_stricmp(szFile, Files[dwI].szFile) == 0)
516 vletoux 1 {
517     fFileFound = TRUE;
518     break;
519     }
520     }
521     }
522     if (!fDirectoryFound)
523     {
524     dwReturn = SCARD_E_DIR_NOT_FOUND;
525     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_E_DIR_NOT_FOUND %S",szFile);
526     __leave;
527     }
528     if (!fFileFound)
529     {
530     dwReturn = SCARD_W_SECURITY_VIOLATION;
531     Trace(WINEVENT_LEVEL_ERROR, L"SCARD_W_SECURITY_VIOLATION %S",szFile);
532     __leave;
533     }
534    
535     }
536     __finally
537     {
538     }
539     Trace(WINEVENT_LEVEL_VERBOSE, L"dwReturn = 0x%08X",dwReturn);
540     return dwReturn;
541     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26