1 # Copyright (c) 2017 Ansible Project
2 # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
3 
4 #Requires -Module Ansible.ModuleUtils.PrivilegeUtil
5 
Load-LinkUtils()6 Function Load-LinkUtils() {
7     $link_util = @'
8 using Microsoft.Win32.SafeHandles;
9 using System;
10 using System.Collections.Generic;
11 using System.IO;
12 using System.Runtime.InteropServices;
13 using System.Text;
14 
15 namespace Ansible
16 {
17     public enum LinkType
18     {
19         SymbolicLink,
20         JunctionPoint,
21         HardLink
22     }
23 
24     public class LinkUtilWin32Exception : System.ComponentModel.Win32Exception
25     {
26         private string _msg;
27 
28         public LinkUtilWin32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
29 
30         public LinkUtilWin32Exception(int errorCode, string message) : base(errorCode)
31         {
32             _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
33         }
34 
35         public override string Message { get { return _msg; } }
36         public static explicit operator LinkUtilWin32Exception(string message) { return new LinkUtilWin32Exception(message); }
37     }
38 
39     public class LinkInfo
40     {
41         public LinkType Type { get; internal set; }
42         public string PrintName { get; internal set; }
43         public string SubstituteName { get; internal set; }
44         public string AbsolutePath { get; internal set; }
45         public string TargetPath { get; internal set; }
46         public string[] HardTargets { get; internal set; }
47     }
48 
49     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
50     public struct REPARSE_DATA_BUFFER
51     {
52         public UInt32 ReparseTag;
53         public UInt16 ReparseDataLength;
54         public UInt16 Reserved;
55         public UInt16 SubstituteNameOffset;
56         public UInt16 SubstituteNameLength;
57         public UInt16 PrintNameOffset;
58         public UInt16 PrintNameLength;
59 
60         [MarshalAs(UnmanagedType.ByValArray, SizeConst = LinkUtil.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)]
61         public char[] PathBuffer;
62     }
63 
64     public class LinkUtil
65     {
66         public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 1024 * 16;
67 
68         private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
69         private const UInt32 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
70 
71         private const UInt32 FSCTL_GET_REPARSE_POINT = 0x000900A8;
72         private const UInt32 FSCTL_SET_REPARSE_POINT = 0x000900A4;
73         private const UInt32 FILE_DEVICE_FILE_SYSTEM = 0x00090000;
74 
75         private const UInt32 IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
76         private const UInt32 IO_REPARSE_TAG_SYMLINK = 0xA000000C;
77 
78         private const UInt32 SYMLINK_FLAG_RELATIVE = 0x00000001;
79 
80         private const Int64 INVALID_HANDLE_VALUE = -1;
81 
82         private const UInt32 SIZE_OF_WCHAR = 2;
83 
84         private const UInt32 SYMBOLIC_LINK_FLAG_FILE = 0x00000000;
85         private const UInt32 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x00000001;
86 
87         [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
88         private static extern SafeFileHandle CreateFile(
89             string lpFileName,
90             [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
91             [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
92             IntPtr lpSecurityAttributes,
93             [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
94             UInt32 dwFlagsAndAttributes,
95             IntPtr hTemplateFile);
96 
97         // Used by GetReparsePointInfo()
98         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
99         private static extern bool DeviceIoControl(
100             SafeFileHandle hDevice,
101             UInt32 dwIoControlCode,
102             IntPtr lpInBuffer,
103             UInt32 nInBufferSize,
104             out REPARSE_DATA_BUFFER lpOutBuffer,
105             UInt32 nOutBufferSize,
106             out UInt32 lpBytesReturned,
107             IntPtr lpOverlapped);
108 
109         // Used by CreateJunctionPoint()
110         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
111         private static extern bool DeviceIoControl(
112             SafeFileHandle hDevice,
113             UInt32 dwIoControlCode,
114             REPARSE_DATA_BUFFER lpInBuffer,
115             UInt32 nInBufferSize,
116             IntPtr lpOutBuffer,
117             UInt32 nOutBufferSize,
118             out UInt32 lpBytesReturned,
119             IntPtr lpOverlapped);
120 
121         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
122         private static extern bool GetVolumePathName(
123             string lpszFileName,
124             StringBuilder lpszVolumePathName,
125             ref UInt32 cchBufferLength);
126 
127         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
128         private static extern IntPtr FindFirstFileNameW(
129             string lpFileName,
130             UInt32 dwFlags,
131             ref UInt32 StringLength,
132             StringBuilder LinkName);
133 
134         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
135         private static extern bool FindNextFileNameW(
136             IntPtr hFindStream,
137             ref UInt32 StringLength,
138             StringBuilder LinkName);
139 
140         [DllImport("kernel32.dll", SetLastError = true)]
141         private static extern bool FindClose(
142             IntPtr hFindFile);
143 
144         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
145         private static extern bool RemoveDirectory(
146             string lpPathName);
147 
148         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
149         private static extern bool DeleteFile(
150             string lpFileName);
151 
152         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
153         private static extern bool CreateSymbolicLink(
154             string lpSymlinkFileName,
155             string lpTargetFileName,
156             UInt32 dwFlags);
157 
158         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
159         private static extern bool CreateHardLink(
160             string lpFileName,
161             string lpExistingFileName,
162             IntPtr lpSecurityAttributes);
163 
164         public static LinkInfo GetLinkInfo(string linkPath)
165         {
166             FileAttributes attr = File.GetAttributes(linkPath);
167             if (attr.HasFlag(FileAttributes.ReparsePoint))
168                 return GetReparsePointInfo(linkPath);
169 
170             if (!attr.HasFlag(FileAttributes.Directory))
171                 return GetHardLinkInfo(linkPath);
172 
173             return null;
174         }
175 
176         public static void DeleteLink(string linkPath)
177         {
178             bool success;
179             FileAttributes attr = File.GetAttributes(linkPath);
180             if (attr.HasFlag(FileAttributes.Directory))
181             {
182                 success = RemoveDirectory(linkPath);
183             }
184             else
185             {
186                 success = DeleteFile(linkPath);
187             }
188 
189             if (!success)
190                 throw new LinkUtilWin32Exception(String.Format("Failed to delete link at {0}", linkPath));
191         }
192 
193         public static void CreateLink(string linkPath, String linkTarget, LinkType linkType)
194         {
195             switch (linkType)
196             {
197                 case LinkType.SymbolicLink:
198                     UInt32 linkFlags;
199                     FileAttributes attr = File.GetAttributes(linkTarget);
200                     if (attr.HasFlag(FileAttributes.Directory))
201                         linkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY;
202                     else
203                         linkFlags = SYMBOLIC_LINK_FLAG_FILE;
204 
205                     if (!CreateSymbolicLink(linkPath, linkTarget, linkFlags))
206                         throw new LinkUtilWin32Exception(String.Format("CreateSymbolicLink({0}, {1}, {2}) failed", linkPath, linkTarget, linkFlags));
207                     break;
208                 case LinkType.JunctionPoint:
209                     CreateJunctionPoint(linkPath, linkTarget);
210                     break;
211                 case LinkType.HardLink:
212                     if (!CreateHardLink(linkPath, linkTarget, IntPtr.Zero))
213                         throw new LinkUtilWin32Exception(String.Format("CreateHardLink({0}, {1}) failed", linkPath, linkTarget));
214                     break;
215             }
216         }
217 
218         private static LinkInfo GetHardLinkInfo(string linkPath)
219         {
220             UInt32 maxPath = 260;
221             List<string> result = new List<string>();
222 
223             StringBuilder sb = new StringBuilder((int)maxPath);
224             UInt32 stringLength = maxPath;
225             if (!GetVolumePathName(linkPath, sb, ref stringLength))
226                 throw new LinkUtilWin32Exception("GetVolumePathName() failed");
227             string volume = sb.ToString();
228 
229             stringLength = maxPath;
230             IntPtr findHandle = FindFirstFileNameW(linkPath, 0, ref stringLength, sb);
231             if (findHandle.ToInt64() != INVALID_HANDLE_VALUE)
232             {
233                 try
234                 {
235                     do
236                     {
237                         string hardLinkPath = sb.ToString();
238                         if (hardLinkPath.StartsWith("\\"))
239                             hardLinkPath = hardLinkPath.Substring(1, hardLinkPath.Length - 1);
240 
241                         result.Add(Path.Combine(volume, hardLinkPath));
242                         stringLength = maxPath;
243 
244                     } while (FindNextFileNameW(findHandle, ref stringLength, sb));
245                 }
246                 finally
247                 {
248                     FindClose(findHandle);
249                 }
250             }
251 
252             if (result.Count > 1)
253                 return new LinkInfo
254                 {
255                     Type = LinkType.HardLink,
256                     HardTargets = result.ToArray()
257                 };
258 
259             return null;
260         }
261 
262         private static LinkInfo GetReparsePointInfo(string linkPath)
263         {
264             SafeFileHandle fileHandle = CreateFile(
265                 linkPath,
266                 FileAccess.Read,
267                 FileShare.None,
268                 IntPtr.Zero,
269                 FileMode.Open,
270                 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
271                 IntPtr.Zero);
272 
273             if (fileHandle.IsInvalid)
274                 throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
275 
276             REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
277             UInt32 bytesReturned;
278             try
279             {
280                 if (!DeviceIoControl(
281                     fileHandle,
282                     FSCTL_GET_REPARSE_POINT,
283                     IntPtr.Zero,
284                     0,
285                     out buffer,
286                     MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
287                     out bytesReturned,
288                     IntPtr.Zero))
289                     throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed for file at {0}", linkPath));
290             }
291             finally
292             {
293                 fileHandle.Dispose();
294             }
295 
296             bool isRelative = false;
297             int pathOffset = 0;
298             LinkType linkType;
299             if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
300             {
301                 UInt32 bufferFlags = Convert.ToUInt32(buffer.PathBuffer[0]) + Convert.ToUInt32(buffer.PathBuffer[1]);
302                 if (bufferFlags == SYMLINK_FLAG_RELATIVE)
303                     isRelative = true;
304                 pathOffset = 2;
305                 linkType = LinkType.SymbolicLink;
306             }
307             else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
308             {
309                 linkType = LinkType.JunctionPoint;
310             }
311             else
312             {
313                 string errorMessage = String.Format("Invalid Reparse Tag: {0}", buffer.ReparseTag.ToString());
314                 throw new Exception(errorMessage);
315             }
316 
317             string printName = new string(buffer.PathBuffer, (int)(buffer.PrintNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.PrintNameLength / SIZE_OF_WCHAR));
318             string substituteName = new string(buffer.PathBuffer, (int)(buffer.SubstituteNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.SubstituteNameLength / SIZE_OF_WCHAR));
319 
320             // TODO: should we check for \?\UNC\server for convert it to the NT style \\server path
321             // Remove the leading Windows object directory \?\ from the path if present
322             string targetPath = substituteName;
323             if (targetPath.StartsWith("\\??\\"))
324                 targetPath = targetPath.Substring(4, targetPath.Length - 4);
325 
326             string absolutePath = targetPath;
327             if (isRelative)
328                 absolutePath = Path.GetFullPath(Path.Combine(new FileInfo(linkPath).Directory.FullName, targetPath));
329 
330             return new LinkInfo
331             {
332                 Type = linkType,
333                 PrintName = printName,
334                 SubstituteName = substituteName,
335                 AbsolutePath = absolutePath,
336                 TargetPath = targetPath
337             };
338         }
339 
340         private static void CreateJunctionPoint(string linkPath, string linkTarget)
341         {
342             // We need to create the link as a dir beforehand
343             Directory.CreateDirectory(linkPath);
344             SafeFileHandle fileHandle = CreateFile(
345                 linkPath,
346                 FileAccess.Write,
347                 FileShare.Read | FileShare.Write | FileShare.None,
348                 IntPtr.Zero,
349                 FileMode.Open,
350                 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
351                 IntPtr.Zero);
352 
353             if (fileHandle.IsInvalid)
354                 throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
355 
356             try
357             {
358                 string substituteName = "\\??\\" + Path.GetFullPath(linkTarget);
359                 string printName = linkTarget;
360 
361                 REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
362                 buffer.SubstituteNameOffset = 0;
363                 buffer.SubstituteNameLength = (UInt16)(substituteName.Length * SIZE_OF_WCHAR);
364                 buffer.PrintNameOffset = (UInt16)(buffer.SubstituteNameLength + 2);
365                 buffer.PrintNameLength = (UInt16)(printName.Length * SIZE_OF_WCHAR);
366 
367                 buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
368                 buffer.ReparseDataLength = (UInt16)(buffer.SubstituteNameLength + buffer.PrintNameLength + 12);
369                 buffer.PathBuffer = new char[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
370 
371                 byte[] unicodeBytes = Encoding.Unicode.GetBytes(substituteName + "\0" + printName);
372                 char[] pathBuffer = Encoding.Unicode.GetChars(unicodeBytes);
373                 Array.Copy(pathBuffer, buffer.PathBuffer, pathBuffer.Length);
374 
375                 UInt32 bytesReturned;
376                 if (!DeviceIoControl(
377                     fileHandle,
378                     FSCTL_SET_REPARSE_POINT,
379                     buffer,
380                     (UInt32)(buffer.ReparseDataLength + 8),
381                     IntPtr.Zero, 0,
382                     out bytesReturned,
383                     IntPtr.Zero))
384                     throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed to create junction point at {0} to {1}", linkPath, linkTarget));
385             }
386             finally
387             {
388                 fileHandle.Dispose();
389             }
390         }
391     }
392 }
393 '@
394 
395     # FUTURE: find a better way to get the _ansible_remote_tmp variable
396     $original_tmp = $env:TMP
397 
398     $remote_tmp = $original_tmp
399     $module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue
400     if ($module_params) {
401         if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) {
402             $remote_tmp = $module_params.Value["_ansible_remote_tmp"]
403             $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
404         }
405     }
406 
407     $env:TMP = $remote_tmp
408     Add-Type -TypeDefinition $link_util
409     $env:TMP = $original_tmp
410 
411     # enable the SeBackupPrivilege if it is disabled
412     $state = Get-AnsiblePrivilege -Name SeBackupPrivilege
413     if ($state -eq $false) {
414         Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
415     }
416 }
417 
Get-Link($link_path)418 Function Get-Link($link_path) {
419     $link_info = [Ansible.LinkUtil]::GetLinkInfo($link_path)
420     return $link_info
421 }
422 
Remove-Link($link_path)423 Function Remove-Link($link_path) {
424     [Ansible.LinkUtil]::DeleteLink($link_path)
425 }
426 
New-Link($link_path, $link_target, $link_type)427 Function New-Link($link_path, $link_target, $link_type) {
428     if (-not (Test-Path -LiteralPath $link_target)) {
429         throw "link_target '$link_target' does not exist, cannot create link"
430     }
431 
432     switch($link_type) {
433         "link" {
434             $type = [Ansible.LinkType]::SymbolicLink
435         }
436         "junction" {
437             if (Test-Path -LiteralPath $link_target -PathType Leaf) {
438                 throw "cannot set the target for a junction point to a file"
439             }
440             $type = [Ansible.LinkType]::JunctionPoint
441         }
442         "hard" {
443             if (Test-Path -LiteralPath $link_target -PathType Container) {
444                 throw "cannot set the target for a hard link to a directory"
445             }
446             $type = [Ansible.LinkType]::HardLink
447         }
448         default { throw "invalid link_type option $($link_type): expecting link, junction, hard" }
449     }
450     [Ansible.LinkUtil]::CreateLink($link_path, $link_target, $type)
451 }
452 
453 # this line must stay at the bottom to ensure all defined module parts are exported
454 Export-ModuleMember -Alias * -Function * -Cmdlet *
455