diff --git a/core/commands/get.go b/core/commands/get.go index b49059066e14..b3f8f740de31 100644 --- a/core/commands/get.go +++ b/core/commands/get.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + gopath "path" "path/filepath" "strings" @@ -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.", @@ -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) { diff --git a/core/commands/get_sanitize_windows.go b/core/commands/get_sanitize_windows.go new file mode 100644 index 000000000000..e7b1b8a6095a --- /dev/null +++ b/core/commands/get_sanitize_windows.go @@ -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 +} diff --git a/docs/windows.md b/docs/windows.md index c5819395591b..085fa1e70a58 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -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](). 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`](), which covers Windows Vista+, as well users who have enabled "Developer Mode" in Windows 10(14972+). +W10+, see this article: +Vista+, see this article: + - **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` diff --git a/package.json b/package.json index bd5cf7bf61b5..606e68ab7360 100644 --- a/package.json +++ b/package.json @@ -560,9 +560,9 @@ }, { "author": "whyrusleeping", - "hash": "QmQine7gvHncNevKtG9QXxf3nXcwSj6aDDmMm52mHofEEp", + "hash": "QmTkC7aeyDyjfdMTCVcG9P485TMJd6foLaLbf11DZ5WrnV", "name": "tar-utils", - "version": "0.0.3" + "version": "0.1.0" }, { "author": "frist",