Skip to content

Commit

Permalink
Handle symlink extraction on Windows
Browse files Browse the repository at this point in the history
License: MIT
Signed-off-by: Dominic Della Valle <ddvpublic@gmail.com>
  • Loading branch information
djdv committed May 17, 2018
1 parent 139d624 commit b932bd3
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 5 deletions.
51 changes: 48 additions & 3 deletions core/commands/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
gopath "path"
"path/filepath"
"strings"

Expand All @@ -15,14 +16,15 @@ import (
path "github.com/ipfs/go-ipfs/path"
uarchive "github.com/ipfs/go-ipfs/unixfs/archive"

tar "gx/ipfs/QmQine7gvHncNevKtG9QXxf3nXcwSj6aDDmMm52mHofEEp/tar-utils"
"gx/ipfs/QmTjNRVt2fvaRFu93keEC7z5M1GS1iH6qZ9227htQioTUY/go-ipfs-cmds"
tar "gx/ipfs/QmTkC7aeyDyjfdMTCVcG9P485TMJd6foLaLbf11DZ5WrnV/tar-utils"
osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
"gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/go-ipfs-cmdkit"
"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
)

var ErrInvalidCompressionLevel = errors.New("compression level must be between 1 and 9")

var haveLinkCreatePriviledge bool
var GetCmd = &cmds.Command{
Helptext: cmdkit.HelpText{
Tagline: "Download IPFS objects.",
Expand Down Expand Up @@ -238,13 +240,56 @@ func (gw *getWriter) writeArchive(r io.Reader, fpath string) error {

func (gw *getWriter) writeExtracted(r io.Reader, fpath string) error {
fmt.Fprintf(gw.Out, "Saving file(s) to %s\n", fpath)
var modified bool
defer func() { //NOTE: defer order important; we want this to display after the progressbar finishes
if modified {
fmt.Fprintf(gw.Out, "WARNING: Extraction output had to be modified in order to be stored successfully\n")
}
}()

bar := makeProgressBar(gw.Err, gw.Size)
bar.Start()
defer bar.Finish()
defer bar.Set64(gw.Size)

extractor := &tar.Extractor{Path: fpath, Progress: bar.Add64}
return extractor.Extract(r)
if osh.IsWindows() {
extractor.Sanitize(true)
builtinSanitizer := extractor.SanitizePathFunc
extractor.SanitizePathFunc = func(path string) (string, error) {
sanitizedPath, err := builtinSanitizer(path)
inBase, outBase := gopath.Base(path), filepath.Base(sanitizedPath)
if inBase != outBase {
modified = true
fmt.Fprintf(gw.Out, "%q extracted as %q\n", inBase, outBase)
}
return sanitizedPath, err
}
extractor.LinkFunc = func(l tar.Link) error {
//remove existing
if _, err := os.Lstat(l.Name); err == nil {
if err = os.Remove(l.Name); err != nil {
return err
}
}

err := os.Symlink(l.Target, l.Name)
if err == nil {
return nil
}
if err != nil && haveLinkCreatePriviledge { // fail only on non-privilege errors
return err
}

// otherwise skip link creation with a warning
modified = true
fmt.Fprintf(gw.Out, "Symlink %q->%q was skipped, user does not have symlink creation privileges (see:https://git.io/vpHKV)\n", l.Name, l.Target)
return nil
}
}

err := extractor.Extract(r)
return err
}

func getCompressOptions(req *cmds.Request) (int, error) {
Expand Down
271 changes: 271 additions & 0 deletions core/commands/get_sanitize_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
//+build windows

package commands

import (
"go/build"
"syscall"
"unsafe"

windows "gx/ipfs/QmPXvegq26x982cQjSfbTvSzZXn7GiaMwhhVPHkeTEhrPT/sys/windows"
registry "gx/ipfs/QmPXvegq26x982cQjSfbTvSzZXn7GiaMwhhVPHkeTEhrPT/sys/windows/registry"
)

const (
SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001
SE_PRIVILEGE_ENABLED = 0x00000002
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
)

//NOTE: arrays larger than 1 are not needed for our purpose
// as a result, conversion of Go slice to WINAPI variable-sized array, does not need to be implemented here
const ANYSIZE_ARRAY = 1

//WINAPI structures
type Luid struct {
LowPart uint32
HighPart int32
}

type LUID_AND_ATTRIBUTES struct {
Luid Luid
Attributes uint32
}

type TOKEN_PRIVILEGES struct {
PrivilegeCount uint32
Privileges [ANYSIZE_ARRAY]LUID_AND_ATTRIBUTES
}

type PRIVILEGE_SET struct {
PrivilegeCount uint32
Control uint32
Privilege [ANYSIZE_ARRAY]LUID_AND_ATTRIBUTES
}

var advapi32 *windows.DLL

func init() {
// Has Developer Mode privileges (Windows 14972+)
if isOSLinkAware() {
// Go does not take advantage of this feature prior to 1.11 (c23afa9)
if !isGoLinkAware() {
return
}
if isDevModeActive() {
haveLinkCreatePriviledge = true
return
}
}

// Has UAC SE_CREATE_SYMBOLIC_LINK_NAME privilege (Vista+)
var err error
advapi32, err = loadSystemDLL("Advapi32.dll")
if err != nil {
return
}
defer advapi32.Release()

if haveUACLinkPrivilege() {
haveLinkCreatePriviledge = true
} else {
haveLinkCreatePriviledge = requestUACLinkPrivilege() //try to gain it
}
}

func loadSystemDLL(name string) (*windows.DLL, error) {
modHandle, err := windows.LoadLibraryEx(name, 0, LOAD_LIBRARY_SEARCH_SYSTEM32)
if err != nil {
return nil, err
}
return &windows.DLL{Name: name, Handle: modHandle}, nil
}

func havePrivilege(ClientToken windows.Handle, RequiredPrivileges *PRIVILEGE_SET) (ret bool) {
privilegeCheck(ClientToken, RequiredPrivileges, &ret)
return ret
}

func haveUACLinkPrivilege() bool {
token, err := windows.OpenCurrentProcessToken()
if err != nil {
return false
}
defer token.Close()

var linkLUID Luid
if !lookupPrivilegeValue("", "SeCreateSymbolicLinkPrivilege", &linkLUID) {
return false
}

requiredPrivs := &PRIVILEGE_SET{
PrivilegeCount: 1,
Control: 0,
Privilege: [ANYSIZE_ARRAY]LUID_AND_ATTRIBUTES{
{
Luid: linkLUID,
Attributes: SE_PRIVILEGE_ENABLED_BY_DEFAULT | SE_PRIVILEGE_ENABLED,
},
},
}
return havePrivilege(windows.Handle(token), requiredPrivs)
}

func requestUACLinkPrivilege() bool {
procHandle, err := windows.GetCurrentProcess()
if err != nil {
return false
}

var accessToken windows.Token
if err := windows.OpenProcessToken(procHandle, windows.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, &accessToken); err != nil {
return false
}
defer accessToken.Close()

var linkLUID Luid
if !lookupPrivilegeValue("", "SeCreateSymbolicLinkPrivilege", &linkLUID) {
return false
}

desiredPrivs := &TOKEN_PRIVILEGES{
PrivilegeCount: 1,
Privileges: [ANYSIZE_ARRAY]LUID_AND_ATTRIBUTES{
{
Luid: linkLUID,
Attributes: SE_PRIVILEGE_ENABLED,
},
},
}

desiredSize := uint32(unsafe.Sizeof(desiredPrivs))

if !adjustTokenPrivileges(windows.Handle(accessToken), false, desiredPrivs, desiredSize, nil, nil) {
return false
}

return true
}

func isGoLinkAware() bool {
for _, tag := range build.Default.ReleaseTags {
if tag == "go1.11" {
return true
}
}
return false
}

func isOSLinkAware() bool {
major, _, build := rawWinver()
if major < 10 {
return false
}
if major == 10 && build < 14972 { // First version to allow symlink creation by regular users, in dev mode
return false
}
return true
}

// TODO: [anyone] Replace with `windows.GetVersion()` when this is resolved: https://github.com/golang/go/issues/17835
func rawWinver() (major, minor, build uint32) {
type rtlOSVersionInfo struct {
dwOSVersionInfoSize uint32
dwMajorVersion uint32
dwMinorVersion uint32
dwBuildNumber uint32
dwPlatformId uint32
szCSDVersion [128]byte
}

ntoskrnl := windows.MustLoadDLL("ntoskrnl.exe")
defer ntoskrnl.Release()
proc := ntoskrnl.MustFindProc("RtlGetVersion")

var verStruct rtlOSVersionInfo
verStruct.dwOSVersionInfoSize = uint32(unsafe.Sizeof(verStruct))
proc.Call(uintptr(unsafe.Pointer(&verStruct)))

return verStruct.dwMajorVersion, verStruct.dwMinorVersion, verStruct.dwBuildNumber
}

// see https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development
func isDevModeActive() bool {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", registry.READ)
if err != nil {
return false
}

val, _, err := key.GetIntegerValue("AllowDevelopmentWithoutDevLicense")
if err != nil {
return false
}

return val != 0
}

//WINAPI wrappers
func privilegeCheck(ClientToken windows.Handle, RequiredPrivileges *PRIVILEGE_SET, pfResult *bool) bool {
if advapi32 == nil {
return false
}

proc, err := advapi32.FindProc("PrivilegeCheck")
if err != nil {
return false
}

r1, _, _ := proc.Call(
uintptr(ClientToken),
uintptr(unsafe.Pointer(RequiredPrivileges)),
uintptr(unsafe.Pointer(pfResult)),
)

return r1 == 1
}

func lookupPrivilegeValue(lpSystemName, lpName string, lpLuid *Luid) bool {
if advapi32 == nil {
return false
}

proc, err := advapi32.FindProc("LookupPrivilegeValueW")
if err != nil {
return false
}

snPtr, err := windows.UTF16PtrFromString(lpSystemName)
nPtr, err := windows.UTF16PtrFromString(lpName)

r1, _, _ := proc.Call(uintptr(unsafe.Pointer(snPtr)), uintptr(unsafe.Pointer(nPtr)), uintptr(unsafe.Pointer(lpLuid)))
return r1 == 1
}

func adjustTokenPrivileges(TokenHandle windows.Handle, DisableAllPrivileges bool, NewState *TOKEN_PRIVILEGES, BufferLength uint32, PreviousState *TOKEN_PRIVILEGES, ReturnLength *uint32) bool {
if advapi32 == nil {
return false
}

proc, err := advapi32.FindProc("AdjustTokenPrivileges")
if err != nil {
return false
}

var DisableAll uintptr
if DisableAllPrivileges {
DisableAll = 1
}

r1, _, err := proc.Call(
uintptr(TokenHandle),
DisableAll,
uintptr(unsafe.Pointer(NewState)),
uintptr(BufferLength),
uintptr(unsafe.Pointer(PreviousState)),
uintptr(unsafe.Pointer(ReturnLength)),
)

winErr := err.(syscall.Errno)
//call success, operation success
return r1 == 1 && winErr == 0
}
6 changes: 6 additions & 0 deletions docs/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ You can check that the ipfs output versions match with `go version` and `git rev
If `ipfs.exe` executes and everything matches, then building was successful.

## Troubleshooting
- **Symlinks**
On Windows, a process must hold a special privilege(`SeCreateSymbolicLinkPrivilege`) in order to create [symlinks](<https://en.wikipedia.org/wiki/Symbolic_link>). The way symlinks are implemented on Windows and the default security policies around them have caused some compatibility difficulties. As a result, the current behavior of `go-ipfs` on Windows, is to *not* create links if we do not have the ability to, instead, warning the user which links are being skipped, while still fetching the rest of the contents.
There are various ways to enable symlink creation, depending on your version of Windows. Currently we support users who hold the [`SeCreateSymbolicLinkPrivilege`](<https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links>), which covers Windows Vista+, as well users who have enabled "Developer Mode" in Windows 10(14972+).
W10+, see this article: <https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/>
Vista+, see this article: <http://answers.perforce.com/articles/KB/3472>

- **Git auth**
If you get authentication problems with Git, you might want to take a look at https://help.github.com/articles/caching-your-github-password-in-git/ and use the suggested solution:
`git config --global credential.helper wincred`
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,9 @@
},
{
"author": "whyrusleeping",
"hash": "QmQine7gvHncNevKtG9QXxf3nXcwSj6aDDmMm52mHofEEp",
"hash": "QmTkC7aeyDyjfdMTCVcG9P485TMJd6foLaLbf11DZ5WrnV",
"name": "tar-utils",
"version": "0.0.3"
"version": "0.1.0"
},
{
"author": "frist",
Expand Down

0 comments on commit b932bd3

Please sign in to comment.