update readme
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func findGo() string {
|
||||
if p, err := exec.LookPath("go"); err == nil {
|
||||
return p
|
||||
}
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
candidate := filepath.Join(goroot, "bin", "go")
|
||||
if fileExists(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
goexe := "go"
|
||||
if runtime.GOOS == "windows" {
|
||||
goexe = "go.exe"
|
||||
}
|
||||
|
||||
for _, p := range []string{
|
||||
"/usr/local/go/bin/go",
|
||||
filepath.Join(os.Getenv("HOME"), ".local", "go", "bin", goexe),
|
||||
"/tmp/go/bin/" + goexe,
|
||||
"/usr/lib/go/bin/go",
|
||||
`C:\Go\bin\go.exe`,
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Go", "bin", "go.exe"),
|
||||
filepath.Join(os.Getenv("LocalAppData"), "go", "bin", goexe),
|
||||
filepath.Join(os.Getenv("HOME"), "sdk", "go", "bin", goexe),
|
||||
filepath.Join(os.TempDir(), "go", "bin", goexe),
|
||||
} {
|
||||
if fileExists(p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return goexe
|
||||
}
|
||||
|
||||
type goFile struct {
|
||||
Filename string `json:"filename"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
type goVersion struct {
|
||||
Version string `json:"version"`
|
||||
Stable bool `json:"stable"`
|
||||
Files []goFile `json:"files"`
|
||||
}
|
||||
|
||||
func ensureGo() (string, error) {
|
||||
if p, err := exec.LookPath("go"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
candidate := filepath.Join(goroot, "bin", "go")
|
||||
if fileExists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
if p := findGo(); fileExists(p) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
log.Printf("[*] Go not found, installing...")
|
||||
|
||||
osName := runtime.GOOS
|
||||
archName := runtime.GOARCH
|
||||
switch archName {
|
||||
case "x86_64":
|
||||
archName = "amd64"
|
||||
case "aarch64":
|
||||
archName = "arm64"
|
||||
}
|
||||
|
||||
ext := ".tar.gz"
|
||||
if osName == "windows" {
|
||||
ext = ".zip"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/VERSION?m=text", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create version request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("check go version: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
versionBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read go version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(strings.Split(string(versionBytes), "\n")[0])
|
||||
|
||||
filename := fmt.Sprintf("%s.%s-%s%s", version, osName, archName, ext)
|
||||
url := fmt.Sprintf("https://go.dev/dl/%s", filename)
|
||||
|
||||
// Fetch checksums from go.dev JSON API
|
||||
checksums, err := fetchGoChecksums()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch go checksums: %w", err)
|
||||
}
|
||||
expectedHash, ok := checksums[filename]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no checksum found for %s", filename)
|
||||
}
|
||||
|
||||
log.Printf("[*] downloading %s ...", url)
|
||||
|
||||
tarball, err := os.CreateTemp("", "go-*"+ext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tarball.Name())
|
||||
|
||||
dlCtx, dlCancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer dlCancel()
|
||||
dlReq, err := http.NewRequestWithContext(dlCtx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create download request: %w", err)
|
||||
}
|
||||
dlResp, err := http.DefaultClient.Do(dlReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download go: %w", err)
|
||||
}
|
||||
defer dlResp.Body.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(io.MultiWriter(tarball, hash), dlResp.Body); err != nil {
|
||||
return "", fmt.Errorf("write go archive: %w", err)
|
||||
}
|
||||
tarball.Close()
|
||||
|
||||
got := hex.EncodeToString(hash.Sum(nil))
|
||||
if got != expectedHash {
|
||||
return "", fmt.Errorf("checksum mismatch for %s:\n expected: %s\n got: %s", filename, expectedHash, got)
|
||||
}
|
||||
log.Printf("[*] SHA256 verified: %s", expectedHash)
|
||||
|
||||
installDir := installGo(tarball.Name(), osName)
|
||||
|
||||
log.Printf("[+] Go %s installed at %s", version, installDir)
|
||||
return filepath.Join(installDir, "bin", "go"), nil
|
||||
}
|
||||
|
||||
func fetchGoChecksums() (map[string]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/dl/?mode=json&include=all", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create metadata request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get go metadata: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var versions []goVersion
|
||||
if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
|
||||
return nil, fmt.Errorf("decode go metadata: %w", err)
|
||||
}
|
||||
|
||||
checksums := make(map[string]string)
|
||||
for _, v := range versions {
|
||||
for _, f := range v.Files {
|
||||
checksums[f.Filename] = f.SHA256
|
||||
}
|
||||
}
|
||||
return checksums, nil
|
||||
}
|
||||
|
||||
func installGo(archive, osName string) string {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
|
||||
candidates := []string{
|
||||
"/usr/local/go",
|
||||
filepath.Join(homeDir, ".local", "go"),
|
||||
"/tmp/go",
|
||||
}
|
||||
if osName == "windows" {
|
||||
progFiles := os.Getenv("ProgramFiles")
|
||||
if progFiles == "" {
|
||||
progFiles = `C:\Program Files`
|
||||
}
|
||||
candidates = []string{
|
||||
filepath.Join(progFiles, "Go"),
|
||||
filepath.Join(homeDir, "sdk", "go"),
|
||||
filepath.Join(os.TempDir(), "go"),
|
||||
}
|
||||
}
|
||||
|
||||
for _, dst := range candidates {
|
||||
parent := filepath.Dir(dst)
|
||||
os.RemoveAll(dst)
|
||||
os.MkdirAll(parent, 0755)
|
||||
if extractGoArchive(archive, parent, osName) == nil {
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
log.Printf("[*] Go extracted to %s", dst)
|
||||
return dst
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: always try the root of the temp dir
|
||||
tmpRoot := "/tmp"
|
||||
if osName == "windows" {
|
||||
tmpRoot = os.TempDir()
|
||||
}
|
||||
os.RemoveAll(filepath.Join(tmpRoot, "go"))
|
||||
extractGoArchive(archive, tmpRoot, osName)
|
||||
return filepath.Join(tmpRoot, "go")
|
||||
}
|
||||
|
||||
func extractGoArchive(archive, dst, osName string) error {
|
||||
if osName == "windows" {
|
||||
return extractZip(archive, dst)
|
||||
}
|
||||
return exec.Command("tar", "-C", dst, "-xzf", archive).Run()
|
||||
}
|
||||
|
||||
func extractZip(src, dst string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open zip: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
target := filepath.Join(dst, f.Name)
|
||||
|
||||
// ZipSlip protection
|
||||
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(dst)+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(target, 0755)
|
||||
continue
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(target), 0755)
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s in zip: %w", f.Name, err)
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return fmt.Errorf("create %s: %w", target, err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, rc)
|
||||
rc.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("extract %s: %w", f.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findGit(dir string) string {
|
||||
gitExe := "git"
|
||||
if runtime.GOOS == "windows" {
|
||||
gitExe = "git.exe"
|
||||
}
|
||||
var found string
|
||||
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() && strings.EqualFold(d.Name(), gitExe) {
|
||||
found = path
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
// auto-installs MinGit on Windows, tries system package manager on Linux/macOS.
|
||||
// Self-contained implant generation is the feature — defer per-platform packaging.
|
||||
func installGit() (string, error) {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
installDir := filepath.Join(homeDir, ".necropolis", "mingit")
|
||||
|
||||
if p := findGit(installDir); p != "" {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
log.Printf("[*] downloading MinGit...")
|
||||
url := "https://github.com/git-for-windows/git/releases/download/v2.47.0.windows.1/MinGit-2.47.0-64-bit.zip"
|
||||
archive, err := os.CreateTemp("", "mingit-*.zip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp: %w", err)
|
||||
}
|
||||
defer os.Remove(archive.Name())
|
||||
|
||||
if err := downloadFile(url, archive); err != nil {
|
||||
return "", fmt.Errorf("download mingit: %w", err)
|
||||
}
|
||||
archive.Close()
|
||||
|
||||
os.RemoveAll(installDir)
|
||||
os.MkdirAll(installDir, 0755)
|
||||
if err := extractZip(archive.Name(), installDir); err != nil {
|
||||
return "", fmt.Errorf("extract mingit: %w", err)
|
||||
}
|
||||
|
||||
if p := findGit(installDir); p != "" {
|
||||
log.Printf("[+] MinGit installed at %s", p)
|
||||
return p, nil
|
||||
}
|
||||
return "", fmt.Errorf("MinGit extracted but git executable not found under %s", installDir)
|
||||
|
||||
case "linux":
|
||||
for _, pm := range [][]string{
|
||||
{"apk", "add", "git"},
|
||||
{"apt-get", "install", "-y", "git"},
|
||||
{"yum", "install", "-y", "git"},
|
||||
} {
|
||||
if _, err := exec.LookPath(pm[0]); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(pm[0], pm[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if cmd.Run() == nil {
|
||||
if p, err := exec.LookPath("git"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("could not install git via package manager; install git manually")
|
||||
|
||||
case "darwin":
|
||||
for _, pm := range [][]string{
|
||||
{"xcode-select", "--install"},
|
||||
{"brew", "install", "git"},
|
||||
} {
|
||||
if _, err := exec.LookPath(pm[0]); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(pm[0], pm[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if cmd.Run() == nil {
|
||||
if p, err := exec.LookPath("git"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("could not install git; install git manually")
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(url string, out *os.File) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user