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) delete junk; 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