mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
c20f9af712
- Remove POST /diff/files endpoint that allowed arbitrary host file reads (CWE-22); clients must now read files locally and POST content to /diff/blobs instead - Add validatePublicURL() to /info remote endpoints, rejecting URLs that resolve to loopback, private, link-local, or multicast addresses to prevent SSRF; also remove attacker-controlled proxy/insecure params - Add SanitizeArchivePath() helper that verifies extracted archive entry paths stay within the destination directory (zip-slip / tar-slip, CWE-22); replace bare filepath.Join(dest, filepath.Clean(name)) calls in SearchZip, OTA parser, AA payload extractor, and ota_extract - Fix server listen address to use net.JoinHostPort to respect Host config
538 lines
14 KiB
Go
538 lines
14 KiB
Go
package utils
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"crypto/sha1"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/apex/log/handlers/cli"
|
|
"github.com/vbauerster/mpb/v8"
|
|
"github.com/vbauerster/mpb/v8/decor"
|
|
)
|
|
|
|
var normalPadding = cli.Default.Padding
|
|
|
|
// Retry will retry a function f a number of attempts with a sleep duration in between
|
|
func Retry(attempts int, sleep time.Duration, f func() error) error {
|
|
_, err := RetryWithResult(attempts, sleep, func() (struct{}, error) {
|
|
return struct{}{}, f()
|
|
})
|
|
return err
|
|
}
|
|
|
|
type StopRetryingError struct {
|
|
Err error
|
|
}
|
|
|
|
func (e *StopRetryingError) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// RetryWithResult will retry a function f a number of attempts with a sleep duration in between, returning the result if successful
|
|
func RetryWithResult[T any](attempts int, sleep time.Duration, f func() (T, error)) (result T, err error) {
|
|
for i := range attempts {
|
|
if i > 0 {
|
|
Indent(log.Debug, 2)(fmt.Sprintf("retrying after error: %s", err))
|
|
time.Sleep(sleep)
|
|
sleep *= 2
|
|
}
|
|
result, err = f()
|
|
if err == nil {
|
|
return result, nil
|
|
}
|
|
// If StopRetryingError is returned, unwrap and return immediately
|
|
var stopErr *StopRetryingError
|
|
if errors.As(err, &stopErr) {
|
|
return result, stopErr.Err
|
|
}
|
|
}
|
|
return result, fmt.Errorf("after %d attempts, last error: %s", attempts, err)
|
|
}
|
|
|
|
func RandomAgent() string {
|
|
var userAgents = []string{
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38",
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0",
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38",
|
|
}
|
|
return userAgents[rand.Int()%len(userAgents)]
|
|
}
|
|
|
|
// Indent indents apex log line to supplied level
|
|
func Indent(f func(s string), level int) func(string) {
|
|
return func(s string) {
|
|
cli.Default.Padding = normalPadding * level
|
|
f(s)
|
|
cli.Default.Padding = normalPadding
|
|
}
|
|
}
|
|
|
|
// Uint64SliceContains returns true if uint64 slice contains given uint64
|
|
func Uint64SliceContains(slice []uint64, item uint64) bool {
|
|
return slices.Contains(slice, item)
|
|
}
|
|
|
|
// Unique returns a slice with only unique elements
|
|
func Unique[T comparable](s []T) []T {
|
|
inResult := make(map[T]bool)
|
|
var result []T
|
|
for _, str := range s {
|
|
if _, ok := inResult[str]; !ok {
|
|
inResult[str] = true
|
|
result = append(result, str)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func UniqueAppend[T comparable](slice []T, i T) []T {
|
|
if slices.Contains(slice, i) {
|
|
return slice
|
|
}
|
|
return append(slice, i)
|
|
}
|
|
|
|
func UniqueConcat[T comparable](slice []T, in []T) []T {
|
|
for _, i := range in {
|
|
if slices.Contains(slice, i) {
|
|
return slice
|
|
}
|
|
slice = append(slice, i)
|
|
}
|
|
return slice
|
|
}
|
|
|
|
type Pair[T, U any] struct {
|
|
First T
|
|
Second U
|
|
}
|
|
|
|
func Zip[T, U any](ts []T, us []U) ([]Pair[T, U], error) {
|
|
if len(ts) != len(us) {
|
|
return nil, fmt.Errorf("slices have different lengths")
|
|
}
|
|
pairs := make([]Pair[T, U], len(ts))
|
|
for i := range ts {
|
|
pairs[i] = Pair[T, U]{ts[i], us[i]}
|
|
}
|
|
return pairs, nil
|
|
}
|
|
|
|
func Reverse[T any](input []T) {
|
|
for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 {
|
|
input[i], input[j] = input[j], input[i]
|
|
}
|
|
}
|
|
|
|
func Difference[T comparable](a []T, b []T) []T {
|
|
amap := make(map[T]bool)
|
|
for _, item := range a {
|
|
amap[item] = true
|
|
}
|
|
for _, item := range b {
|
|
delete(amap, item)
|
|
}
|
|
return slices.Collect(maps.Keys(amap))
|
|
}
|
|
|
|
// ReverseBytes reverse byte array order
|
|
func ReverseBytes(a []byte) []byte {
|
|
for i := len(a)/2 - 1; i >= 0; i-- {
|
|
opp := len(a) - 1 - i
|
|
a[i], a[opp] = a[opp], a[i]
|
|
}
|
|
return a
|
|
}
|
|
|
|
// Verify verifies the downloaded against it's hash
|
|
func Verify(sha1sum, name string) (bool, error) {
|
|
f, err := os.Open(name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer f.Close()
|
|
|
|
h := sha1.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
match := strings.EqualFold(sha1sum, fmt.Sprintf("%x", h.Sum(nil)))
|
|
|
|
if !match {
|
|
Indent(log.WithFields(log.Fields{
|
|
"expected": sha1sum,
|
|
"actual": fmt.Sprintf("%x", h.Sum(nil)),
|
|
}).Error, 3)("BAD CHECKSUM")
|
|
}
|
|
|
|
return match, nil
|
|
}
|
|
|
|
// SanitizeArchivePath joins an untrusted archive entry name to a destination
|
|
// directory and verifies the result stays within that directory. Returns an
|
|
// error if the entry name would escape via path traversal (zip-slip / tar-slip).
|
|
//
|
|
// This must be used instead of filepath.Join(dest, filepath.Clean(name)) when
|
|
// name comes from an archive — filepath.Clean does NOT strip leading "../".
|
|
//
|
|
// Lexical check only: does NOT resolve symlinks. If dest already contains a
|
|
// symlink pointing outside, writes through it will escape. Callers must not
|
|
// create symlinks from untrusted archive entries (SearchZip writes them as
|
|
// regular files; UnTarGz rejects tar.TypeSymlink via the default case).
|
|
func SanitizeArchivePath(dest, name string) (string, error) {
|
|
target := filepath.Join(dest, name)
|
|
absDest, err := filepath.Abs(dest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve destination: %w", err)
|
|
}
|
|
absTarget, err := filepath.Abs(target)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve target: %w", err)
|
|
}
|
|
rel, err := filepath.Rel(absDest, absTarget)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("archive entry %q escapes destination directory", name)
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
// SearchZip searches for files in a zip.Reader
|
|
func SearchZip(files []*zip.File, pattern *regexp.Regexp, folder string, flat, progress bool) ([]string, error) {
|
|
var fname string
|
|
var artifacts []string
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, f := range files {
|
|
if pattern.MatchString(f.Name) {
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
found = true
|
|
if flat {
|
|
fname = filepath.Join(folder, filepath.Base(f.Name))
|
|
} else {
|
|
fname, err = SanitizeArchivePath(folder, f.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
|
|
return nil, fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
|
|
}
|
|
|
|
var r io.ReadCloser
|
|
if _, err := os.Stat(fname); os.IsNotExist(err) {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error opening remote zipped file %s: %v", f.Name, err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
var p *mpb.Progress
|
|
if progress {
|
|
// setup progress bar
|
|
var total = int64(f.UncompressedSize64)
|
|
p = mpb.New(
|
|
mpb.WithWidth(60),
|
|
mpb.WithRefreshRate(180*time.Millisecond),
|
|
)
|
|
bar := p.New(total,
|
|
mpb.BarStyle().Lbound("[").Filler("=").Tip(">").Padding("-").Rbound("|"),
|
|
mpb.PrependDecorators(
|
|
decor.CountersKibiByte("\t% .2f / % .2f"),
|
|
),
|
|
mpb.AppendDecorators(
|
|
decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "✅ "),
|
|
decor.Name(" ] "),
|
|
decor.AverageSpeed(decor.SizeB1024(0), "% .2f", decor.WCSyncWidth),
|
|
),
|
|
)
|
|
// create proxy reader
|
|
r = bar.ProxyReader(io.LimitReader(rc, total))
|
|
defer r.Close()
|
|
} else {
|
|
r = rc
|
|
}
|
|
|
|
Indent(log.Debug, 2)(fmt.Sprintf("Extracting %s", strings.TrimPrefix(fname, cwd)))
|
|
out, err := os.Create(fname)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating remote unzipped file destination %s: %v", fname, err)
|
|
}
|
|
defer out.Close()
|
|
|
|
io.Copy(out, r)
|
|
|
|
if progress {
|
|
// wait for our bar to complete and flush and close remote zip and temp file
|
|
p.Wait()
|
|
}
|
|
|
|
artifacts = append(artifacts, fname)
|
|
} else {
|
|
Indent(log.Warn, 2)(fmt.Sprintf("%s already exists", fname))
|
|
artifacts = append(artifacts, fname)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, fmt.Errorf("no files found matching %s", pattern.String())
|
|
}
|
|
|
|
return artifacts, nil
|
|
}
|
|
|
|
// SearchPartialZip searches for files in a zip.Reader and returns a byte slice of the file
|
|
func SearchPartialZip(files []*zip.File, pattern *regexp.Regexp, folder string, size int64, flat, progress bool) ([]string, error) {
|
|
var fname string
|
|
var artifacts []string
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, f := range files {
|
|
if pattern.MatchString(f.Name) {
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
found = true
|
|
if flat {
|
|
fname = filepath.Join(folder, filepath.Base(f.Name))
|
|
} else {
|
|
fname, err = SanitizeArchivePath(folder, f.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
|
|
return nil, fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
|
|
}
|
|
|
|
var r io.ReadCloser
|
|
if _, err := os.Stat(fname); os.IsNotExist(err) {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error opening remote zipped file %s: %v", f.Name, err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
var p *mpb.Progress
|
|
if progress {
|
|
// setup progress bar
|
|
var total = int64(size)
|
|
p = mpb.New(
|
|
mpb.WithWidth(60),
|
|
mpb.WithRefreshRate(180*time.Millisecond),
|
|
)
|
|
bar := p.New(total,
|
|
mpb.BarStyle().Lbound("[").Filler("=").Tip(">").Padding("-").Rbound("|"),
|
|
mpb.PrependDecorators(
|
|
decor.CountersKibiByte("\t% .2f / % .2f"),
|
|
),
|
|
mpb.AppendDecorators(
|
|
decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "✅ "),
|
|
decor.Name(" ] "),
|
|
decor.AverageSpeed(decor.SizeB1024(0), "% .2f", decor.WCSyncWidth),
|
|
),
|
|
)
|
|
// create proxy reader
|
|
r = bar.ProxyReader(io.LimitReader(rc, total))
|
|
defer r.Close()
|
|
} else {
|
|
r = rc
|
|
}
|
|
|
|
Indent(log.Debug, 2)(fmt.Sprintf("Extracting %#x bytes of %s", size, strings.TrimPrefix(fname, cwd)))
|
|
out, err := os.Create(fname)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating remote unzipped file destination %s: %v", fname, err)
|
|
}
|
|
defer out.Close()
|
|
|
|
io.CopyN(out, r, size)
|
|
|
|
if progress {
|
|
// wait for our bar to complete and flush and close remote zip and temp file
|
|
p.Wait()
|
|
}
|
|
|
|
artifacts = append(artifacts, fname)
|
|
} else {
|
|
Indent(log.Warn, 2)(fmt.Sprintf("%s already exists", fname))
|
|
artifacts = append(artifacts, fname)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, fmt.Errorf("no files found matching %s", pattern.String())
|
|
}
|
|
|
|
return artifacts, nil
|
|
}
|
|
|
|
// Unzip - https://stackoverflow.com/a/24792688
|
|
func Unzip(src, dest string, filter func(f *zip.File) bool) ([]string, error) {
|
|
|
|
var fNames []string
|
|
|
|
r, err := zip.OpenReader(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := r.Close(); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
os.MkdirAll(dest, 0750)
|
|
|
|
// Closure to address file descriptors issue with all the deferred .Close() methods
|
|
extractAndWriteFile := func(f *zip.File) error {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := rc.Close(); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
path := filepath.Join(dest, filepath.Base(filepath.Clean(f.Name)))
|
|
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(path, 0750)
|
|
} else {
|
|
// TODO: add the ability to preserve folder structure if user wants
|
|
// os.MkdirAll(filepath.Dir(path), 0750)
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
Indent(log.Debug, 2)(fmt.Sprintf("Extracted %s from %s", path, filepath.Base(src)))
|
|
_, err = io.Copy(f, rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, f := range r.File {
|
|
if filter(f) {
|
|
fNames = append(fNames, filepath.Join(dest, filepath.Base(filepath.Clean(f.Name))))
|
|
err := extractAndWriteFile(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return fNames, nil
|
|
}
|
|
|
|
// UnTarGz - https://stackoverflow.com/a/57640231
|
|
func UnTarGz(tarfile, destPath string) error {
|
|
|
|
r, err := os.Open(tarfile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uncompressedStream, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tarReader := tar.NewReader(uncompressedStream)
|
|
|
|
for {
|
|
header, err := tarReader.Next()
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("tarReader.Next() failed: %v", err)
|
|
}
|
|
|
|
target, err := SanitizeArchivePath(destPath, header.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(target, 0755); err != nil {
|
|
return fmt.Errorf("os.MkdirAll failed: %v", err)
|
|
}
|
|
case tar.TypeReg:
|
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
|
return fmt.Errorf("os.MkdirAll failed: %v", err)
|
|
}
|
|
outFile, err := os.Create(target)
|
|
if err != nil {
|
|
return fmt.Errorf("os.Create failed: %v", err)
|
|
}
|
|
defer outFile.Close()
|
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
|
return fmt.Errorf("io.Copy failed: %v", err)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("uknown type: %v in %s",
|
|
header.Typeflag,
|
|
header.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Sha1(in string) (string, error) {
|
|
f, err := os.Open(in)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
h := sha1.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
|
}
|