1 /******************************************************************************
2  *
3  * copyright:   Copyright © 2007 Daniel Keep.  All rights reserved.
4  * license:     BSD style: $(LICENSE)
5  * version:     Dec 2007: Initial release$(BR)
6  *              May 2009: Inherit File
7  * authors:     Daniel Keep
8  * credits:     Thanks to John Reimer for helping test this module under
9  *              Linux.
10  *
11  ******************************************************************************/
12 
13 module tango.io.device.TempFile;
14 
15 import Path = tango.io.Path;
16 import tango.math.random.Kiss : Kiss;
17 import tango.io.device.Device : Device;
18 import tango.io.device.File;
19 import tango.stdc.stringz : toStringz;
20 import tango.core.Octal;
21 /******************************************************************************
22  ******************************************************************************/
23 
24 version( Win32 )
25 {
26     import tango.sys.Common : DWORD, LONG, MAX_PATH, PCHAR, CP_UTF8;
27 
28     enum : DWORD { FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 }
29 
30     version( Win32SansUnicode )
31     {
32         import tango.sys.Common :
33             GetVersionExA, OSVERSIONINFO,
34             FILE_FLAG_DELETE_ON_CLOSE,
35             GetTempPathA;
36 
37         char[] GetTempPath()
38         {
39             auto len = GetTempPathA(0, null);
40             if( len == 0 )
41                 throw new Exception("could not obtain temporary path");
42 
43             auto result = new char[len+1];
44             len = GetTempPathA(len+1, result.ptr);
45             if( len == 0 )
46                 throw new Exception("could not obtain temporary path");
47             return Path.standard(result[0..len]);
48         }
49     }
50     else
51     {
52         import tango.sys.Common :
53             WideCharToMultiByte,
54             GetVersionExW, OSVERSIONINFO,
55             FILE_FLAG_DELETE_ON_CLOSE,
56             GetTempPathW;
57 
58         char[] GetTempPath()
59         {
60             auto len = GetTempPathW(0, null);
61             if( len == 0 )
62                 throw new Exception("could not obtain temporary path");
63 
64             auto result = new wchar[len+1];
65             len = GetTempPathW(len+1, result.ptr);
66             if( len == 0 )
67                 throw new Exception("could not obtain temporary path");
68 
69             auto dir = new char [len * 3];
70             auto i = WideCharToMultiByte (CP_UTF8, 0, result.ptr, len,
71                                           cast(PCHAR) dir.ptr, dir.length, null, null);
72             return Path.standard (dir[0..i]);
73         }
74     }
75 
76     // Determines if reparse points (aka: symlinks) are supported.  Support
77     // was introduced in Windows Vista.
78     @property bool reparseSupported()
79     {
80         OSVERSIONINFO versionInfo = void;
81         versionInfo.dwOSVersionInfoSize = versionInfo.sizeof;
82 
83         void e(){throw new Exception("could not determine Windows version");}
84 
85         version( Win32SansUnicode )
86         {
87             if( !GetVersionExA(&versionInfo) ) e();
88         }
89         else
90         {
91             if( !GetVersionExW(&versionInfo) ) e();
92         }
93 
94         return (versionInfo.dwMajorVersion >= 6);
95     }
96 }
97 
98 else version( Posix )
99 {
100     import tango.stdc.posix.pwd : getpwnam;
101     import tango.stdc.posix.unistd : access, getuid, lseek, unlink, W_OK;
102     import tango.stdc.posix.sys.types : off_t;
103     import tango.stdc.posix.sys.stat : stat, stat_t;
104     import tango.sys.consts.fcntl : O_NOFOLLOW;
105     import tango.stdc.posix.stdlib : getenv;
106     import tango.stdc.string : strlen;
107 }
108 
109 /******************************************************************************
110  *
111  * The TempFile class aims to provide a safe way of creating and destroying
112  * temporary files.  The TempFile class will automatically close temporary
113  * files when the object is destroyed, so it is recommended that you make
114  * appropriate use of scoped destruction.
115  *
116  * Temporary files can be created with one of several styles, much like normal
117  * Files.  TempFile styles have the following properties:
118  *
119  * $(UL
120  * $(LI $(B Transience): this determines whether the file should be destroyed
121  * as soon as it is closed (transient,) or continue to persist even after the
122  * application has terminated (permanent.))
123  * )
124  *
125  * Eventually, this will be expanded to give you greater control over the
126  * temporary file's properties.
127  *
128  * For the typical use-case (creating a file to temporarily store data too
129  * large to fit into memory,) the following is sufficient:
130  *
131  * -----
132  *  {
133  *      scope temp = new TempFile;
134  *
135  *      // Use temp as a normal conduit; it will be automatically closed when
136  *      // it goes out of scope.
137  *  }
138  * -----
139  *
140  * Important:
141  * It is recommended that you $(I do not) use files created by this class to
142  * store sensitive information.  There are several known issues with the
143  * current implementation that could allow an attacker to access the contents
144  * of these temporary files.
145  *
146  * Todo: Detail security properties and guarantees.
147  *
148  ******************************************************************************/
149 
150 class TempFile : File
151 {
152     /+enum Visibility : ubyte
153     {
154         /**
155          * The temporary file will have read and write access to it restricted
156          * to the current user.
157          */
158         User,
159         /**
160          * The temporary file will have read and write access available to any
161          * user on the system.
162          */
163         World
164     }+/
165 
166     /**************************************************************************
167      *
168      * This enumeration is used to control whether the temporary file should
169      * persist after the TempFile object has been destroyed.
170      *
171      **************************************************************************/
172 
173     enum Transience : ubyte
174     {
175         /**
176          * The temporary file should be destroyed along with the owner object.
177          */
178         Transient,
179         /**
180          * The temporary file should persist after the object has been
181          * destroyed.
182          */
183         Permanent
184     }
185 
186     /+enum Sensitivity : ubyte
187     {
188         /**
189          * Transient files will be truncated to zero length immediately
190          * before closure to prevent casual filesystem inspection to recover
191          * their contents.
192          *
193          * No additional action is taken on permanent files.
194          */
195         None,
196         /**
197          * Transient files will be zeroed-out before truncation, to mask their
198          * contents from more thorough filesystem inspection.
199          *
200          * This option is not compatible with permanent files.
201          */
202         Low
203         /+
204         /**
205          * Transient files will be overwritten first with zeroes, then with
206          * ones, and then with a random 32- or 64-bit pattern (dependant on
207          * which is most efficient.)  The file will then be truncated.
208          *
209          * This option is not compatible with permanent files.
210          */
211         Medium
212         +/
213     }+/
214 
215     /**************************************************************************
216      *
217      * This structure is used to determine how the temporary files should be
218      * opened and used.
219      *
220      **************************************************************************/
221     align(1) struct TempStyle
222     {
223         //Visibility visibility;      ///
224         Transience transience;        ///
225         //Sensitivity sensitivity;    ///
226         //Share share;                ///
227         //Cache cache;                ///
228         ubyte attempts = 10;          ///
229     }
230 
231     /**
232      * TempStyle for creating a transient temporary file that only the current
233      * user can access.
234      */
235     static __gshared const TempStyle Transient = {Transience.Transient};
236     /**
237      * TempStyle for creating a permanent temporary file that only the current
238      * user can access.
239      */
240     static __gshared const TempStyle Permanent = {Transience.Permanent};
241 
242     // Path to the temporary file
243     private char[] _path;
244 
245     // TempStyle we've opened with
246     private TempStyle _style;
247 
248     ///
249     this(TempStyle style = TempStyle.init)
250     {
251         open (style);
252     }
253 
254     ///
255     this(const(char)[] prefix, TempStyle style = TempStyle.init)
256     {
257         open (prefix, style);
258     }
259 
260     /**************************************************************************
261      *
262      * Indicates the style that this TempFile was created with.
263      *
264      **************************************************************************/
265     TempStyle tempStyle()
266     {
267         return _style;
268     }
269 
270     /*
271      * Creates a new temporary file with the given style.
272      */
273     private void open (TempStyle style)
274     {
275         open (tempPath(), style);
276     }
277 
278     private void open (const(char)[] prefix, TempStyle style)
279     {
280         for( ubyte i=style.attempts; i--; )
281         {
282             if( openTempFile(Path.join(prefix, randomName()), style) )
283                 return;
284         }
285 
286         error("could not create temporary file");
287     }
288 
289     version( Win32 )
290     {
291         private static enum DEFAULT_LENGTH = 6;
292         private static enum DEFAULT_PREFIX = "~t";
293         private static enum DEFAULT_SUFFIX = ".tmp";
294 
295         private static enum JUNK_CHARS =
296             "abcdefghijklmnopqrstuvwxyz0123456789";
297 
298        /**********************************************************************
299          *
300          * Returns the path to the directory where temporary files will be
301          * created.  The returned path is safe to mutate.
302          *
303          **********************************************************************/
304         public static char[] tempPath()
305         {
306             return GetTempPath();
307         }
308 
309         /*
310          * Creates a new temporary file at the given path, with the specified
311          * style.
312          */
313         private bool openTempFile(const(char)[] path, TempStyle style)
314         {
315             // TODO: Check permissions directly and throw an exception;
316             // otherwise, we could spin trying to make a file when it's
317             // actually not possible.
318 
319             Style filestyle = {Access.ReadWrite, Open.New,
320                                Share.None, Cache.None};
321 
322             DWORD attr;
323 
324             // Set up flags
325             attr = reparseSupported ? FILE_FLAG_OPEN_REPARSE_POINT : 0;
326             if( style.transience == Transience.Transient )
327                 attr |= FILE_FLAG_DELETE_ON_CLOSE;
328 
329             if (!super.open (path, filestyle, attr))
330                 return false;
331 
332             _style = style;
333             return true;
334         }
335     }
336     else version( Posix )
337     {
338         private static enum DEFAULT_LENGTH = 6;
339         private static enum DEFAULT_PREFIX = ".tmp";
340 
341         // Use "~" to work around a bug in DMD where it elides empty constants
342         private static enum DEFAULT_SUFFIX = "~";
343 
344         private static enum JUNK_CHARS =
345             "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ~
346             "abcdefghijklmnopqrstuvwxyz0123456789";
347 
348        /**********************************************************************
349          *
350          * Returns the path to the directory where temporary files will be
351          * created.  The returned path is safe to mutate.
352          *
353          **********************************************************************/
354         public static const(char)[] tempPath()
355         {
356             // Check for TMPDIR; failing that, use /tmp
357             char* ptr = getenv ("TMPDIR");
358             if (ptr is null)
359                 return "/tmp/";
360             else
361                 return ptr[0 .. strlen (ptr)].dup;
362         }
363 
364         /*
365          * Creates a new temporary file at the given path, with the specified
366          * style.
367          */
368         private bool openTempFile(const(char)[] path, TempStyle style)
369         {
370             // Check suitability
371             {
372                 auto parentz = toStringz(Path.parse(path).path);
373 
374                 // Make sure we have write access
375                 if( access(parentz, W_OK) == -1 )
376                     error("do not have write access to temporary directory");
377 
378                 // Get info on directory
379                 stat_t sb;
380                 if( stat(parentz, &sb) == -1 )
381                     error("could not stat temporary directory");
382 
383                 // Get root's UID
384                 auto pwe = getpwnam("root");
385                 if( pwe is null ) error("could not get root's uid");
386                 auto root_uid = pwe.pw_uid;
387 
388                 // Make sure either we or root are the owner
389                 if( !(sb.st_uid == root_uid || sb.st_uid == getuid()) )
390                     error("temporary directory owned by neither root nor user");
391 
392                 // Check to see if anyone other than us can write to the dir.
393                 if( (sb.st_mode & octal!22) != 0 && (sb.st_mode & octal!1000) == 0 )
394                     error("sticky bit not set on world-writable directory");
395             }
396 
397             // Create file
398             {
399                 Style filestyle = {Access.ReadWrite, Open.New,
400                                    Share.None, Cache.None};
401 
402                 auto addflags = O_NOFOLLOW;
403 
404                 if (!super.open(path, filestyle, addflags, octal!600))
405                     return false;
406 
407                 if( style.transience == Transience.Transient )
408                 {
409                     // BUG TODO: check to make sure the path still points
410                     // to the file we opened.  Pity you can't unlink a file
411                     // descriptor...
412 
413                     // NOTE: This should be an exception and not simply
414                     // returning false, since this is a violation of our
415                     // guarantees.
416                     if( unlink(toStringz(path)) == -1 )
417                         error("could not remove transient file");
418                 }
419 
420                 _style = style;
421 
422                 return true;
423             }
424         }
425     }
426     else
427     {
428         static assert(false, "Unsupported platform");
429     }
430 
431     /*
432      * Generates a new random file name, sans directory.
433      */
434     private char[] randomName(size_t length=DEFAULT_LENGTH,
435             const(char)[] prefix=DEFAULT_PREFIX,
436             const(char)[] suffix=DEFAULT_SUFFIX)
437     {
438         auto junk = new char[length];
439         scope(exit) junk.destroy;
440 
441         foreach( ref c ; junk )
442             c = JUNK_CHARS[Kiss.instance.toInt(cast(uint)$)];
443 
444         return prefix~junk~suffix;
445     }
446 
447     override void detach()
448     {
449         static assert( !is(Sensitivity) );
450         super.detach();
451     }
452 }
453 
454 version( TempFile_SelfTest ):
455 
456 import tango.io.Console : Cin;
457 import tango.io.Stdout : Stdout;
458 
459 void main()
460 {
461     Stdout(r"
462 Please ensure that the transient file no longer exists once the TempFile
463 object is destroyed, and that the permanent file does.  You should also check
464 the following on both:
465 
466  * the file should be owned by you,
467  * the owner should have read and write permissions,
468  * no other permissions should be set on the file.
469 
470 For POSIX systems:
471 
472  * the temp directory should be owned by either root or you,
473  * if anyone other than root or you can write to it, the sticky bit should be
474    set,
475  * if the directory is writable by anyone other than root or the user, and the
476    sticky bit is *not* set, then creating the temporary file should fail.
477 
478 You might want to delete the permanent one afterwards, too. :)")
479     .newline;
480 
481     Stdout.formatln("Creating a transient file:");
482     {
483         scope tempFile = new TempFile(/*TempFile.UserPermanent*/);
484 
485         Stdout.formatln(" .. path: {}", tempFile);
486 
487         tempFile.write("Transient temp file.");
488 
489         char[] buffer = new char[1023];
490         tempFile.seek(0);
491         buffer = buffer[0..tempFile.read(buffer)];
492 
493         Stdout.formatln(" .. contents: \"{}\"", buffer);
494 
495         Stdout(" .. press Enter to destroy TempFile object.").newline;
496         Cin.copyln();
497     }
498 
499     Stdout.newline;
500 
501     Stdout.formatln("Creating a permanent file:");
502     {
503         scope tempFile = new TempFile(TempFile.Permanent);
504 
505         Stdout.formatln(" .. path: {}", tempFile);
506 
507         tempFile.write("Permanent temp file.");
508 
509         char[] buffer = new char[1023];
510         tempFile.seek(0);
511         buffer = buffer[0..tempFile.read(buffer)];
512 
513         Stdout.formatln(" .. contents: \"{}\"", buffer);
514 
515         Stdout(" .. press Enter to destroy TempFile object.").flush;
516         Cin.copyln();
517     }
518 
519     Stdout("\nDone.").newline;
520 }
521 
522