1 /******************************************************************************
2  *
3  * Copyright:
4  *     Copyright © 2007 Daniel Keep.
5  *     Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
6  *     All rights reserved.
7  *
8  * License:
9  *     Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
10  *     See LICENSE_TANGO.txt for details.
11  *
12  * Version:
13  *     Dec 2007: Initial release$(BR)
14  *     May 2009: Inherit File
15  *
16  * Authors: Daniel Keep
17  *
18  * Credits:
19  *     Thanks to John Reimer for helping test this module under Linux.
20  *
21  *
22  ******************************************************************************/
23 
24 module ocean.io.device.TempFile;
25 
26 import ocean.transition;
27 import ocean.core.Verify;
28 
29 import Path = ocean.io.Path;
30 import ocean.math.random.Kiss : Kiss;
31 import ocean.io.device.Device : Device;
32 import ocean.io.device.File;
33 import ocean.text.util.StringC;
34 
35 import core.stdc.string : strlen;
36 import core.sys.posix.pwd : getpwnam;
37 import core.sys.posix.unistd : access, getuid, lseek, unlink, W_OK;
38 import core.sys.posix.sys.types : off_t;
39 import core.sys.posix.sys.stat : stat, stat_t;
40 import ocean.stdc.posix.fcntl : O_NOFOLLOW;
41 import core.sys.posix.stdlib : getenv;
42 
43 /******************************************************************************
44  *
45  * The TempFile class aims to provide a safe way of creating and destroying
46  * temporary files.  The TempFile class will automatically close temporary
47  * files when the object is destroyed, so it is recommended that you make
48  * appropriate use of scoped destruction.
49  *
50  * Temporary files can be created with one of several styles, much like normal
51  * Files.  TempFile styles have the following properties:
52  *
53  * $(UL
54  * $(LI $(B Transience): this determines whether the file should be destroyed
55  * as soon as it is closed (transient,) or continue to persist even after the
56  * application has terminated (permanent.))
57  * )
58  *
59  * Eventually, this will be expanded to give you greater control over the
60  * temporary file's properties.
61  *
62  * For the typical use-case (creating a file to temporarily store data too
63  * large to fit into memory,) the following is sufficient:
64  *
65  * -----
66  *  {
67  *      scope temp = new TempFile;
68  *
69  *      // Use temp as a normal conduit; it will be automatically closed when
70  *      // it goes out of scope.
71  *  }
72  * -----
73  *
74  * Important:
75  * It is recommended that you $(I do not) use files created by this class to
76  * store sensitive information.  There are several known issues with the
77  * current implementation that could allow an attacker to access the contents
78  * of these temporary files.
79  *
80  * Todo: Detail security properties and guarantees.
81  *
82  ******************************************************************************/
83 
84 class TempFile : File
85 {
86     /**************************************************************************
87      *
88      * This enumeration is used to control whether the temporary file should
89      * persist after the TempFile object has been destroyed.
90      *
91      **************************************************************************/
92 
93     enum Transience : ubyte
94     {
95         /**
96          * The temporary file should be destroyed along with the owner object.
97          */
98         Transient,
99         /**
100          * The temporary file should persist after the object has been
101          * destroyed.
102          */
103         Permanent
104     }
105 
106     /**************************************************************************
107      *
108      * This structure is used to determine how the temporary files should be
109      * opened and used.
110      *
111      **************************************************************************/
112     align(1) struct TempStyle
113     {
114         align(1):
115         //Visibility visibility;      ///
116         Transience transience;        ///
117         //Sensitivity sensitivity;    ///
118         //Share share;                ///
119         //Cache cache;                ///
120         ubyte attempts = 10;          ///
121     }
122 
123     /**
124      * TempStyle for creating a transient temporary file that only the current
125      * user can access.
126      */
127     static immutable TempStyle Transient = {Transience.Transient};
128     /**
129      * TempStyle for creating a permanent temporary file that only the current
130      * user can access.
131      */
132     static immutable TempStyle Permanent = {Transience.Permanent};
133 
134     // Path to the temporary file
135     private istring _path;
136 
137     // TempStyle we've opened with
138     private TempStyle _style;
139 
140     ///
141     this(TempStyle style = TempStyle.init)
142     {
143         open (style);
144     }
145 
146     ///
147     this(istring prefix, TempStyle style = TempStyle.init)
148     {
149         open (prefix, style);
150     }
151 
152     /**************************************************************************
153      *
154      * Indicates the style that this TempFile was created with.
155      *
156      **************************************************************************/
157     TempStyle tempStyle()
158     {
159         return _style;
160     }
161 
162     /*
163      * Creates a new temporary file with the given style.
164      */
165     private void open (TempStyle style)
166     {
167         open (tempPath, style);
168     }
169 
170     private void open (istring prefix, TempStyle style)
171     {
172         for( ubyte i=style.attempts; i--; )
173         {
174             if( openTempFile(Path.join(prefix, randomName), style) )
175                 return;
176         }
177 
178         error("could not create temporary file");
179     }
180 
181     private static immutable DEFAULT_LENGTH = 6;
182     private static immutable DEFAULT_PREFIX = ".tmp";
183 
184     // Use "~" to work around a bug in DMD where it elides empty constants
185     private static immutable DEFAULT_SUFFIX = "~";
186 
187     private static immutable JUNK_CHARS =
188         "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
189 
190     /***************************************************************************
191      *
192      * Returns the path to the directory where temporary files will be
193      * created.  The returned path is safe to mutate.
194      *
195      **************************************************************************/
196 
197     public static istring tempPath()
198     {
199         // Check for TMPDIR; failing that, use /tmp
200         char* ptr = getenv ("TMPDIR".ptr);
201         if (ptr is null)
202             return "/tmp/";
203         else
204             return idup(ptr[0 .. strlen (ptr)]);
205     }
206 
207     /*
208      * Creates a new temporary file at the given path, with the specified
209      * style.
210      */
211     private bool openTempFile(cstring path, TempStyle style)
212     {
213         // Check suitability
214         {
215             mstring path_mut = Path.parse(path.dup).path;
216             assumeSafeAppend(path_mut);
217             auto parentz = StringC.toCString(path_mut);
218 
219             // Make sure we have write access
220             if( access(parentz, W_OK) == -1 )
221                 error("do not have write access to temporary directory");
222 
223             // Get info on directory
224             stat_t sb;
225             if( stat(parentz, &sb) == -1 )
226                 error("could not stat temporary directory");
227 
228             // Get root's UID
229             auto pwe = getpwnam("root".ptr);
230             if( pwe is null ) error("could not get root's uid");
231             auto root_uid = pwe.pw_uid;
232 
233             // Make sure either we or root are the owner
234             if( !(sb.st_uid == root_uid || sb.st_uid == getuid) )
235                 error("temporary directory owned by neither root nor user");
236 
237             // Check to see if anyone other than us can write to the dir.
238             if( (sb.st_mode & Octal!("22")) != 0 && (sb.st_mode & Octal!("1000")) == 0 )
239                 error("sticky bit not set on world-writable directory");
240         }
241 
242         // Create file
243         {
244             Style filestyle = {Access.ReadWrite, Open.New,
245                                Share.None, Cache.None};
246 
247             auto addflags = O_NOFOLLOW;
248 
249             if (!super.open(path, filestyle, addflags, Octal!("600")))
250                 return false;
251 
252             if( style.transience == Transience.Transient )
253             {
254                 // BUG TODO: check to make sure the path still points
255                 // to the file we opened.  Pity you can't unlink a file
256                 // descriptor...
257 
258                 // NOTE: This should be an exception and not simply
259                 // returning false, since this is a violation of our
260                 // guarantees.
261                 if( unlink((path ~ "\0").ptr) == -1 )
262                     error("could not remove transient file");
263             }
264 
265             _style = style;
266 
267             return true;
268         }
269     }
270 
271     /*
272      * Generates a new random file name, sans directory.
273      */
274     private istring randomName(uint length=DEFAULT_LENGTH,
275             istring prefix=DEFAULT_PREFIX,
276             istring suffix=DEFAULT_SUFFIX)
277     {
278         import core.memory;
279         auto junk = new char[length];
280         scope(exit) GC.free(junk.ptr);
281 
282         foreach( ref c ; junk )
283         {
284             verify(JUNK_CHARS.length < uint.max);
285             c = JUNK_CHARS[Kiss.instance.toInt(cast(uint) $)];
286         }
287 
288         return prefix ~ assumeUnique(junk) ~ suffix;
289     }
290 
291     override void detach()
292     {
293         static assert( !is(Sensitivity) );
294         super.detach();
295     }
296 }