feat: ipsw daemon (API) (#219)

This commit is contained in:
blacktop
2023-04-12 20:00:05 -06:00
committed by GitHub
parent 753a14a88e
commit f85daa1ac3
94 changed files with 6405 additions and 944 deletions
-1
View File
@@ -23,7 +23,6 @@ __debug_bin
*.zip
*.im4p
*_Restore
System
usr
*.xz
*.bin
+253 -40
View File
@@ -136,7 +136,7 @@ builds:
env:
- CGO_ENABLED=1
- CC=zig cc -target x86_64-linux-musl
- CC=zig c++ -target x86_64-linux-musl
- CXX=zig c++ -target x86_64-linux-musl
goos:
- linux
goarch:
@@ -151,7 +151,7 @@ builds:
env:
- CGO_ENABLED=1
- CC=zig cc -target aarch64-linux-musl
- CC=zig c++ -target aarch64-linux-musl
- CXX=zig c++ -target aarch64-linux-musl
goos:
- linux
goarch:
@@ -172,7 +172,7 @@ builds:
# env:
# - CGO_ENABLED=1
# - CC=zig cc -target x86_64-linux-musl
# - CC=zig c++ -target x86_64-linux-musl
# - CXX=zig c++ -target x86_64-linux-musl
# - CGO_LDFLAGS=-L/tmp/install_x86_64/usr/local/lib
# - PKG_CONFIG_PATH=/tmp/install_x86_64/usr/local/lib/pkgconfig
# - CGO_CFLAGS=-I/tmp/install_x86_64/usr/local/include/libusb-1.0 -I/tmp/install_x86_64/usr/local/include
@@ -199,7 +199,7 @@ builds:
# env:
# - CGO_ENABLED=1
# - CC=zig cc -target aarch64-linux-musl
# - CC=zig c++ -target aarch64-linux-musl
# - CXX=zig c++ -target aarch64-linux-musl
# - CGO_LDFLAGS=-L/tmp/install_aarch64/usr/local/lib
# - PKG_CONFIG_PATH=/tmp/install_aarch64/usr/local/lib/pkgconfig
# - CGO_CFLAGS=-I/tmp/install_aarch64/usr/local/include/libusb-1.0 -I/tmp/install_aarch64/usr/local/include
@@ -220,7 +220,7 @@ builds:
env:
- CGO_ENABLED=1
- CC=zig cc -target x86_64-windows-gnu
- CC=zig c++ -target x86_64-windows-gnu
- CXX=zig c++ -target x86_64-windows-gnu
goos:
- windows
goarch:
@@ -235,7 +235,7 @@ builds:
env:
- CGO_ENABLED=1
- CC=zig cc -target aarch64-windows-gnu
- CC=zig c++ -target aarch64-windows-gnu
- CXX=zig c++ -target aarch64-windows-gnu
goos:
- windows
goarch:
@@ -244,10 +244,87 @@ builds:
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppVersion={{.Version}} -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppBuildTime={{.Date}}
# DAEMONS ###########################################################
- id: darwin_daemon_build
main: ./cmd/ipswd
binary: ipswd
env:
- CGO_ENABLED=1
goos:
- darwin
goarch:
- amd64
- arm64
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/api/types.BuildVersion={{.Version}} -X github.com/blacktop/ipsw/api/types.BuildTime={{.Date}}
- id: linux_amd64_daemon_build
main: ./cmd/ipswd
binary: ipswd
# env:
# - CGO_ENABLED=1
# - CC=zig cc -target x86_64-linux-musl
# - CXX=zig c++ -target x86_64-linux-musl
goos:
- linux
goarch:
- amd64
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/api/types.BuildVersion={{.Version}} -X github.com/blacktop/ipsw/api/types.BuildTime={{.Date}}
- id: linux_arm64_daemon_build
main: ./cmd/ipswd
binary: ipswd
# env:
# - CGO_ENABLED=1
# - CC=zig cc -target aarch64-linux-musl
# - CXX=zig c++ -target aarch64-linux-musl
goos:
- linux
goarch:
- arm64
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/api/types.BuildVersion={{.Version}} -X github.com/blacktop/ipsw/api/types.BuildTime={{.Date}}
- id: windows_amd64_daemon_build
main: ./cmd/ipswd
binary: ipswd
env:
- CGO_ENABLED=1
- CC=zig cc -target x86_64-windows-gnu
- CXX=zig c++ -target x86_64-windows-gnu
goos:
- windows
goarch:
- amd64
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/api/types.BuildVersion={{.Version}} -X github.com/blacktop/ipsw/api/types.BuildTime={{.Date}}
- id: windows_arm64_daemon_build
main: ./cmd/ipswd
binary: ipswd
env:
- CGO_ENABLED=1
- CC=zig cc -target aarch64-windows-gnu
- CXX=zig c++ -target aarch64-windows-gnu
goos:
- windows
goarch:
- arm64
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags: -s -w -X github.com/blacktop/ipsw/api/types.BuildVersion={{.Version}} -X github.com/blacktop/ipsw/api/types.BuildTime={{.Date}}
universal_binaries:
- id: darwin_build
replace: false
- id: darwin_daemon_build
replace: false
archives:
- id: default_archive
@@ -272,7 +349,7 @@ archives:
files:
- README.md
- LICENSE
- completions/*
- completions/ipsw/*
- manpages/*
wrap_in_directory: true
- id: extras_archive
@@ -294,7 +371,7 @@ archives:
files:
- README.md
- LICENSE
- completions/*
- completions/ipsw/*
- manpages/*
wrap_in_directory: true
- id: frida_archive
@@ -314,9 +391,31 @@ archives:
files:
- README.md
- LICENSE
- completions/*
- completions/ipsw/*
- manpages/*
wrap_in_directory: true
- id: daemon_archive
builds:
- darwin_daemon_build
- linux_amd64_daemon_build
- linux_arm64_daemon_build
- windows_amd64_daemon_build
- windows_arm64_daemon_build
name_template: >-
{{ .ProjectName }}_{{ .Version }}_
{{- if eq .Os "darwin" }}macOS
{{- else if eq .Os "ios" }}iOS
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else if eq .Arch "all" }}universal
{{- else }}{{ .Arch }}{{ end }}_daemon
rlcp: true
files:
- README.md
- LICENSE
- completions/ipswd/*
wrap_in_directory: true
brews:
- tap:
@@ -341,9 +440,9 @@ brews:
- ipsw-frida
install: |
bin.install "ipsw"
bash_completion.install "completions/_bash" => "ipsw"
zsh_completion.install "completions/_zsh" => "_ipsw"
fish_completion.install "completions/_fish" => "ipsw.fish"
bash_completion.install "completions/ipsw/_bash" => "ipsw"
zsh_completion.install "completions/ipsw/_zsh" => "_ipsw"
fish_completion.install "completions/ipsw/_fish" => "ipsw.fish"
man1.install Dir["manpages/*"]
test: |
system "#{bin}/ipsw --version"
@@ -366,12 +465,37 @@ brews:
- ipsw
install: |
bin.install "ipsw"
bash_completion.install "completions/_bash" => "ipsw"
zsh_completion.install "completions/_zsh" => "_ipsw"
fish_completion.install "completions/_fish" => "ipsw.fish"
bash_completion.install "completions/ipsw/_bash" => "ipsw"
zsh_completion.install "completions/ipsw/_zsh" => "_ipsw"
fish_completion.install "completions/ipsw/_fish" => "ipsw.fish"
man1.install Dir["manpages/*"]
test: |
system "#{bin}/ipsw --version"
- name: ipswd
tap:
owner: blacktop
name: homebrew-tap
ids:
- daemon_archive
folder: Formula
homepage: "https://github.com/blacktop/ipsw"
description: "ipsw - Daemon."
license: MIT
dependencies:
- name: ipsw
- name: libusb
type: optional
install: |
bin.install "ipswd"
bash_completion.install "completions/ipswd/_bash" => "ipswd"
zsh_completion.install "completions/ipswd/_zsh" => "_ipswd"
fish_completion.install "completions/ipswd/_fish" => "ipswd.fish"
service: |
run: ipswd
keep_alive: true
sockets: /var/run/ipsw.sock
test: |
system "#{bin}/ipswd --version"
checksum:
name_template: "checksums.txt"
@@ -401,9 +525,9 @@ aurs:
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/ipsw" "${pkgdir}/usr/share/bash-completion/completions/ipsw"
install -Dm644 "./completions/_ipsw" "${pkgdir}/usr/share/zsh/site-functions/_ipsw"
install -Dm644 "./completions/ipsw.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/ipsw.fish"
install -Dm644 "./completions/ipsw/_bash" "${pkgdir}/usr/share/bash-completion/completions/ipsw"
install -Dm644 "./completions/ipsw/_zsh" "${pkgdir}/usr/share/zsh/site-functions/_ipsw"
install -Dm644 "./completions/ipsw/_fish" "${pkgdir}/usr/share/fish/vendor_completions.d/ipsw.fish"
# man pages
install -Dm644 "./manpages/ipsw.1.gz" "${pkgdir}/usr/share/man/man1/ipsw.1.gz"
@@ -422,16 +546,16 @@ nfpms:
bindir: /usr/bin
section: utils
contents:
- src: ./completions/_bash
- src: ./completions/ipsw/_bash
dst: /usr/share/bash-completion/completions/ipsw
file_info:
mode: 0644
- src: ./completions/_fish
dst: /usr/share/fish/vendor_completions.d/ipsw.fish
- src: ./completions/ipsw/_zsh
dst: /usr/share/zsh/vendor-completions/_ipsw
file_info:
mode: 0644
- src: ./completions/_zsh
dst: /usr/share/zsh/vendor-completions/_ipsw
- src: ./completions/ipsw/_fish
dst: /usr/share/fish/vendor_completions.d/ipsw.fish
file_info:
mode: 0644
- src: ./manpages/ipsw.1.gz
@@ -457,6 +581,61 @@ nfpms:
lintian_overrides:
- statically-linked-binary
- changelog-file-missing-in-native-package
- id: daemon
builds:
- linux_amd64_daemon_build
- linux_arm64_daemon_build
homepage: https://github.com/blacktop/ipsw
description: |-
ipsw daemon.
maintainer: Blacktop <https://github.com/blacktop>
license: MIT
vendor: Blacktop
bindir: /usr/bin
section: utils
contents:
- src: ./hack/goreleaser/linux_install/ipsw.service
dst: /lib/systemd/system/ipsws.service
file_info:
mode: 0644
- src: ./hack/goreleaser/linux_install/ipsw.socket
dst: /lib/systemd/system/ipsws.socket
file_info:
mode: 0644
- src: ./completions/ipswd/_bash
dst: /usr/share/bash-completion/completions/ipswd
file_info:
mode: 0644
- src: ./completions/ipswd/_zsh
dst: /usr/share/zsh/vendor-completions/_ipswd
file_info:
mode: 0644
- src: ./completions/ipswd/_fish
dst: /usr/share/fish/vendor_completions.d/ipswd.fish
file_info:
mode: 0644
- src: ./LICENSE
dst: /usr/share/doc/ipsw/copyright
file_info:
mode: 0644
scripts:
postinstall: "./hack/goreleaser/linux_install/postinstall.sh"
postremove: "./hack/goreleaser/linux_install/postremove.sh"
file_name_template: >-
{{ .ProjectName }}_{{ .Version }}_
{{- .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}_daemon
formats:
- deb
- rpm
- apk
- archlinux
deb:
lintian_overrides:
- statically-linked-binary
- changelog-file-missing-in-native-package
# - id: packages_extras
# file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}_extras"
# builds:
@@ -471,18 +650,18 @@ nfpms:
# bindir: /usr/bin
# section: utils
# contents:
# - src: ./completions/_bash
# dst: /usr/share/bash-completion/completions/ipsw
# file_info:
# mode: 0644
# - src: ./completions/_fish
# dst: /usr/share/fish/completions/ipsw.fish
# file_info:
# mode: 0644
# - src: ./completions/_zsh
# dst: /usr/share/zsh/vendor-completions/_ipsw
# file_info:
# mode: 0644
# - src: ./completions/ipsw/_bash
# dst: /usr/share/bash-completion/completions/ipsw
# file_info:
# mode: 0644
# - src: ./completions/ipsw/_zsh
# dst: /usr/share/zsh/vendor-completions/_ipsw
# file_info:
# mode: 0644
# - src: ./completions/ipsw/_fish
# dst: /usr/share/fish/vendor_completions.d/ipsw.fish
# file_info:
# mode: 0644
# - src: ./manpages/ipsw.1.gz
# dst: /usr/share/man/man1/ipsw.1.gz
# file_info:
@@ -517,15 +696,15 @@ snapcrafts:
publish: true
license: MIT
extra_files:
- source: ./completions/_bash
- source: ./completions/ipsw/_bash
destination: /usr/share/bash-completion/completions/ipsw
mode: 0644
- source: ./completions/_fish
destination: /usr/share/fish/vendor_completions.d/ipsw.fish
mode: 0644
- source: ./completions/_zsh
- source: ./completions/ipsw/_zsh
destination: /usr/share/zsh/vendor-completions/_ipsw
mode: 0644
- source: ./completions/ipsw/_fish
destination: /usr/share/fish/vendor_completions.d/ipsw.fish
mode: 0644
- source: ./manpages/ipsw.1.gz
destination: /usr/share/man/man1/ipsw.1.gz
mode: 0644
@@ -536,6 +715,40 @@ snapcrafts:
ipsw:
command: ipsw
plugs: ["home", "network"]
- id: snaps-daemon
builds:
- linux_amd64_daemon_build
- linux_arm64_daemon_build
summary: ipsw - Daemon.
description: |
ipsw - Daemon.
grade: stable
confinement: strict
publish: true
license: MIT
extra_files:
- source: ./completions/ipsw/_bash
destination: /usr/share/bash-completion/completions/ipsw
mode: 0644
- source: ./completions/ipsw/_zsh
destination: /usr/share/zsh/vendor-completions/_ipsw
mode: 0644
- source: ./completions/ipsw/_fish
destination: /usr/share/fish/vendor_completions.d/ipsw.fish
mode: 0644
- source: ./LICENSE
destination: /usr/share/doc/ipsw/copyright
mode: 0644
apps:
ipsw:
command: ipswd
daemon: simple
sockets:
sock:
listen-stream: $SNAP_COMMON/ipsw.sock
socket-group: socket-group
socket-mode: 416
plugs: ["home", "network", "network-bind"]
scoop:
bucket:
+9 -1
View File
@@ -8,7 +8,7 @@ GO_BIN=go
.PHONY: build-deps
build-deps: ## Install the build dependencies
@echo " > Installing build deps"
brew install $(GO_BIN) goreleaser zig unicorn libusb
brew install $(GO_BIN) goreleaser zig unicorn libusb go-swagger/go-swagger/go-swagger
.PHONY: dev-deps
dev-deps: ## Install the dev dependencies
@@ -71,6 +71,14 @@ build-ios: ## Build ipsw for iOS
@CGO_ENABLED=1 GOOS=ios GOARHC=arm64 CC=$(shell go env GOROOT)/misc/ios/clangwrap.sh $(GO_BIN) build -ldflags "-s -w -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppVersion=$(CUR_VERSION) -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppBuildTime==$(date -u +%Y%m%d)" ./cmd/ipsw
@codesign --entitlements hack/make/data/ent.plist -s - -f ipsw
build-linux: ## Build ipsw (linux)
@echo " > Building ipsw (linux)"
@$(GO_BIN) mod download
@CGO_ENABLED=1 GOOS=linux GOARHC=arm64 CC='zig cc -target aarch64-linux-musl' CXX='zig c++ -target aarch64-linux-musl' $(GO_BIN) build -ldflags "-s -w -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppVersion=$(CUR_VERSION) -X github.com/blacktop/ipsw/cmd/ipsw/cmd.AppBuildTime=$(date -u +%Y%m%d)" ./cmd/ipsw
@echo " > Building ipswd (linux)"
@CGO_ENABLED=0 GOOS=linux GOARHC=arm64 $(GO_BIN) build -ldflags "-s -w --X github.com/blacktop/ipsw/api/types.BuildVersion=$(CUR_VERSION) -X github.com/blacktop/ipsw/api/types.BuildTime=$(date -u +%Y%m%d)" ./cmd/ipswd
.PHONY: docs
docs: ## Build the cli docs
@echo " > Updating CLI Docs"
+9
View File
@@ -0,0 +1,9 @@
# ipsw API
## TODO
<https://github.com/gin-gonic/examples>
- [ ] https://github.com/gin-gonic/examples/tree/master/send_chunked_data
- [ ] https://github.com/gin-gonic/examples/tree/master/server-sent-event *(daemon status)*
- [ ] https://github.com/gin-gonic/examples/tree/master/grpc/example1 *(daemon speed?)*
+10
View File
@@ -0,0 +1,10 @@
// Package api contains common constants for daemon and client.
package api
//go:generate swagger generate spec -o swagger.json
// Common constants for daemon and client.
const (
// DefaultVersion of Current REST API
DefaultVersion = "1"
)
+51
View File
@@ -0,0 +1,51 @@
// Package daemon provides the daemon routes
package daemon
import (
"net/http"
"runtime"
"github.com/blacktop/ipsw/api"
"github.com/blacktop/ipsw/api/types"
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
// swagger:route HEAD /_ping Daemon headDaemonPing
//
// Ping
//
// This will return if 200 the daemon is running.
rg.HEAD("/_ping", pingHandler)
// swagger:route GET /_ping Daemon getDaemonPing
//
// Ping
//
// This will return "OK" if the daemon is running.
rg.GET("/_ping", pingHandler)
// swagger:route GET /version Daemon getDaemonVersion
//
// Version
//
// This will return the daemon version info.
rg.GET("/version", func(c *gin.Context) {
c.JSON(http.StatusOK, types.Version{
ApiVersion: api.DefaultVersion,
OSType: runtime.GOOS,
BuilderVersion: types.BuildVersion,
})
})
}
func pingHandler(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
if c.Request.Method == "HEAD" {
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Content-Length", "0")
return
}
c.String(http.StatusOK, "OK")
}
+35
View File
@@ -0,0 +1,35 @@
// Package devicelist contains the /device_list routes for the API
package devicelist
import (
"net/http"
"sort"
"github.com/blacktop/ipsw/pkg/xcode"
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
// swagger:route GET /device_list DeviceList getDeviceList
//
// List XCode Devices.
//
// This will return JSON of all XCode devices.
//
// Produces:
// - application/json
rg.GET("/device_list", func(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
devices, err := xcode.GetDevices()
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
sort.Sort(xcode.ByProductType{Devices: devices})
c.JSON(http.StatusOK, devices)
})
}
+38
View File
@@ -0,0 +1,38 @@
package download
import (
"net/http"
"github.com/blacktop/ipsw/internal/commands/download/ipsw"
"github.com/gin-gonic/gin"
)
func downloadIPSW(c *gin.Context) {
version := c.Query("version")
build := c.Query("build")
device := c.Query("device")
c.IndentedJSON(http.StatusOK, gin.H{"version": version, "build": build, "device": device})
}
func downloadLatestIPSWs(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{"count": 0})
}
func latestVersion(c *gin.Context) {
version, err := ipsw.GetLatestIosVersion("", false)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"version": version})
}
func latestBuild(c *gin.Context) {
build, err := ipsw.GetLatestIosBuild()
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"build": build})
}
+36
View File
@@ -0,0 +1,36 @@
// Package download contains the /download routes
package download
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
dl := rg.Group("/download")
// dl.GET("/dev", handler) // TODO:
// dl.GET("/git", handler) // TODO:
// dl.GET("/ipa", handler) // TODO:
// dl.GET("/ipsw", downloadIPSW) // TODO:
// dl.GET("/ipsw/ios/latest", downloadLatestIPSWs) // TODO:
// swagger:route GET /download/ipsw/ios/latest/version Download getDownloadLatestIPSWsVersion
//
// Latest iOS Version
//
// Get latest iOS version.
dl.GET("/ipsw/ios/latest/version", latestVersion)
// swagger:route GET /download/ipsw/ios/latest/build Download getDownloadLatestIPSWsBuild
//
// Latest iOS Build
//
// Get latest iOS build.
dl.GET("/ipsw/ios/latest/build", latestBuild)
// dl.GET("/macos", handler) // TODO:
// dl.GET("/ota", handler) // TODO:
// dl.GET("/rss", handler) // TODO:
// dl.GET("/tss", handler) // TODO:
// dl.GET("/wiki", handler) // TODO:
}
+121
View File
@@ -0,0 +1,121 @@
// Package dsc provides the /dsc route and handlers
package dsc
import (
"net/http"
cmd "github.com/blacktop/ipsw/internal/commands/dsc"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/gin-gonic/gin"
)
func dscImports(c *gin.Context) {
dscPath := c.Query("path")
if dscPath == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing required 'path' query parameter"})
return
}
f, err := dyld.Open(dscPath)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
imps, err := cmd.GetDylibsThatImport(f, c.Query("dylib"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": dscPath, "imported_by": imps})
}
func dscInfo(c *gin.Context) {
dscPath := c.Query("path")
if dscPath == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing required 'path' query parameter"})
return
}
f, err := dyld.Open(dscPath)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
info, err := cmd.GetInfo(f)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": dscPath, "info": info})
}
func dscMacho(c *gin.Context) {
dscPath := c.Query("path")
f, err := dyld.Open(dscPath)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
image, err := f.Image(c.Query("dylib"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
m, err := image.GetMacho()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": dscPath, "macho": m})
}
func dscStrings(c *gin.Context) {
dscPath := c.Query("path")
f, err := dyld.Open(dscPath)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
pattern := c.Query("pattern")
strs, err := cmd.GetStrings(f, pattern)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": dscPath, "strings": strs})
}
func dscSymbols(c *gin.Context) {
dscPath := c.Query("path")
f, err := dyld.Open(dscPath)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
var lookups []cmd.Symbol
if err := c.ShouldBindJSON(&lookups); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
syms, err := cmd.GetSymbols(f, lookups)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": dscPath, "symbols": syms})
}
+131
View File
@@ -0,0 +1,131 @@
package dsc
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
dr := rg.Group("/dsc")
// dr.GET("/a2f", handler) // TODO: implement this
// dr.GET("/a2o", handler) // TODO: implement this
// dr.GET("/a2s", handler) // TODO: implement this
// dr.GET("/disass", handler) // TODO: implement this
// dr.GET("/dump", handler) // TODO: implement this
// dr.GET("/extract", handler) // TODO: implement this
// dr.GET("/ida", handler) // TODO: implement this
// dr.GET("/image", handler) // TODO: implement this
// swagger:route GET /dsc/imports DSC getDscImports
//
// Imports
//
// Get list of dylibs that import a given dylib.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
// + name: dylib
// in: query
// description: dylib to search for
// required: true
// type: string
dr.GET("/imports", dscImports)
// swagger:route GET /dsc/info DSC getDscInfo
//
// Info
//
// Get info about a given DSC
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
dr.GET("/info", dscInfo)
// swagger:route GET /dsc/macho DSC getDscMacho
//
// MachO
//
// Get MachO info for a given dylib in the DSC.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
// + name: dylib
// in: query
// description: dylib to search for
// required: true
// type: string
dr.GET("/macho", dscMacho)
// dr.GET("/o2a", handler) // TODO: implement this
// dr.GET("/objc", handler) // TODO: implement this
// dr.GET("/patches", handler) // TODO: implement this
// dr.GET("/search", handler) // TODO: implement this
// dr.GET("/slide", handler) // TODO: implement this
// dr.GET("/split", handler) // TODO: implement this
// swagger:route GET /dsc/str DSC getDscStrings
//
// Strings
//
// Get strings in the DSC that match a given pattern.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
// + name: pattern
// in: query
// description: regex to search for
// required: true
// type: string
dr.GET("/str", dscStrings)
// dr.GET("/stubs", handler) // TODO: implement this
// dr.GET("/swift", handler) // TODO: implement this
// swagger:route GET /dsc/symaddr DSC getDscSymbols
//
// Symbols
//
// Get symbols addresses in the DSC that match a given lookup JSON payload.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
dr.POST("/symaddr", dscSymbols)
// dr.GET("/tbd", handler) // TODO: implement this
// dr.GET("/webkit", handler) // TODO: implement this
// dr.GET("/xref", handler) // TODO: implement this
}
+1
View File
@@ -0,0 +1 @@
package dtree
+85
View File
@@ -0,0 +1,85 @@
package extract
import (
"fmt"
"net/http"
"github.com/blacktop/ipsw/internal/commands/extract"
cmd "github.com/blacktop/ipsw/internal/commands/extract"
"github.com/blacktop/ipsw/internal/utils"
"github.com/gin-gonic/gin"
)
func extractDSC(c *gin.Context) {
var query extract.Config
if err := c.BindQuery(&query); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
artifacts, err := cmd.DSC(&query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"query": query, "artifacts": artifacts})
}
func extractDMG(c *gin.Context) {
var query cmd.Config
if err := c.BindQuery(&query); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
if !utils.StrSliceHas([]string{"app", "sys", "fs"}, query.DmgType) {
c.IndentedJSON(http.StatusBadRequest, fmt.Errorf("invalid dmg type: %s", query.DmgType))
return
}
artifacts, err := cmd.DMG(&query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"query": query, "artifacts": artifacts})
}
func extractKBAG(c *gin.Context) {
var query cmd.Config
if err := c.BindQuery(&query); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
artifacts, err := cmd.Keybags(&query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"query": query, "artifacts": artifacts})
}
func extractKernel(c *gin.Context) {
var query cmd.Config
if err := c.BindQuery(&query); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
artifacts, err := cmd.Kernelcache(&query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"query": query, "artifacts": artifacts})
}
func extractPattern(c *gin.Context) {
var query cmd.Config
if err := c.BindQuery(&query); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
artifacts, err := cmd.Search(&query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"query": query, "artifacts": artifacts})
}
+42
View File
@@ -0,0 +1,42 @@
// Package extract provides the /extract API route
package extract
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
dl := rg.Group("/extract")
// swagger:route GET /extract/dsc Extract getExtractDsc
//
// DSC
//
// Extract dyld_shared_caches from an IPSW.
dl.GET("/dsc", extractDSC)
// swagger:route GET /extract/dmg Extract getExtractDmg
//
// DMG
//
// Extract DMGs from an IPSW.
dl.GET("/dmg", extractDMG)
// swagger:route GET /extract/kbag Extract getExtractKbags
//
// KBAG
//
// Extract KBAGs from an IPSW.
dl.GET("/kbag", extractKBAG)
// swagger:route GET /extract/kernel Extract getExtractKernel
//
// Kernel
//
// Extract kernelcaches from an IPSW.
dl.GET("/kernel", extractKernel)
// swagger:route GET /extract/pattern Extract getExtractPattern
//
// Pattern
//
// Extract files from an IPSW that match a given pattern.
dl.GET("/pattern", extractPattern)
}
+53
View File
@@ -0,0 +1,53 @@
// Package idev provides the /idev route and handlers
package idev
import (
"fmt"
"net/http"
"github.com/blacktop/ipsw/pkg/usb"
"github.com/blacktop/ipsw/pkg/usb/lockdownd"
"github.com/gin-gonic/gin"
)
func idevInfo(c *gin.Context) {
conn, err := usb.NewConn()
if err != nil {
c.JSON(http.StatusInternalServerError, fmt.Errorf("failed to connect to usbmuxd: %w", err))
return
}
defer conn.Close()
devices, err := conn.ListDevices()
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
if len(devices) == 0 {
c.JSON(http.StatusOK, gin.H{"status": "no devices found", "devices": nil})
return
}
var dds []*lockdownd.DeviceValues
for _, device := range devices {
cli, err := lockdownd.NewClient(device.SerialNumber)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
values, err := cli.GetValues()
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
dds = append(dds, values)
cli.Close()
}
c.JSON(http.StatusOK, gin.H{"devices": dds})
}
+97
View File
@@ -0,0 +1,97 @@
package idev
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
ig := rg.Group("/idev")
// ig.GET("/afc", handler) // TODO: implement this
// ig.GET("/afc/cat", handler) // TODO:
// ig.GET("/afc/ls", handler) // TODO:
// ig.GET("/afc/mkdir", handler) // TODO:
// ig.GET("/afc/pull", handler) // TODO:
// ig.GET("/afc/push", handler) // TODO:
// ig.GET("/afc/rm", handler) // TODO:
// ig.GET("/afc/tree", handler) // TODO:
// ig.GET("/apps", handler) // TODO: implement this
// ig.GET("/apps/install", handler) // TODO: implement this
// ig.GET("/apps/ls", handler) // TODO: implement this
// ig.GET("/apps/uninstall", handler) // TODO: implement this
// ig.GET("/comp", handler) // TODO: implement this
// ig.GET("/crash", handler) // TODO: implement this
// ig.GET("/crash/clear", handler) // TODO: implement this
// ig.GET("/crash/ls", handler) // TODO: implement this
// ig.GET("/crash/pull", handler) // TODO: implement this
// ig.GET("/diag", handler) // TODO: implement this
// ig.GET("/diag/bat", handler) // TODO:
// ig.GET("/diag/info", handler) // TODO:
// ig.GET("/diag/ioreg", handler) // TODO:
// ig.GET("/diag/mg", handler) // TODO:
// ig.GET("/diag/restart", handler) // TODO:
// ig.GET("/diag/shutdown", handler) // TODO:
// ig.GET("/diag/sleep", handler) // TODO:
// ig.GET("/fsyms", handler) // TODO: implement this
// ig.GET("/img", handler) // TODO: implement this
// ig.GET("/img/lookup", handler) // TODO: implement this
// ig.GET("/img/ls", handler) // TODO: implement this
// ig.GET("/img/mount", handler) // TODO: implement this
// ig.GET("/img/unmount", handler) // TODO: implement this
// swagger:route GET /idev/info USB getIdevInfo
//
// Info
//
// Get info about USB connected devices.
ig.GET("/info", idevInfo) // `ipsw idev list`
// ig.GET("/loc", handler) // TODO: implement this
// ig.GET("/loc/clear", handler) // TODO: implement this
// ig.GET("/loc/play", handler) // TODO: implement this
// ig.GET("/loc/set", handler) // TODO: implement this
// ig.GET("/noti", handler) // TODO: implement this
// ig.GET("/pcap", handler) // TODO: implement this
// ig.GET("/prof", handler) // TODO: implement this
// ig.GET("/prof/cloud", handler) // TODO: implement this
// ig.GET("/prof/install", handler) // TODO: implement this
// ig.GET("/prof/ls", handler) // TODO: implement this
// ig.GET("/prof/rm", handler) // TODO: implement this
// ig.GET("/prof/wifi", handler) // TODO: implement this
// ig.GET("/prov", handler) // TODO: implement this
// ig.GET("/prov/clear", handler) // TODO: implement this
// ig.GET("/prov/dump", handler) // TODO: implement this
// ig.GET("/prov/install", handler) // TODO: implement this
// ig.GET("/prov/ls", handler) // TODO: implement this
// ig.GET("/prov/rm", handler) // TODO: implement this
// ig.GET("/proxy", handler) // TODO: implement this
// ig.GET("/ps", handler) // TODO: implement this
// ig.GET("/restore", handler) // TODO: implement this
// ig.GET("/restore/enter", handler) // TODO: implement this
// ig.GET("/restore/exit", handler) // TODO: implement this
// ig.GET("/screen", handler) // TODO: implement this
// ig.GET("/springb", handler) // TODO: implement this
// ig.GET("/springb/icon", handler) // TODO: implement this
// ig.GET("/springb/orient", handler) // TODO: implement this
// ig.GET("/springb/wallpaper", handler) // TODO: implement this
// ig.GET("/syslog", handler) // TODO: implement this
// ig.GET("/wifi", handler) // TODO: implement this
}
+1
View File
@@ -0,0 +1 @@
package img4
+39
View File
@@ -0,0 +1,39 @@
// Package info provides a route for getting info about an IPSW or OTA file
package info
import (
"net/http"
"strconv"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/pkg/info"
"github.com/gin-gonic/gin"
)
func getInfo(c *gin.Context) {
path := c.Query("path")
i, err := info.Parse(path)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": path, "info": i})
}
func getRemoteInfo(c *gin.Context) {
insecure, _ := strconv.ParseBool(c.Query("insecure"))
zr, err := download.NewRemoteZipReader(c.Query("url"), &download.RemoteConfig{
Proxy: c.Query("proxy"),
Insecure: insecure,
})
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
i, err := info.ParseZipFiles(zr.File)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"url": c.Query("url"), "info": i})
}
+90
View File
@@ -0,0 +1,90 @@
package info
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
ig := rg.Group("/info")
// swagger:route GET /info/ipsw Info getIpswInfo
//
// IPSW
//
// Get IPSW info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
ig.GET("/ipsw", getInfo)
// swagger:route GET /info/ota Info getOtaInfo
//
// OTA
//
// Get OTA info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to OTA
// required: true
// type: string
ig.GET("/ota", getInfo)
// swagger:route GET /info/ipsw/remote Info getRemoteIpswInfo
//
// Remote IPSW
//
// Get remote IPSW info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: url
// in: query
// description: url to IPSW
// required: true
// type: string
// + name: proxy
// in: query
// description: http proxy to use
// type: string
// + name: insecure
// in: query
// description: ignore TLS errors
// type: boolean
ig.GET("/ipsw/remote", getRemoteInfo)
// swagger:route GET /info/ota/remote Info getRemoteOtaInfo
//
// Remote OTA
//
// Get remote OTA info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: url
// in: query
// description: url to OTA
// required: true
// type: string
// + name: proxy
// in: query
// description: http proxy to use
// type: string
// + name: insecure
// in: query
// description: ignore TLS errors
// type: boolean
ig.GET("/ota/remote", getRemoteInfo)
}
+130
View File
@@ -0,0 +1,130 @@
package ipsw
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/apex/log"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/internal/commands/ent"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/info"
"github.com/gin-gonic/gin"
)
type File struct {
Name string
Size int64
Mode string
ModTime time.Time
}
func getFsFiles(c *gin.Context) {
ipswPath := c.Query("path")
ipswPath = filepath.Clean(ipswPath)
i, err := info.Parse(ipswPath)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
dmgPath, err := i.GetFileSystemOsDmg()
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
// extract filesystem DMG
dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
c.JSON(http.StatusInternalServerError, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err))
}
if len(dmgs) == 0 {
c.JSON(http.StatusInternalServerError, fmt.Errorf("failed to find %s in IPSW", dmgPath))
}
defer os.Remove(filepath.Clean(dmgs[0]))
} else {
utils.Indent(log.Debug, 2)(fmt.Sprintf("Found extracted %s", dmgPath))
}
// mount filesystem DMG
utils.Indent(log.Info, 2)(fmt.Sprintf("Mounting %s", dmgPath))
mountPoint, alreadyMounted, err := utils.MountFS(dmgPath)
if err != nil {
if !errors.Is(err, utils.ErrMountResourceBusy) {
c.JSON(http.StatusInternalServerError, fmt.Errorf("failed to mount DMG: %v", err))
}
}
if alreadyMounted {
utils.Indent(log.Info, 3)(fmt.Sprintf("%s already mounted", dmgPath))
} else {
defer func() {
utils.Indent(log.Info, 2)(fmt.Sprintf("Unmounting %s", dmgPath))
if err := utils.Retry(3, 2*time.Second, func() error {
return utils.Unmount(mountPoint, false)
}); err != nil {
log.Errorf("failed to unmount %s at %s: %v", dmgPath, mountPoint, err)
}
}()
}
var files []File
if err := filepath.Walk(mountPoint, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v", path, err)
}
if info.IsDir() {
return nil
// return filepath.SkipDir
}
fpath, err := filepath.Rel(mountPoint, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %v", path, err)
}
files = append(files, File{
Name: fpath,
Size: info.Size(),
Mode: info.Mode().String(),
ModTime: info.ModTime(),
})
return nil
}); err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, gin.H{"path": ipswPath, "files": files})
}
func getFsEntitlements(c *gin.Context) {
ipswPath := c.Query("path")
ipswPath = filepath.Clean(ipswPath)
ents, err := ent.GetDatabase(ipswPath, "")
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
entDB := make(map[string]map[string]any)
for f, ent := range ents {
ents := make(map[string]any)
if err := plist.NewDecoder(bytes.NewReader([]byte(ent))).Decode(&ents); err != nil {
c.JSON(http.StatusInternalServerError, fmt.Errorf("failed to decode entitlements plist for %s: %v", f, err))
}
entDB[f] = ents
}
c.IndentedJSON(http.StatusOK, gin.H{"path": ipswPath, "entitlements": entDB})
}
+42
View File
@@ -0,0 +1,42 @@
package ipsw
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
dl := rg.Group("/ipsw")
// swagger:route GET /ipsw/fs/files IPSW getIpswFsFiles
//
// Files
//
// Get IPSW Filesystem DMG file listing.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
dl.GET("/fs/files", getFsFiles)
// swagger:route GET /ipsw/fs/ents IPSW getIpswFsEntitlements
//
// Entitlements
//
// Get IPSW Filesystem DMG MachO entitlements.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
dl.GET("/fs/ents", getFsEntitlements)
}
+60
View File
@@ -0,0 +1,60 @@
package kernel
import (
"net/http"
"github.com/blacktop/go-macho"
"github.com/blacktop/ipsw/pkg/kernelcache"
"github.com/gin-gonic/gin"
)
func listKexts(c *gin.Context) {
kernelPath := c.Query("path")
m, err := macho.Open(kernelPath)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
defer m.Close()
bundles, err := kernelcache.GetKexts(m)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{"path": kernelPath, "kexts": bundles})
}
func getSyscalls(c *gin.Context) {
kernelPath := c.Query("path")
m, err := macho.Open(kernelPath)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
defer m.Close()
syscalls, err := kernelcache.GetSyscallTable(m)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{"path": kernelPath, "syscalls": syscalls})
}
func getVersion(c *gin.Context) {
kernelPath := c.Query("path")
m, err := macho.Open(kernelPath)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
defer m.Close()
v, err := kernelcache.GetVersion(m)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{"path": kernelPath, "version": v})
}
+67
View File
@@ -0,0 +1,67 @@
// Package kernel provides the /kernel routes
package kernel
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
kg := rg.Group("/kernel")
// kg.GET("/ctfdump", handler) // TODO: implement this
// kg.GET("/dec", handler) // TODO: implement this
// kg.GET("/dwarf", handler) // TODO: implement this
// kg.GET("/extract", handler) // TODO: implement this
// swagger:route GET /kernel/kexts Kernel getKernelKexts
//
// Kexts
//
// Get kernelcache KEXTs info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to kernelcache
// required: true
// type: string
kg.GET("/kexts", listKexts)
// kg.GET("/sbopts", handler) // TODO: implement this
// kg.GET("/symbolsets", handler) // TODO: implement this
// swagger:route GET /kernel/syscall Kernel getKernelSyscalls
//
// Syscalls
//
// Get kernelcache syscalls info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to kernelcache
// required: true
// type: string
kg.GET("/syscall", getSyscalls)
// swagger:route GET /kernel/version Kernel getKernelVersion
//
// Version
//
// Get kernelcache version.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to kernelcache
// required: true
// type: string
kg.GET("/version", getVersion)
}
+50
View File
@@ -0,0 +1,50 @@
package macho
import (
"net/http"
"strings"
"github.com/blacktop/go-macho"
"github.com/gin-gonic/gin"
)
// Info is the struct for the macho info route parameters
type Info struct {
Path string `form:"path" json:"path" binding:"required"`
Arch string `form:"arch" json:"arch"`
}
func machoInfo(c *gin.Context) {
var m *macho.File
var params Info
if err := c.BindQuery(&params); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fat, err := macho.OpenFat(params.Path)
if err != nil {
if err == macho.ErrNotFat { // not a fat binary
m, err = macho.Open(params.Path)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
} else {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
} else { // fat binary
if params.Arch == "" {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "'arch' query parameter is required for universal binaries"})
return
}
for _, farch := range fat.Arches {
if strings.EqualFold(farch.SubCPU.String(farch.CPU), params.Arch) {
m = farch.File
}
}
}
c.IndentedJSON(http.StatusOK, gin.H{"info": m})
}
+42
View File
@@ -0,0 +1,42 @@
package macho
import (
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
m := rg.Group("/macho")
// m.GET("/a2o", handler) // TODO: implement this
// m.GET("/a2s", handler) // TODO: implement this
// m.GET("/bbl", handler) // TODO: implement this
// m.GET("/disass", handler) // TODO: implement this
// m.GET("/dump", handler) // TODO: implement this
// swagger:route GET /macho/info MachO getMachoInfo
//
// Info
//
// Get MachO info.
//
// Produces:
// - application/json
//
// Parameters:
// + name: path
// in: query
// description: path to MachO
// required: true
// type: string
// + name: arch
// in: query
// description: architecture to get info for in universal MachO
// required: false
// type: string
m.GET("/info", machoInfo)
// m.GET("/lipo", handler) // TODO: implement this
// m.GET("/o2a", handler) // TODO: implement this
// m.GET("/patch", handler) // TODO: implement this
// m.GET("/search", handler) // TODO: implement this
// m.GET("/sign", handler) // TODO: implement this
}
+1
View File
@@ -0,0 +1 @@
package mdevs
+74
View File
@@ -0,0 +1,74 @@
// Package mount provides the /mount API route
package mount
import (
"net/http"
"path/filepath"
"github.com/blacktop/ipsw/internal/commands/mount"
"github.com/gin-gonic/gin"
)
// AddRoutes adds the download routes to the router
func AddRoutes(rg *gin.RouterGroup) {
// swagger:route POST /mount/{type} Mount postMount
//
// Mount
//
// Mount a DMG inside a given IPSW.
//
// Produces:
// - application/json
//
// Parameters:
// + name: type
// in: path
// description: type of DMG to mount
// pattern: (app|sys|fs)
// required: true
// type: string
// + name: path
// in: query
// description: path to IPSW
// required: true
// type: string
rg.POST("/mount/:type", func(c *gin.Context) {
ipswPath := filepath.Clean(c.Query("path"))
ctx, err := mount.DmgInIPSW(ipswPath, c.Param("type"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
c.JSON(http.StatusOK, gin.H{"dmg_path": ctx.DmgPath, "mount_point": ctx.MountPoint, "already_mounted": ctx.AlreadyMounted})
})
// swagger:route POST /unmount Mount postUnmount
//
// Unmount
//
// Unmount a previously mounted DMG.
//
// Produces:
// - application/json
//
// Parameters:
// + name: mount_point
// in: path
// description: mount point of DMG
// required: true
// type: string
// + name: dmg_path
// in: query
// description: path to DMG
// type: string
rg.POST("/unmount", func(c *gin.Context) {
ctx := mount.Context{}
if err := c.ShouldBindJSON(&ctx); err != nil {
c.IndentedJSON(http.StatusBadRequest, err)
return
}
if err := ctx.Unmount(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
})
}
+1
View File
@@ -0,0 +1 @@
package ota
+1
View File
@@ -0,0 +1 @@
package pongo
+39
View File
@@ -0,0 +1,39 @@
// Package routes contains all the routes for the API
package routes
import (
"github.com/blacktop/ipsw/api/server/routes/daemon"
"github.com/blacktop/ipsw/api/server/routes/devicelist"
"github.com/blacktop/ipsw/api/server/routes/download"
"github.com/blacktop/ipsw/api/server/routes/dsc"
"github.com/blacktop/ipsw/api/server/routes/extract"
"github.com/blacktop/ipsw/api/server/routes/idev"
"github.com/blacktop/ipsw/api/server/routes/info"
"github.com/blacktop/ipsw/api/server/routes/ipsw"
"github.com/blacktop/ipsw/api/server/routes/kernel"
"github.com/blacktop/ipsw/api/server/routes/macho"
"github.com/blacktop/ipsw/api/server/routes/mount"
"github.com/gin-gonic/gin"
)
// Add adds the command routes to the router
func Add(rg *gin.RouterGroup) {
daemon.AddRoutes(rg)
devicelist.AddRoutes(rg)
download.AddRoutes(rg)
// dtree.AddRoutes(rg) // TODO: add dtree routes
dsc.AddRoutes(rg)
extract.AddRoutes(rg)
idev.AddRoutes(rg)
// img4.AddRoutes(rg) // TODO: add img4 routes
info.AddRoutes(rg)
ipsw.AddRoutes(rg)
kernel.AddRoutes(rg)
macho.AddRoutes(rg)
// mdevs.AddRoutes(rg) // TODO: add mdevs routes
mount.AddRoutes(rg)
// ota.AddRoutes(rg) // TODO: add ota routes
// pongo.AddRoutes(rg) // TODO: add pongo routes
// sepfw.AddRoutes(rg) // TODO: add sepfw routes
// symbolicate.AddRoutes(rg) // TODO: add symbolicate routes
}
+1
View File
@@ -0,0 +1 @@
package sepfw
@@ -0,0 +1 @@
package symbolicate
+121
View File
@@ -0,0 +1,121 @@
// Package server The ipsw API.
//
// Schemes: http
// Host: localhost:8080
// BasePath: /v1
// Version: v1.0
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// swagger:meta
package server
import (
"context"
"fmt"
"net"
"net/http"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/apex/log"
"github.com/blacktop/ipsw/api"
"github.com/blacktop/ipsw/api/server/routes"
"github.com/blacktop/ipsw/api/types"
"github.com/gin-gonic/gin"
)
// Config is the server config
type Config struct {
Host string
Port int
Socket string
Debug bool
}
// Server is the main server struct
type Server struct {
router *gin.Engine
server *http.Server
conf *Config
}
// NewServer creates a new server
func NewServer(conf *Config) *Server {
return &Server{
router: gin.Default(),
conf: conf,
}
}
// Start starts the server
func (s *Server) Start() error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
s.router.GET("/version", func(c *gin.Context) {
c.JSON(http.StatusOK, types.Version{
ApiVersion: api.DefaultVersion,
OSType: runtime.GOOS,
BuilderVersion: types.BuildVersion,
})
})
rg := s.router.Group("/v" + api.DefaultVersion)
routes.Add(rg)
s.server = &http.Server{
Addr: fmt.Sprintf(":%d", s.conf.Port),
Handler: s.router,
}
go func() {
if len(s.conf.Socket) > 0 {
l, err := net.Listen("unix", filepath.Clean(s.conf.Socket))
if err != nil {
log.Fatalf("server: failed to listen: %v\n", err)
}
if err := s.server.Serve(l); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: failed to serve: %v\n", err)
}
} else {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: failed to listen and serve: %v\n", err)
}
}
}()
// Listen for the interrupt signal.
<-ctx.Done()
// Restore default behavior on the interrupt signal and notify user of shutdown.
stop()
log.Warn("Shutting down gracefully: Press Ctrl+C again to force")
return s.Stop()
}
// Stop stops the server
func (s *Server) Stop() error {
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.server.Shutdown(ctx); err != nil {
return fmt.Errorf("server forced to shutdown: %v", err)
}
log.Info("Server Exiting")
return nil
}
+582
View File
@@ -0,0 +1,582 @@
{
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"title": "The ipsw API.",
"version": "v1.0"
},
"host": "localhost:8080",
"basePath": "/v1",
"paths": {
"/_ping": {
"get": {
"description": "This will return \"OK\" if the daemon is running.",
"tags": [
"Daemon"
],
"summary": "Ping",
"operationId": "getDaemonPing"
},
"head": {
"description": "This will return if 200 the daemon is running.",
"tags": [
"Daemon"
],
"summary": "Ping",
"operationId": "headDaemonPing"
}
},
"/device_list": {
"get": {
"description": "This will return JSON of all XCode devices.",
"produces": [
"application/json"
],
"tags": [
"DeviceList"
],
"summary": "List XCode Devices.",
"operationId": "getDeviceList"
}
},
"/download/ipsw/ios/latest/build": {
"get": {
"description": "Get latest iOS build.",
"tags": [
"Download"
],
"summary": "Latest iOS Build",
"operationId": "getDownloadLatestIPSWsBuild"
}
},
"/download/ipsw/ios/latest/version": {
"get": {
"description": "Get latest iOS version.",
"tags": [
"Download"
],
"summary": "Latest iOS Version",
"operationId": "getDownloadLatestIPSWsVersion"
}
},
"/dsc/imports": {
"get": {
"description": "Get list of dylibs that import a given dylib.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Imports",
"operationId": "getDscImports",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "dylib to search for",
"name": "dylib",
"in": "query",
"required": true
}
]
}
},
"/dsc/info": {
"get": {
"description": "Get info about a given DSC",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Info",
"operationId": "getDscInfo",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/dsc/macho": {
"get": {
"description": "Get MachO info for a given dylib in the DSC.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "MachO",
"operationId": "getDscMacho",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "dylib to search for",
"name": "dylib",
"in": "query",
"required": true
}
]
}
},
"/dsc/str": {
"get": {
"description": "Get strings in the DSC that match a given pattern.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Strings",
"operationId": "getDscStrings",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "regex to search for",
"name": "pattern",
"in": "query",
"required": true
}
]
}
},
"/dsc/symaddr": {
"get": {
"description": "Get symbols addresses in the DSC that match a given lookup JSON payload.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Symbols",
"operationId": "getDscSymbols",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/extract/dmg": {
"get": {
"description": "Extract DMGs from an IPSW.",
"tags": [
"Extract"
],
"summary": "DMG",
"operationId": "getExtractDmg"
}
},
"/extract/dsc": {
"get": {
"description": "Extract dyld_shared_caches from an IPSW.",
"tags": [
"Extract"
],
"summary": "DSC",
"operationId": "getExtractDsc"
}
},
"/extract/kbag": {
"get": {
"description": "Extract KBAGs from an IPSW.",
"tags": [
"Extract"
],
"summary": "KBAG",
"operationId": "getExtractKbags"
}
},
"/extract/kernel": {
"get": {
"description": "Extract kernelcaches from an IPSW.",
"tags": [
"Extract"
],
"summary": "Kernel",
"operationId": "getExtractKernel"
}
},
"/extract/pattern": {
"get": {
"description": "Extract files from an IPSW that match a given pattern.",
"tags": [
"Extract"
],
"summary": "Pattern",
"operationId": "getExtractPattern"
}
},
"/idev/info": {
"get": {
"description": "Get info about USB connected devices.",
"tags": [
"USB"
],
"summary": "Info",
"operationId": "getIdevInfo"
}
},
"/info/ipsw": {
"get": {
"description": "Get IPSW info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "IPSW",
"operationId": "getIpswInfo",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/info/ipsw/remote": {
"get": {
"description": "Get remote IPSW info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "Remote IPSW",
"operationId": "getRemoteIpswInfo",
"parameters": [
{
"type": "string",
"description": "url to IPSW",
"name": "url",
"in": "query",
"required": true
},
{
"type": "string",
"description": "http proxy to use",
"name": "proxy",
"in": "query"
},
{
"type": "boolean",
"description": "ignore TLS errors",
"name": "insecure",
"in": "query"
}
]
}
},
"/info/ota": {
"get": {
"description": "Get OTA info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "OTA",
"operationId": "getOtaInfo",
"parameters": [
{
"type": "string",
"description": "path to OTA",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/info/ota/remote": {
"get": {
"description": "Get remote OTA info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "Remote OTA",
"operationId": "getRemoteOtaInfo",
"parameters": [
{
"type": "string",
"description": "url to OTA",
"name": "url",
"in": "query",
"required": true
},
{
"type": "string",
"description": "http proxy to use",
"name": "proxy",
"in": "query"
},
{
"type": "boolean",
"description": "ignore TLS errors",
"name": "insecure",
"in": "query"
}
]
}
},
"/ipsw/fs/ents": {
"get": {
"description": "Get IPSW Filesystem DMG MachO entitlements.",
"produces": [
"application/json"
],
"tags": [
"IPSW"
],
"summary": "Entitlements",
"operationId": "getIpswFsEntitlements",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/ipsw/fs/files": {
"get": {
"description": "Get IPSW Filesystem DMG file listing.",
"produces": [
"application/json"
],
"tags": [
"IPSW"
],
"summary": "Files",
"operationId": "getIpswFsFiles",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/kexts": {
"get": {
"description": "Get kernelcache KEXTs info.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Kexts",
"operationId": "getKernelKexts",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/syscall": {
"get": {
"description": "Get kernelcache syscalls info.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Syscalls",
"operationId": "getKernelSyscalls",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/version": {
"get": {
"description": "Get kernelcache version.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Version",
"operationId": "getKernelVersion",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/macho/info": {
"get": {
"description": "Get MachO info.",
"produces": [
"application/json"
],
"tags": [
"MachO"
],
"summary": "Info",
"operationId": "getMachoInfo",
"parameters": [
{
"type": "string",
"description": "path to MachO",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "architecture to get info for in universal MachO",
"name": "arch",
"in": "query"
}
]
}
},
"/mount/{type}": {
"post": {
"description": "Mount a DMG inside a given IPSW.",
"produces": [
"application/json"
],
"tags": [
"Mount"
],
"summary": "Mount",
"operationId": "postMount",
"parameters": [
{
"type": "string",
"description": "type of DMG to mount",
"name": "type",
"in": "path",
"required": true
},
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/unmount": {
"post": {
"description": "Unmount a previously mounted DMG.",
"produces": [
"application/json"
],
"tags": [
"Mount"
],
"summary": "Unmount",
"operationId": "postUnmount",
"parameters": [
{
"type": "string",
"description": "mount point of DMG",
"name": "mount_point",
"in": "path",
"required": true
},
{
"type": "string",
"description": "path to DMG",
"name": "dmg_path",
"in": "query"
}
]
}
},
"/version": {
"get": {
"description": "This will return the daemon version info.",
"tags": [
"Daemon"
],
"summary": "Version",
"operationId": "getDaemonVersion"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
package types
var (
BuildVersion string
BuildTime string
)
// Version is the version struct
type Version struct {
ApiVersion string
OSType string
BuilderVersion string
}
+35 -44
View File
@@ -25,16 +25,14 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/commands/extract"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/kernelcache"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -161,11 +159,6 @@ var ipswCmd = &cobra.Command{
dFlg.Build = dev.BuildVersion
}
var destPath string
if len(output) > 0 {
destPath = filepath.Clean(output)
}
if len(device) > 0 {
db, err := info.GetIpswDB()
if err != nil {
@@ -332,70 +325,68 @@ var ipswCmd = &cobra.Command{
"signed": ipsw.Signed,
}).Info("Parsing remote IPSW")
zr, err := download.NewRemoteZipReader(ipsw.URL, &download.RemoteConfig{
config := &extract.Config{
IPSW: "",
URL: ipsw.URL,
Pattern: remotePattern,
Arches: dyldArches,
Proxy: proxy,
Insecure: insecure,
})
if err != nil {
return fmt.Errorf("failed to create remote zip reader of IPSW: %v", err)
DMGs: false,
DmgType: "",
Flatten: flat,
Progress: true,
Output: output,
}
inf, err := info.ParseZipFiles(zr.File)
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to parse remote IPSW metadata: %v", err)
return fmt.Errorf("failed to get current working directory: %v", err)
}
folder, err := inf.GetFolder()
if err != nil {
log.Errorf("failed to get folder from remote zip metadata: %v", err)
}
destPath = filepath.Join(destPath, folder)
// REMOTE KERNEL MODE
if remoteKernel {
log.Info("Extracting remote kernelcache")
if err := kernelcache.RemoteParse(zr, destPath); err != nil {
return fmt.Errorf("failed to download kernelcache from remote IPSW: %v", err)
artifacts, err := extract.Kernelcache(config)
if err != nil {
return fmt.Errorf("failed to extract kernelcache from remote IPSW: %v", err)
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
// REMOTE DSC MODE
if remoteDSC {
log.Info("Extracting remote dyld_shared_cache(s)")
sysDMG, err := inf.GetSystemOsDmg()
artifacts, err := extract.DSC(config)
if err != nil {
return fmt.Errorf("only iOS16.x/macOS13.x supported: failed to get SystemOS DMG from remote zip metadata: %v", err)
return err
}
if len(sysDMG) == 0 {
return fmt.Errorf("only iOS16.x/macOS13.x supported: no SystemOS DMG found in remote zip metadata")
}
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_remote_dyld")
if err != nil {
return fmt.Errorf("failed to create temporary directory to store SystemOS DMG: %v", err)
}
defer os.RemoveAll(tmpDIR)
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(fmt.Sprintf("^%s$", sysDMG)), tmpDIR, true, true); err != nil {
return fmt.Errorf("failed to extract SystemOS DMG from remote IPSW: %v", err)
}
if err := dyld.ExtractFromDMG(inf, filepath.Join(tmpDIR, sysDMG), destPath, viper.GetStringSlice("extract.dyld-arch")); err != nil {
return fmt.Errorf("failed to extract dyld_shared_cache(s) from remote IPSW: %v", err)
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
// PATTERN MATCHING MODE
if len(remotePattern) > 0 {
dlRE, err := regexp.Compile(remotePattern)
if err != nil {
return fmt.Errorf("failed to compile regex for pattern '%s': %v", remotePattern, err)
}
log.Infof("Downloading files matching pattern %#v", remotePattern)
if err := utils.RemoteUnzip(zr.File, dlRE, destPath, flat, true); err != nil {
return fmt.Errorf("failed to download pattern matching files from remote IPSW: %v", err)
artifacts, err := extract.Search(config)
if err != nil {
return err
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
}
} else { // NORMAL MODE
for _, i := range ipsws {
destName := getDestName(i.URL, removeCommas)
if len(output) > 0 {
destName = filepath.Join(filepath.Clean(output), destName)
}
if err := os.MkdirAll(filepath.Dir(destName), 0755); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
if _, err := os.Stat(destName); os.IsNotExist(err) {
log.WithFields(log.Fields{
"device": i.Identifier,
+2 -2
View File
@@ -359,7 +359,7 @@ var otaDLCmd = &cobra.Command{
if remoteKernel { // REMOTE KERNEL MODE
log.Info("Extracting remote kernelcache")
err = kernelcache.RemoteParse(zr, destPath)
_, err = kernelcache.RemoteParse(zr, destPath)
if err != nil {
return fmt.Errorf("failed to download kernelcache from remote ota: %v", err)
}
@@ -413,7 +413,7 @@ var otaDLCmd = &cobra.Command{
return fmt.Errorf("failed to compile regex for pattern '%s': %v", remotePattern, err)
}
log.Infof("Downloading files matching pattern %#v", remotePattern)
if err := utils.RemoteUnzip(zr.File, re, destPath, flat, true); err != nil {
if _, err := utils.SearchZip(zr.File, re, destPath, flat, true); err != nil {
utils.Indent(log.Warn, 2)("0 files matched pattern in remote OTA zip. Now checking payloadv2 payloads...")
rfiles, err := ota.RemoteList(zr)
if err != nil {
+20 -21
View File
@@ -26,15 +26,14 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/commands/extract"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/kernelcache"
"github.com/blacktop/ipsw/pkg/plist"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -307,41 +306,41 @@ var wikiCmd = &cobra.Command{
d, v, b := download.ParseIpswURLString(url)
log.WithFields(log.Fields{"devices": d, "build": b, "version": v}).Info("Parsing remote IPSW")
zr, err := download.NewRemoteZipReader(url, &download.RemoteConfig{
config := &extract.Config{
URL: url,
Pattern: pattern,
Proxy: proxy,
Insecure: insecure,
})
if err != nil {
return fmt.Errorf("failed to create remote zip reader of IPSW: %v", err)
Flatten: flat,
Progress: true,
Output: output,
}
inf, err := info.ParseZipFiles(zr.File)
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to parse remote IPSW metadata: %v", err)
return fmt.Errorf("failed to get current working directory: %v", err)
}
folder, err := inf.GetFolder()
if err != nil {
log.Errorf("failed to get folder from remote zip metadata: %v", err)
}
destPath = filepath.Join(destPath, folder)
// REMOTE KERNEL MODE
if kernel {
log.Info("Extracting remote kernelcache")
if err := kernelcache.RemoteParse(zr, destPath); err != nil {
return fmt.Errorf("failed to download kernelcache from remote IPSW: %v", err)
artifacts, err := extract.Kernelcache(config)
if err != nil {
return fmt.Errorf("failed to extract kernelcache from remote IPSW: %v", err)
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
// PATTERN MATCHING MODE
if len(pattern) > 0 {
dlRE, err := regexp.Compile(pattern)
log.Infof("Downloading files matching pattern %#v", pattern)
artifacts, err := extract.Search(config)
if err != nil {
return fmt.Errorf("failed to compile regex for pattern '%s': %v", pattern, err)
return err
}
log.Infof("Downloading files that contain: %s", pattern)
if err := utils.RemoteUnzip(zr.File, dlRE, destPath, flat, true); err != nil {
return fmt.Errorf("failed to download pattern matching files from remote IPSW: %v", err)
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
}
+12 -39
View File
@@ -29,6 +29,7 @@ import (
"github.com/apex/log"
"github.com/blacktop/go-macho"
dscCmd "github.com/blacktop/ipsw/internal/commands/dsc"
"github.com/blacktop/ipsw/internal/search"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/pkg/errors"
@@ -127,52 +128,24 @@ var dyldImportsCmd = &cobra.Command{
fmt.Print(title)
fmt.Println(strings.Repeat("=", len(title)-2))
if f.SupportsDylibPrebuiltLoader() {
importedBy, err := dscCmd.GetDylibsThatImport(f, image.Name)
if err != nil {
return fmt.Errorf("failed to get dylibs that import %s: %v", image.Name, err)
}
if len(importedBy.DSC) > 0 {
fmt.Println("\nIn DSC (Dylibs)")
fmt.Println("---------------")
for _, img := range f.Images {
pbl, err := f.GetDylibPrebuiltLoader(img.Name)
if err != nil {
return err
}
for _, dep := range pbl.Dependents {
if strings.EqualFold(dep.Name, image.Name) {
fmt.Println(img.Name)
}
}
}
} else {
for _, img := range f.Images {
m, err := img.GetPartialMacho()
if err != nil {
return err
}
for _, imp := range m.ImportedLibraries() {
if strings.EqualFold(imp, image.Name) {
fmt.Println(img.Name)
}
}
m.Close()
for _, img := range importedBy.DSC {
fmt.Println(img)
}
}
if f.SupportsPrebuiltLoaderSet() {
if len(importedBy.Apps) > 0 {
fmt.Println("\nIn FileSystem DMG (Apps)")
fmt.Println("------------------------")
if err := f.ForEachLaunchLoaderSet(func(execPath string, pset *dyld.PrebuiltLoaderSet) {
for _, loader := range pset.Loaders {
for _, dep := range loader.Dependents {
if strings.EqualFold(dep.Name, image.Name) {
if execPath != loader.Path {
fmt.Printf("%s (%s)\n", execPath, loader.Path)
} else {
fmt.Println(execPath)
}
}
}
}
}); err != nil {
return err
for _, img := range importedBy.Apps {
fmt.Println(img)
}
}
}
+4 -64
View File
@@ -35,7 +35,7 @@ import (
"github.com/alecthomas/chroma/quick"
"github.com/apex/log"
"github.com/blacktop/go-macho"
"github.com/blacktop/go-macho/pkg/codesign"
dscCmd "github.com/blacktop/ipsw/internal/commands/dsc"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/fullsailor/pkcs7"
@@ -64,27 +64,6 @@ func init() {
dyldInfoCmd.MarkZshCompPositionalArgumentFile(1, "dyld_shared_cache*")
}
type dylib struct {
Index int `json:"index,omitempty"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
UUID string `json:"uuid,omitempty"`
LoadAddress uint64 `json:"load_address,omitempty"`
}
type dyldInfo struct {
Magic string `json:"magic,omitempty"`
UUID string `json:"uuid,omitempty"`
Platform string `json:"platform,omitempty"`
MaxSlide int `json:"max_slide,omitempty"`
SubCacheArrayCount int `json:"num_sub_caches,omitempty"`
SubCacheGroupID int `json:"sub_cache_group_id,omitempty"`
SymSubCacheUUID string `json:"sym_sub_cache_uuid,omitempty"`
Mappings map[string][]dyld.CacheMappingWithSlideInfo `json:"mappings,omitempty"`
CodeSignature map[string]codesign.CodeSignature `json:"code_signature,omitempty"`
Dylibs []dylib `json:"dylibs,omitempty"`
}
// dyldInfoCmd represents the info command
var dyldInfoCmd = &cobra.Command{
Use: "info <dyld_shared_cache>",
@@ -145,54 +124,15 @@ var dyldInfoCmd = &cobra.Command{
defer f.Close()
if outAsJSON {
dinfo := dyldInfo{
Magic: f.Headers[f.UUID].Magic.String(),
UUID: f.UUID.String(),
Platform: f.Headers[f.UUID].Platform.String(),
MaxSlide: int(f.Headers[f.UUID].MaxSlide),
dinfo, err := dscCmd.GetInfo(f)
if err != nil {
return fmt.Errorf("failed to get DSC info: %s", err)
}
dinfo.Mappings = make(map[string][]dyld.CacheMappingWithSlideInfo)
for u, mp := range f.MappingsWithSlideInfo {
for _, m := range mp {
dinfo.Mappings[u.String()] = append(dinfo.Mappings[u.String()], *m)
}
}
dinfo.CodeSignature = make(map[string]codesign.CodeSignature)
if showSignature {
for u, cs := range f.CodeSignatures {
dinfo.CodeSignature[u.String()] = *cs
}
}
if showDylibs {
for idx, img := range f.Images {
m, err := img.GetPartialMacho()
if err != nil {
continue
// return fmt.Errorf("failed to create partial MachO for image %s: %v", img.Name, err)
}
dinfo.Dylibs = append(dinfo.Dylibs, dylib{
Index: idx + 1,
Name: img.Name,
Version: m.SourceVersion().Version.String(),
UUID: m.UUID().String(),
LoadAddress: img.Info.Address,
})
m.Close()
}
}
j, err := json.Marshal(dinfo)
if err != nil {
return err
}
fmt.Println(string(j))
return nil
}
+13 -120
View File
@@ -22,16 +22,13 @@ THE SOFTWARE.
package dyld
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"text/tabwriter"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/utils"
dscCmd "github.com/blacktop/ipsw/internal/commands/dsc"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -40,11 +37,7 @@ import (
func init() {
DyldCmd.AddCommand(StrSearchCmd)
StrSearchCmd.Flags().BoolP("insensitive", "i", false, "Case-insensitive search")
StrSearchCmd.Flags().BoolP("contains", "c", false, "Match strings that contain the search substring")
StrSearchCmd.Flags().StringP("pattern", "p", "", "Regex match strings (FAST)")
viper.BindPFlag("dyld.str.insensitive", StrSearchCmd.Flags().Lookup("insensitive"))
viper.BindPFlag("dyld.str.contains", StrSearchCmd.Flags().Lookup("contains"))
StrSearchCmd.Flags().StringP("pattern", "p", "", "Regex match strings")
viper.BindPFlag("dyld.str.pattern", StrSearchCmd.Flags().Lookup("pattern"))
}
@@ -59,20 +52,8 @@ var StrSearchCmd = &cobra.Command{
log.SetLevel(log.DebugLevel)
}
var err error
var strRE *regexp.Regexp
insensitive := viper.GetBool("dyld.str.insensitive")
contains := viper.GetBool("dyld.str.contains")
pattern := viper.GetString("dyld.str.pattern")
if len(pattern) > 0 {
strRE, err = regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("invalid regex: %w", err)
}
}
dscPath := filepath.Clean(args[0])
fileInfo, err := os.Lstat(dscPath)
@@ -99,106 +80,18 @@ var StrSearchCmd = &cobra.Command{
}
defer f.Close()
for _, i := range f.Images {
log.Debugf("Searching image %s", i.Name)
m, err := i.GetMacho()
if err != nil {
return err
}
// cstrings
for _, sec := range m.Sections {
if sec.Flags.IsCstringLiterals() || sec.Seg == "__TEXT" && sec.Name == "__const" {
uuid, off, err := f.GetOffset(sec.Addr)
if err != nil {
return fmt.Errorf("failed to get offset for %s.%s: %v", sec.Seg, sec.Name, err)
}
dat, err := f.ReadBytesForUUID(uuid, int64(off), sec.Size)
if err != nil {
return fmt.Errorf("failed to read cstrings in %s.%s: %v", sec.Seg, sec.Name, err)
}
csr := bytes.NewBuffer(dat)
for {
pos := sec.Addr + uint64(csr.Cap()-csr.Len())
s, err := csr.ReadString('\x00')
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("failed to read string: %v", err)
}
s = strings.Trim(s, "\x00")
if len(s) > 0 {
if (sec.Seg == "__TEXT" && sec.Name == "__const") && !utils.IsASCII(s) {
continue // skip non-ascii strings when dumping __TEXT.__const
}
if len(pattern) > 0 {
if strRE.MatchString(s) {
fmt.Printf("%#x: (%s)\t%#v\n", pos, filepath.Base(i.Name), s)
}
} else {
if contains && insensitive {
if strings.Contains(strings.ToLower(s), strings.ToLower(args[1])) {
fmt.Printf("%#x: (%s)\t%#v\n", pos, filepath.Base(i.Name), s)
}
} else if contains {
if strings.Contains(s, args[1]) {
fmt.Printf("%#x: (%s)\t%#v\n", pos, filepath.Base(i.Name), s)
}
} else if insensitive {
if len(s) == len(args[1]) && strings.EqualFold(s, args[1]) {
fmt.Printf("%#x: (%s)\t%#v\n", pos, filepath.Base(i.Name), s)
}
} else {
if len(s) == len(args[1]) && s == args[1] {
fmt.Printf("%#x: (%s)\t%#v\n", pos, filepath.Base(i.Name), s)
}
}
}
}
}
}
}
// objc cfstrings
if cfstrs, err := m.GetCFStrings(); err == nil {
if len(cfstrs) > 0 {
for _, cfstr := range cfstrs {
if len(pattern) > 0 {
if strRE.MatchString(cfstr.Name) {
fmt.Printf("%#09x: (%s)\t%#v\n", cfstr.Address, filepath.Base(i.Name), cfstr.Name)
}
} else {
if contains && insensitive {
if strings.Contains(strings.ToLower(cfstr.Name), strings.ToLower(args[1])) {
fmt.Printf("%#09x: (%s)\t%#v\n", cfstr.Address, filepath.Base(i.Name), cfstr.Name)
}
} else if contains {
if strings.Contains(cfstr.Name, args[1]) {
fmt.Printf("%#09x: (%s)\t%#v\n", cfstr.Address, filepath.Base(i.Name), cfstr.Name)
}
} else if insensitive {
if len(cfstr.Name) == len(args[1]) && strings.EqualFold(cfstr.Name, args[1]) {
fmt.Printf("%#09x: (%s)\t%#v\n", cfstr.Address, filepath.Base(i.Name), cfstr.Name)
}
} else {
if len(cfstr.Name) == len(args[1]) && cfstr.Name == args[1] {
fmt.Printf("%#09x: (%s)\t%#v\n", cfstr.Address, filepath.Base(i.Name), cfstr.Name)
}
}
}
}
}
}
log.Info("Searching for strings...")
strs, err := dscCmd.GetStrings(f, pattern)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
for _, str := range strs {
fmt.Fprintf(w, "%s: %s\t%s\n", colorAddr("%#x", str.Address), str.String, symImageColor(str.Image))
}
w.Flush()
return nil
},
}
+15 -128
View File
@@ -27,9 +27,9 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/apex/log"
dscCmd "github.com/blacktop/ipsw/internal/commands/dsc"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/fatih/color"
@@ -100,147 +100,34 @@ var SymAddrCmd = &cobra.Command{
/******************************************
* Search for symbols in JSON lookup file *
******************************************/
var enc *json.Encoder
var slin []dyld.Symbol
var slout []dyld.Symbol
var lookups []dscCmd.Symbol
symbolFile = filepath.Clean(symbolFile)
sdata, _ := os.ReadFile(symbolFile)
if err := json.Unmarshal(sdata, &slin); err != nil {
lookupData, err := os.ReadFile(filepath.Clean(symbolFile))
if err != nil {
return fmt.Errorf("failed to read symbol lookup JSON file %s: %v", symbolFile, err)
}
if err := json.Unmarshal(lookupData, &lookups); err != nil {
return fmt.Errorf("failed to parse symbol lookup JSON file %s: %v", symbolFile, err)
}
// group syms by image
symages := make(map[string][]dyld.Symbol)
for _, s := range slin {
if len(s.Image) > 0 {
image, err := f.Image(s.Image)
if err != nil {
return err
}
symages[image.Name] = append(symages[image.Name], s)
} else {
symages["unknown"] = append(symages["unknown"], s)
}
}
if _, uhoh := symages["unknown"]; uhoh {
log.Warn("you should supply 'image' fields for each symbol to GREATLY increase speed")
}
for imageName, syms := range symages {
if imageName == "unknown" {
for _, s := range syms {
found := false
for _, image := range f.Images {
if len(s.Regex) > 0 {
re, err := regexp.Compile(s.Regex)
if err != nil {
return err
}
m, err := image.GetPartialMacho()
if err != nil {
return err
}
image.ParseLocalSymbols(false)
for _, lsym := range image.LocalSymbols {
if re.MatchString(lsym.Name) {
var sec string
if lsym.Sect > 0 && int(lsym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[lsym.Sect-1].Seg, m.Sections[lsym.Sect-1].Name)
}
slout = append(slout, dyld.Symbol{
Name: lsym.Name,
Address: lsym.Value,
Type: lsym.Type.String(sec),
Image: image.Name,
Kind: dyld.LOCAL,
})
}
}
image.ParsePublicSymbols(false)
for _, sym := range image.PublicSymbols {
if re.MatchString(sym.Name) {
sym.Image = filepath.Base(image.Name)
slout = append(slout, *sym)
}
}
} else {
if sym, err := image.GetSymbol(s.Name); err == nil {
if sym.Address > 0 {
slout = append(slout, *sym)
found = true
break
}
}
}
}
if !found {
log.Errorf("failed to find address for symbol %s", s.Name)
}
}
} else {
image, err := f.Image(imageName)
if err != nil {
return err
}
for _, s := range syms {
if len(s.Regex) > 0 {
re, err := regexp.Compile(s.Regex)
if err != nil {
return err
}
m, err := image.GetPartialMacho()
if err != nil {
return err
}
image.ParseLocalSymbols(false)
for _, lsym := range image.LocalSymbols {
if re.MatchString(lsym.Name) {
var sec string
if lsym.Sect > 0 && int(lsym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[lsym.Sect-1].Seg, m.Sections[lsym.Sect-1].Name)
}
slout = append(slout, dyld.Symbol{
Name: lsym.Name,
Address: lsym.Value,
Type: lsym.Type.String(sec),
Image: image.Name,
Kind: dyld.LOCAL,
})
}
}
image.ParsePublicSymbols(false)
for _, sym := range image.PublicSymbols {
if re.MatchString(sym.Name) {
sym.Image = filepath.Base(image.Name)
slout = append(slout, *sym)
}
}
} else {
if sym, err := image.GetSymbol(s.Name); err == nil {
slout = append(slout, *sym)
} else {
log.Errorf("failed to find address for symbol %s in image %s", s.Name, filepath.Base(image.Name))
}
}
}
}
syms, err := dscCmd.GetSymbols(f, lookups)
if err != nil {
return fmt.Errorf("failed to lookup symbols from lookup JSON file: %v", err)
}
var enc *json.Encoder
if len(jsonFile) > 0 {
jFile, err := os.Create(jsonFile)
jf, err := os.Create(jsonFile)
if err != nil {
return err
}
defer jFile.Close()
enc = json.NewEncoder(jFile)
defer jf.Close()
enc = json.NewEncoder(jf)
} else {
enc = json.NewEncoder(os.Stdout)
}
if err := enc.Encode(slout); err != nil {
if err := enc.Encode(syms); err != nil {
return err
}
+103 -223
View File
@@ -22,32 +22,17 @@ THE SOFTWARE.
package cmd
import (
"archive/zip"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/commands/extract"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/devicetree"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/blacktop/ipsw/pkg/img4"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/kernelcache"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func isURL(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}
func init() {
rootCmd.AddCommand(extractCmd)
@@ -57,7 +42,7 @@ func init() {
extractCmd.Flags().BoolP("kernel", "k", false, "Extract kernelcache")
extractCmd.Flags().BoolP("dyld", "d", false, "Extract dyld_shared_cache")
extractCmd.Flags().BoolP("dtree", "t", false, "Extract DeviceTree")
extractCmd.Flags().BoolP("dmg", "m", false, "Extract File System DMG file")
extractCmd.Flags().StringP("dmg", "m", "", "Extract DMG file (app, sys, fs)")
extractCmd.Flags().BoolP("iboot", "i", false, "Extract iBoot")
extractCmd.Flags().BoolP("sep", "s", false, "Extract sep-firmware")
extractCmd.Flags().BoolP("kbag", "b", false, "Extract Im4p Keybags")
@@ -66,6 +51,13 @@ func init() {
extractCmd.Flags().StringP("output", "o", "", "Folder to extract files to")
extractCmd.Flags().Bool("flat", false, "Do NOT perserve directory structure when extracting")
extractCmd.Flags().StringArrayP("dyld-arch", "a", []string{}, "dyld_shared_cache architecture to extract")
extractCmd.RegisterFlagCompletionFunc("dmg", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"app\tAppOS",
"sys\tSystemOS",
"fs\tFileSystem",
}, cobra.ShellCompDirectiveDefault
})
viper.BindPFlag("extract.proxy", extractCmd.Flags().Lookup("proxy"))
viper.BindPFlag("extract.insecure", extractCmd.Flags().Lookup("insecure"))
@@ -112,243 +104,131 @@ var extractCmd = &cobra.Command{
return fmt.Errorf("invalid dyld_shared_cache architecture '%s' (must be: arm64, arm64e, x86_64 or x86_64h)", arch)
}
}
} else if viper.GetBool("extract.dmg") {
if !utils.StrSliceHas([]string{"app", "sys", "fs"}, viper.GetString("extract.dmg")) {
return fmt.Errorf("invalid DMG type '%s' (must be: app, sys or fs)", viper.GetString("extract.dmg"))
}
}
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %v", err)
}
config := &extract.Config{
IPSW: "",
URL: "",
Pattern: viper.GetString("extract.pattern"),
Arches: viper.GetStringSlice("extract.dyld-arch"),
Proxy: viper.GetString("extract.proxy"),
Insecure: viper.GetBool("extract.insecure"),
DMGs: false,
DmgType: viper.GetString("extract.dmg"),
Flatten: viper.GetBool("extract.flat"),
Progress: true,
Output: viper.GetString("extract.output"),
}
if viper.GetBool("extract.remote") {
remoteURL := args[0]
config.URL = args[0]
} else {
config.IPSW = args[0]
}
if !isURL(remoteURL) {
log.Fatal("must supply valid URL when using the remote flag")
}
// Get handle to remote IPSW zip
zr, err := download.NewRemoteZipReader(remoteURL, &download.RemoteConfig{
Proxy: viper.GetString("extract.proxy"),
Insecure: viper.GetBool("extract.insecure"),
})
if viper.GetBool("extract.kernel") {
log.Info("Extracting kernelcache")
artifacts, err := extract.Kernelcache(config)
if err != nil {
return fmt.Errorf("unable to download remote zip: %v", err)
return err
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
i, err := info.ParseZipFiles(zr.File)
if viper.GetBool("extract.dyld") {
log.Info("Extracting dyld_shared_cache")
artifacts, err := extract.DSC(config)
if err != nil {
return fmt.Errorf("failed to parse plists in remote zip: %v", err)
return err
}
folder, err := i.GetFolder()
if err != nil {
log.Errorf("failed to get folder from remote zip metadata: %v", err)
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
destPath := filepath.Join(filepath.Clean(viper.GetString("extract.output")), folder)
}
if viper.GetBool("extract.kernel") {
log.Info("Extracting remote kernelcache")
if err = kernelcache.RemoteParse(zr, destPath); err != nil {
return fmt.Errorf("failed to extract kernelcache from remote IPSW: %v", err)
}
}
if viper.GetBool("extract.dyld") {
log.Info("Extracting remote dyld_shared_cache(s)")
sysDMG, err := i.GetSystemOsDmg()
if err != nil {
return fmt.Errorf("only iOS16.x/macOS13.x supported: failed to get SystemOS DMG from remote zip metadata: %v", err)
}
if len(sysDMG) == 0 {
return fmt.Errorf("only iOS16.x/macOS13.x supported: no SystemOS DMG found in remote zip metadata")
}
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_remote_dyld")
if err != nil {
return fmt.Errorf("failed to create temporary directory to store SystemOS DMG: %v", err)
}
defer os.RemoveAll(tmpDIR)
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(fmt.Sprintf("^%s$", sysDMG)), tmpDIR, viper.GetBool("extract.flat"), true); err != nil {
return fmt.Errorf("failed to extract SystemOS DMG from remote IPSW: %v", err)
}
if err := dyld.ExtractFromDMG(i, filepath.Join(tmpDIR, sysDMG), destPath, viper.GetStringSlice("extract.dyld-arch")); err != nil {
return fmt.Errorf("failed to extract dyld_shared_cache(s) from remote IPSW: %v", err)
}
}
if viper.GetBool("extract.dmg") {
if viper.GetBool("extract.dmg") {
config.DMGs = true
if viper.GetBool("extract.remote") {
log.Error("unable to extract File System DMG remotely (let the author know if this is something you want)")
}
if viper.GetBool("extract.dtree") {
log.Info("Extracting remote DeviceTree(s)")
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(`.*DeviceTree.*im(3|4)p$`), destPath, viper.GetBool("extract.flat"), true); err != nil {
return fmt.Errorf("failed to extract DeviceTree from remote IPSW: %v", err)
}
}
if viper.GetBool("extract.iboot") {
log.Info("Extracting remote iBoot(s)")
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(`.*iBoot.*im4p$`), destPath, viper.GetBool("extract.flat"), true); err != nil {
return fmt.Errorf("failed to extract iBoot from remote IPSW: %v", err)
}
}
if viper.GetBool("extract.sep") {
log.Info("Extracting sep-firmware(s)")
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(`.*sep-firmware.*im4p$`), destPath, viper.GetBool("extract.flat"), true); err != nil {
return fmt.Errorf("failed to extract SEPOS from remote IPSW: %v", err)
}
}
if viper.GetBool("extract.kbag") {
log.Info("Extracting im4p kbags")
kbags, err := img4.ParseZipKeyBags(zr.File, i, viper.GetString("extract.pattern"))
} else {
log.Info("Extracting DMG")
artifacts, err := extract.DMG(config)
if err != nil {
return fmt.Errorf("failed to parse im4p kbags: %v", err)
return err
}
out, err := json.Marshal(kbags)
if err != nil {
return fmt.Errorf("failed to marshal im4p kbags: %v", err)
}
fmt.Println(string(out))
os.Mkdir(destPath, 0770)
if err := os.WriteFile(filepath.Join(destPath, "kbags.json"), out, 0660); err != nil {
return fmt.Errorf("failed to write %s: %v", filepath.Join(destPath, "kbags.json"), err)
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
}
if len(viper.GetString("extract.pattern")) > 0 {
log.Infof("Extracting files matching pattern %#v", viper.GetString("extract.pattern"))
validRegex, err := regexp.Compile(viper.GetString("extract.pattern"))
if err != nil {
return fmt.Errorf("failed to compile regexp: %v", err)
}
if err := utils.RemoteUnzip(zr.File, validRegex, destPath, viper.GetBool("extract.flat"), true); err != nil {
return fmt.Errorf("failed to extract files matching pattern in remote IPSW: %v", err)
}
}
} else { // local IPSW/OTA
ipswPath := filepath.Clean(args[0])
if _, err := os.Stat(ipswPath); os.IsNotExist(err) {
return fmt.Errorf("file %s does not exist", ipswPath)
}
i, err := info.Parse(ipswPath)
if viper.GetBool("extract.dtree") {
log.Info("Extracting DeviceTree")
config.Pattern = `.*DeviceTree.*im(3|4)p$`
artifacts, err := extract.Search(config)
if err != nil {
return fmt.Errorf("failed to parse plists in IPSW: %v", err)
return err
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
folder, err := i.GetFolder()
if viper.GetBool("extract.iboot") {
log.Info("Extracting iBoot")
config.Pattern = `.*iBoot.*im4p$`
artifacts, err := extract.Search(config)
if err != nil {
log.Errorf("failed to get folder from zip metadata: %v", err)
return err
}
destPath := filepath.Join(filepath.Clean(viper.GetString("extract.output")), folder)
if viper.GetBool("extract.kernel") {
log.Info("Extracting kernelcaches")
if err := kernelcache.Extract(ipswPath, destPath); err != nil {
return fmt.Errorf("failed to extract kernelcaches from IPSW: %v", err)
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
if viper.GetBool("extract.dyld") {
log.Info("Extracting dyld_shared_cache")
if err := dyld.Extract(ipswPath, destPath, viper.GetStringSlice("extract.dyld-arch")); err != nil {
return fmt.Errorf("failed to extract dyld_shared_cache(s) from IPSW: %v", err)
}
if viper.GetBool("extract.sep") {
log.Info("Extracting sep-firmware")
config.Pattern = `.*sep-firmware.*im4p$`
artifacts, err := extract.Search(config)
if err != nil {
return err
}
if viper.GetBool("extract.dtree") {
log.Info("Extracting DeviceTrees")
if err := devicetree.Extract(ipswPath, destPath); err != nil {
return fmt.Errorf("failed to extract DeviceTrees from IPSW: %v", err)
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
if viper.GetBool("extract.dmg") {
fsDMG, err := i.GetFileSystemOsDmg()
if err != nil {
return fmt.Errorf("failed to find filesystem DMG in IPSW: %v", err)
}
log.Info("Extracting File System DMG")
if _, err := utils.Unzip(ipswPath, destPath, func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), fsDMG)
}); err != nil {
return fmt.Errorf("failed extract %s from IPSW: %v", fsDMG, err)
}
log.Infof("Created %s", filepath.Join(destPath, fsDMG))
if viper.GetBool("extract.kbag") {
log.Info("Extracting im4p key bags")
artifact, err := extract.Keybags(config)
if err != nil {
return err
}
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
if viper.GetBool("extract.iboot") {
log.Info("Extracting iBoot")
if _, err := utils.Unzip(ipswPath, destPath, func(f *zip.File) bool {
var validIBoot = regexp.MustCompile(`.*iBoot.*im4p$`)
return validIBoot.MatchString(f.Name)
}); err != nil {
return fmt.Errorf("failed extract iBoot from IPSW: %v", err)
}
if len(viper.GetString("extract.pattern")) > 0 {
log.Infof("Extracting files matching pattern %#v", viper.GetString("extract.pattern"))
if viper.GetBool("extract.files") {
config.DMGs = true
}
if viper.GetBool("extract.sep") {
log.Info("Extracting sep-firmwares")
if _, err := utils.Unzip(ipswPath, destPath, func(f *zip.File) bool {
var validSEP = regexp.MustCompile(`.*sep-firmware.*im4p$`)
return validSEP.MatchString(f.Name)
}); err != nil {
return fmt.Errorf("failed to extract sep-firmwares from IPSW: %v", err)
}
artifacts, err := extract.Search(config)
if err != nil {
return err
}
if viper.GetBool("extract.kbag") {
log.Info("Extracting im4p kbags")
zr, err := zip.OpenReader(ipswPath)
if err != nil {
return fmt.Errorf("failed to open zip: %v", err)
}
defer zr.Close()
kbags, err := img4.ParseZipKeyBags(zr.File, i, viper.GetString("extract.pattern"))
if err != nil {
return fmt.Errorf("failed to parse im4p kbags: %v", err)
}
out, err := json.Marshal(kbags)
if err != nil {
return fmt.Errorf("failed to marshal im4p kbags: %v", err)
}
fmt.Println(string(out))
os.Mkdir(destPath, 0770)
if err := os.WriteFile(filepath.Join(destPath, "kbags.json"), out, 0660); err != nil {
return fmt.Errorf("failed to write %s: %v", filepath.Join(destPath, "kbags.json"), err)
}
}
if len(viper.GetString("extract.pattern")) > 0 {
log.Infof("Extracting files matching pattern %#v", viper.GetString("extract.pattern"))
patternRE, err := regexp.Compile(viper.GetString("extract.pattern"))
if err != nil {
return fmt.Errorf("failed to compile regexp: %v", err)
}
if viper.GetBool("extract.files") { // SEARCH THE DMGs
if appOS, err := i.GetAppOsDmg(); err == nil {
if err := utils.ExtractFromDMG(ipswPath, appOS, destPath, patternRE); err != nil {
return fmt.Errorf("failed to extract files from AppOS %s: %v", appOS, err)
}
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
if err := utils.ExtractFromDMG(ipswPath, systemOS, destPath, patternRE); err != nil {
return fmt.Errorf("failed to extract files from SystemOS %s: %v", systemOS, err)
}
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
if err := utils.ExtractFromDMG(ipswPath, fsOS, destPath, patternRE); err != nil {
return fmt.Errorf("failed to extract files from filesystem %s: %v", fsOS, err)
}
}
} else { // SEARCH THE ZIP
zr, err := zip.OpenReader(ipswPath)
if err != nil {
return fmt.Errorf("failed to open IPSW: %v", err)
}
defer zr.Close()
if err := utils.RemoteUnzip(zr.File, patternRE, destPath, viper.GetBool("extract.flat"), false); err != nil {
return fmt.Errorf("failed to extract files matching pattern: %v", err)
}
}
for _, artifact := range artifacts {
utils.Indent(log.Info, 2)("Extracted " + strings.TrimPrefix(cwd, artifact))
}
}
+9 -73
View File
@@ -1,5 +1,3 @@
//go:build darwin
/*
Copyright © 2018-2023 blacktop
@@ -24,19 +22,15 @@ THE SOFTWARE.
package cmd
import (
"archive/zip"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/commands/mount"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/info"
"github.com/spf13/cobra"
)
@@ -66,82 +60,24 @@ var mountCmd = &cobra.Command{
log.SetLevel(log.DebugLevel)
}
ipswPath := filepath.Clean(args[1])
i, err := info.Parse(ipswPath)
mctx, err := mount.DmgInIPSW(args[1], args[0])
if err != nil {
return fmt.Errorf("failed to parse IPSW: %v", err)
return fmt.Errorf("failed to mount %s DMG: %v", args[0], err)
}
var dmgPath string
switch args[0] {
case "fs":
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return fmt.Errorf("failed to get filesystem DMG: %v", err)
}
log.Info("Found Filesystem DMG")
case "sys":
dmgPath, err = i.GetSystemOsDmg()
if err != nil {
return fmt.Errorf("failed to get SystemOS DMG: %v", err)
}
log.Info("Found SystemOS DMG")
case "app":
dmgPath, err = i.GetAppOsDmg()
if err != nil {
return fmt.Errorf("failed to get AppOS DMG: %v", err)
}
log.Info("Found AppOS DMG")
default:
return fmt.Errorf("invalid subcommand: %s; must be one of: '%s'", args[0], strings.Join(mountCmdSubCmds, "', '"))
}
// check if filesystem DMG already exists (due to previous mount command)
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
// extract filesystem DMG
dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
return fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
defer os.Remove(dmgs[0])
if mctx.AlreadyMounted {
log.Infof("%s DMG already mounted at %s", args[0], mctx.MountPoint)
} else {
utils.Indent(log.Debug, 2)(fmt.Sprintf("Found extracted %s", dmgPath))
}
// mount filesystem DMG
utils.Indent(log.Info, 2)(fmt.Sprintf("Mounting %s", dmgPath))
mountPoint, alreadyMounted, err := utils.MountFS(dmgPath)
if err != nil {
if !errors.Is(err, utils.ErrMountResourceBusy) {
return fmt.Errorf("failed to mount DMG: %v", err)
}
}
if alreadyMounted {
utils.Indent(log.Info, 3)(fmt.Sprintf("%s already mounted", dmgPath))
} else {
defer func() {
utils.Indent(log.Info, 2)(fmt.Sprintf("Unmounting %s", dmgPath))
if err := utils.Retry(3, 2*time.Second, func() error {
return utils.Unmount(mountPoint, false)
}); err != nil {
log.Errorf("failed to unmount %s at %s: %v", dmgPath, mountPoint, err)
}
}()
log.Infof("Mounted %s DMG %s", args[0], filepath.Base(mctx.DmgPath))
}
// block until user hits ctrl-c
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
utils.Indent(log.Info, 3)(fmt.Sprintf("Press Ctrl+C to unmount '%s' ...", mountPoint))
utils.Indent(log.Info, 2)(fmt.Sprintf("Press Ctrl+C to unmount '%s' ...", mctx.MountPoint))
<-done
return nil
utils.Indent(log.Info, 2)(fmt.Sprintf("Unmounting %s", mctx.MountPoint))
return mctx.Unmount()
},
}
+3 -9
View File
@@ -79,14 +79,8 @@ var pongoCmd = &cobra.Command{
var kbags *img4.KeyBags
if viper.GetBool("pongo.remote") {
remoteURL := args[0]
if !isURL(remoteURL) {
log.Fatal("must supply valid URL when using the remote flag")
}
// Get handle to remote IPSW zip
zr, err := download.NewRemoteZipReader(remoteURL, &download.RemoteConfig{
zr, err := download.NewRemoteZipReader(args[0], &download.RemoteConfig{
Proxy: viper.GetString("pongo.proxy"),
Insecure: viper.GetBool("pongo.insecure"),
})
@@ -112,7 +106,7 @@ var pongoCmd = &cobra.Command{
if viper.GetBool("pongo.decrypt") {
for _, kbag := range kbags.Files {
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(kbag.Name), destPath, true, true); err != nil {
if _, err := utils.SearchZip(zr.File, regexp.MustCompile(kbag.Name), destPath, true, true); err != nil {
return fmt.Errorf("failed to extract files matching pattern in remote IPSW: %v", err)
}
}
@@ -148,7 +142,7 @@ var pongoCmd = &cobra.Command{
if viper.GetBool("pongo.decrypt") {
for _, kbag := range kbags.Files {
if err := utils.RemoteUnzip(zr.File, regexp.MustCompile(fmt.Sprintf(".*%s$", kbag.Name)), destPath, true, false); err != nil {
if _, err := utils.SearchZip(zr.File, regexp.MustCompile(fmt.Sprintf(".*%s$", kbag.Name)), destPath, true, false); err != nil {
return fmt.Errorf("failed to extract files matching pattern in remote IPSW: %v", err)
}
}
+2 -2
View File
@@ -77,7 +77,7 @@ func init() {
// will be global for your application.
// Flags
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ipsw/config.yaml)")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/ipsw/config.yaml)")
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "V", false, "verbose output")
rootCmd.PersistentFlags().BoolVar(&Color, "color", false, "colorize output")
rootCmd.PersistentFlags().String("diff-tool", "", "git diff tool (for --diff commands)")
@@ -109,7 +109,7 @@ func initConfig() {
cobra.CheckErr(err)
// Search config in home directory with name ".ipsw" (without extension).
viper.AddConfigPath(filepath.Join(home, ".ipsw"))
viper.AddConfigPath(filepath.Join(home, ".config", "ipsw"))
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+87
View File
@@ -0,0 +1,87 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/apex/log"
clihander "github.com/apex/log/handlers/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "ipswd",
Short: "ipsw daemon",
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Error(err.Error())
os.Exit(1)
}
}
func init() {
log.SetHandler(clihander.Default)
cobra.OnInitialize(initConfig)
// Flags
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/ipsw/config.yaml)")
// Settings
rootCmd.CompletionOptions.HiddenDefaultCmd = true
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".ipsw" (without extension).
viper.AddConfigPath(filepath.Join(home, ".config", "ipsw"))
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
viper.SetEnvPrefix("ipsw")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
viper.AutomaticEnv()
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"github.com/blacktop/ipsw/internal/daemon"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.AddCommand(startCmd)
startCmd.Flags().BoolP("debug", "d", false, "Debug mode")
viper.BindPFlag("start.debug", startCmd.Flags().Lookup("debug"))
}
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "Start the ipswd daemon",
RunE: func(cmd *cobra.Command, args []string) error {
return daemon.NewDaemon().Start()
},
}
+39
View File
@@ -0,0 +1,39 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(statusCmd)
}
// statusCmd represents the status command
var statusCmd = &cobra.Command{
Use: "status",
Short: "Get the ipswd daemon status",
RunE: func(cmd *cobra.Command, args []string) error {
panic("not implemented")
},
}
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"github.com/blacktop/ipsw/internal/daemon"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(stopCmd)
}
// stopCmd represents the stop command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the ipswd daemon",
RunE: func(cmd *cobra.Command, args []string) error {
return daemon.NewDaemon().Stop()
},
}
+44
View File
@@ -0,0 +1,44 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"strings"
"github.com/blacktop/ipsw/api/types"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Aliases: []string{"v"},
Short: "Print the version number of ipswd",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s, BuildTime: %s\n", strings.TrimSpace(types.BuildVersion), strings.TrimSpace(types.BuildTime))
},
}
+28
View File
@@ -0,0 +1,28 @@
/*
Copyright © 2023 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package main
import "github.com/blacktop/ipsw/cmd/ipswd/cmd"
func main() {
cmd.Execute()
}
+9
View File
@@ -0,0 +1,9 @@
# This is an example config.yml file with some sensible defaults.
# Make sure to check the documentation at https://blacktop.github.io/ipsw
daemon:
socket: /tmp/ipsw.sock
debug: false
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://blacktop.github.io/ipsw/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
+29 -3
View File
@@ -23,12 +23,13 @@ require (
github.com/frida/frida-go v0.6.2
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
github.com/gen2brain/beeep v0.0.0-20230307103607-6e717729cb4f
github.com/gin-gonic/gin v1.9.0
github.com/glebarez/sqlite v1.7.0
github.com/gocolly/colly/v2 v2.1.0
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/google/gousb v1.1.2
github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.6.0
github.com/jinzhu/gorm v1.9.16
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
@@ -50,6 +51,8 @@ require (
golang.org/x/sys v0.7.0
golang.org/x/term v0.7.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.0
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
)
// TODO: remove this once https://github.com/spf13/cast/pull/155 is merged
@@ -70,6 +73,8 @@ require (
github.com/antchfx/htmlquery v1.3.0 // indirect
github.com/antchfx/xmlquery v1.3.15 // indirect
github.com/antchfx/xpath v1.2.4 // indirect
github.com/bytedance/sonic v1.8.7 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
@@ -79,8 +84,14 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.12.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -90,22 +101,31 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -117,12 +137,18 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.4.0 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
)
+77 -20
View File
@@ -97,9 +97,15 @@ github.com/blacktop/lzss v0.1.1 h1:ADOUgVH3hq2miBfaxhV1GgY97wNSv5fBiXRnUVe8QSg=
github.com/blacktop/lzss v0.1.1/go.mod h1:eWPx0Tq21QndictvoAb20bFvvhDDbZ3mkcZ/mDHkwBk=
github.com/blacktop/ranger v1.0.3 h1:uIWlR8chK6AtNNPZz67+cwibOe8T5XnYyQfkk04ueZA=
github.com/blacktop/ranger v1.0.3/go.mod h1:2KXlAasHzrHMkXg1+/87hT5ISdQjxlcjbmZFZD69zas=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/caarlos0/ctrlc v1.2.0 h1:AtbThhmbeYx1WW3WXdWrd94EHKi+0NPRGS4/4pzrjwk=
github.com/caarlos0/ctrlc v1.2.0/go.mod h1:n3gDlSjsXZ7rbD9/RprIR040b7oaLfNStikPd4gFago=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -118,8 +124,6 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
@@ -143,8 +147,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
@@ -158,16 +160,31 @@ github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvD
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/gen2brain/beeep v0.0.0-20230307103607-6e717729cb4f h1:oRm7Hy2dQWfHgOuOWRaYZf+kZcWJst7fxAlq+yjdLss=
github.com/gen2brain/beeep v0.0.0-20230307103607-6e717729cb4f/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
@@ -177,8 +194,6 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -224,6 +239,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gousb v1.1.2 h1:1BwarNB3inFTFhPgUEfah4hwOPuDz/49I0uX8XNginU=
github.com/google/gousb v1.1.2/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -239,6 +255,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -263,16 +280,24 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -281,19 +306,23 @@ github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8Nz
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -309,9 +338,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -321,6 +347,11 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
@@ -346,11 +377,15 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@@ -392,8 +427,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
@@ -408,6 +444,10 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/unicorn-engine/unicorn v0.0.0-20230207094436-7b8c63dfe650 h1:O8wGQwIFVKk04THvsqbrMi9rXfJwxRU1Bf04NBWvdhI=
@@ -427,17 +467,19 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -515,6 +557,7 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
@@ -583,6 +626,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -783,6 +827,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -792,6 +840,15 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
@@ -0,0 +1,38 @@
[Unit]
Description=ipsw daemon
Documentation=https://blacktop.github.io/ipsw
After=network-online.target ipsw.socket
Wants=network-online.target
Requires=ipsw.socket
[Service]
Type=notify
ExecStart=/usr/bin/ipswd start
ExecStop=/usr/bin/ipswd stop
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
# Note that StartLimit* options were moved from "Service" to "Unit" in systemd 229.
# Both the old, and new location are accepted by systemd 229 and up, so using the old location
# to make them work for either version of systemd.
StartLimitBurst=3
# Note that StartLimitInterval was renamed to StartLimitIntervalSec in systemd 230.
# Both the old, and new name are accepted by systemd 230 and up, so using the old name to make
# this option work for either version of systemd.
StartLimitInterval=60s
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
# Comment TasksMax if your systemd version does not support it.
# Only systemd 226 and above support this option.
TasksMax=infinity
[Install]
WantedBy=multi-user.target
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=ipswd Socket for the API
[Socket]
# If /var/run is not implemented as a symlink to /run, you may need to
# specify ListenStream=/var/run/ipsw.sock instead.
ListenStream=/run/ipsw.sock
SocketMode=0660
SocketUser=root
SocketGroup=ipsw
[Install]
WantedBy=sockets.target
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace
fi
# utility functions
INFO() {
/bin/echo -e "\e[104m\e[97m[INFO]\e[49m\e[39m $@"
}
WARNING() {
/bin/echo >&2 -e "\e[101m\e[97m[WARNING]\e[49m\e[39m $@"
}
ERROR() {
/bin/echo >&2 -e "\e[101m\e[97m[ERROR]\e[49m\e[39m $@"
}
# constants
SYSTEMD_UNIT="ipsw.service"
# global vars
BIN=""
SYSTEMD=""
CFG_DIR=""
XDG_RUNTIME_DIR_CREATED=""
# run checks and also initialize global vars
init() {
# OS verification: Linux only
case "$(uname)" in
Linux) ;;
*)
ERROR "ipswd cannot be installed on $(uname)"
exit 1
;;
esac
# set SYSTEMD
if systemctl --user show-environment >/dev/null 2>&1; then
SYSTEMD=1
fi
# HOME verification
if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
ERROR "HOME needs to be set"
exit 1
fi
if [ ! -w "$HOME" ]; then
ERROR "HOME needs to be writable"
exit 1
fi
# set CFG_DIR
CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
# Validate XDG_RUNTIME_DIR and set XDG_RUNTIME_DIR_CREATED
if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ ! -w "$XDG_RUNTIME_DIR" ]; then
if [ -n "$SYSTEMD" ]; then
ERROR "Aborting because systemd was detected but XDG_RUNTIME_DIR (\"$XDG_RUNTIME_DIR\") is not set, does not exist, or is not writable"
ERROR "Hint: this could happen if you changed users with 'su' or 'sudo'. To work around this:"
ERROR "- try again by first running with root privileges 'loginctl enable-linger <user>' where <user> is the unprivileged user and export XDG_RUNTIME_DIR to the value of RuntimePath as shown by 'loginctl show-user <user>'"
ERROR "- or simply log back in as the desired unprivileged user (ssh works for remote machines, machinectl shell works for local machines)"
exit 1
fi
export XDG_RUNTIME_DIR="$HOME/.docker/run"
mkdir -p -m 700 "$XDG_RUNTIME_DIR"
XDG_RUNTIME_DIR_CREATED=1
fi
}
show_systemd_error() {
n="20"
ERROR "Failed to start ${SYSTEMD_UNIT}. Run \`journalctl -n ${n} --no-pager --user --unit ${SYSTEMD_UNIT}\` to show the error log."
}
# install (systemd)
install_systemd() {
mkdir -p "${CFG_DIR}/systemd/user"
unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
if [ -f "${unit_file}" ]; then
WARNING "File already exists, skipping: ${unit_file}"
else
systemctl --user daemon-reload
fi
if ! systemctl --user --no-pager status "${SYSTEMD_UNIT}" >/dev/null 2>&1; then
INFO "starting systemd service ${SYSTEMD_UNIT}"
(
set -x
if ! systemctl --user start "${SYSTEMD_UNIT}"; then
set +x
show_systemd_error
exit 1
fi
sleep 3
)
fi
(
set -x
if ! systemctl --user --no-pager --full status "${SYSTEMD_UNIT}"; then
set +x
show_systemd_error
exit 1
fi
IPSW_HOST="unix://$XDG_RUNTIME_DIR/ipsw.sock" $BIN/ipswd version
systemctl --user enable "${SYSTEMD_UNIT}"
)
INFO "Installed ${SYSTEMD_UNIT} successfully."
INFO "To control ${SYSTEMD_UNIT}, run: \`systemctl --user (start|stop|restart) ${SYSTEMD_UNIT}\`"
INFO "To run ${SYSTEMD_UNIT} on system startup, run: \`sudo loginctl enable-linger $(id -un)\`"
echo
}
main() {
init
install_systemd
echo " 🎉 Done!"
}
main "$@"
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace
fi
# utility functions
INFO() {
/bin/echo -e "\e[104m\e[97m[INFO]\e[49m\e[39m $@"
}
WARNING() {
/bin/echo >&2 -e "\e[101m\e[97m[WARNING]\e[49m\e[39m $@"
}
ERROR() {
/bin/echo >&2 -e "\e[101m\e[97m[ERROR]\e[49m\e[39m $@"
}
# constants
SYSTEMD_UNIT="ipsw.service"
# global vars
BIN=""
SYSTEMD=""
CFG_DIR=""
XDG_RUNTIME_DIR_CREATED=""
# run checks and also initialize global vars
init() {
# OS verification: Linux only
case "$(uname)" in
Linux) ;;
*)
ERROR "ipswd cannot be installed on $(uname)"
exit 1
;;
esac
# set SYSTEMD
if systemctl --user show-environment >/dev/null 2>&1; then
SYSTEMD=1
fi
# HOME verification
if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
ERROR "HOME needs to be set"
exit 1
fi
if [ ! -w "$HOME" ]; then
ERROR "HOME needs to be writable"
exit 1
fi
# set CFG_DIR
CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
# Validate XDG_RUNTIME_DIR and set XDG_RUNTIME_DIR_CREATED
if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ ! -w "$XDG_RUNTIME_DIR" ]; then
if [ -n "$SYSTEMD" ]; then
ERROR "Aborting because systemd was detected but XDG_RUNTIME_DIR (\"$XDG_RUNTIME_DIR\") is not set, does not exist, or is not writable"
ERROR "Hint: this could happen if you changed users with 'su' or 'sudo'. To work around this:"
ERROR "- try again by first running with root privileges 'loginctl enable-linger <user>' where <user> is the unprivileged user and export XDG_RUNTIME_DIR to the value of RuntimePath as shown by 'loginctl show-user <user>'"
ERROR "- or simply log back in as the desired unprivileged user (ssh works for remote machines, machinectl shell works for local machines)"
exit 1
fi
export XDG_RUNTIME_DIR="$HOME/.docker/run"
mkdir -p -m 700 "$XDG_RUNTIME_DIR"
XDG_RUNTIME_DIR_CREATED=1
fi
}
# CLI subcommand: "uninstall"
cmd_uninstall() {
# requirements are already checked in init()
if [ -z "$SYSTEMD" ]; then
INFO "systemd not detected, ${DOCKERD_ROOTLESS_SH} needs to be stopped manually:"
else
unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
(
set -x
systemctl --user stop "${SYSTEMD_UNIT}"
) || :
(
set -x
systemctl --user disable "${SYSTEMD_UNIT}"
) || :
rm -f "${unit_file}"
INFO "Uninstalled ${SYSTEMD_UNIT}"
fi
if cli_ctx_exists "${CLI_CONTEXT}"; then
cli_ctx_rm "${CLI_CONTEXT}"
INFO "Deleted CLI context \"${CLI_CONTEXT}\""
fi
unset DOCKER_HOST
unset DOCKER_CONTEXT
cli_ctx_use "default"
INFO 'Configured CLI use the "default" context.'
INFO
INFO 'Make sure to unset or update the environment PATH, DOCKER_HOST, and DOCKER_CONTEXT environment variables if you have added them to `~/.bashrc`.'
INFO "This uninstallation tool does NOT remove Docker binaries and data."
INFO "To remove data, run: \`$BIN/rootlesskit rm -rf $HOME/.local/share/docker\`"
}
main() {
init
cmd_uninstall
echo " 🎉 Done!"
}
main "$@"
+5 -2
View File
@@ -1,7 +1,10 @@
#!/bin/sh
set -e
rm -rf completions
mkdir completions
mkdir -p completions/{ipsw,ipswd}
for sh in bash zsh fish powershell; do
go run ./cmd/ipsw/main.go completion "$sh" >"completions/_$sh"
go run ./cmd/ipsw/main.go completion "$sh" >"completions/ipsw/_$sh"
done
for sh in bash zsh fish powershell; do
go run ./cmd/ipswd/main.go completion "$sh" >"completions/ipswd/_$sh"
done
+8
View File
@@ -16,7 +16,15 @@ This script generates markdown docs from cli.
exit
fi
move_swagger() {
if [[ -f api/swagger.json ]]; then
cp api/swagger.json www/static/api/swagger.json
fi
}
main() {
move_swagger
SED="sed"
if which gsed >/dev/null 2>&1; then
SED="gsed"
+1
View File
@@ -0,0 +1 @@
package download
+25
View File
@@ -0,0 +1,25 @@
package ipsw
import (
"fmt"
"github.com/blacktop/ipsw/internal/download"
)
// GetLatestIosVersion returns the latest iOS version
func GetLatestIosVersion(proxy string, insecure bool) (string, error) {
assets, err := download.GetAssetSets(proxy, insecure)
if err != nil {
return "", fmt.Errorf("failed to get asset latest version: %v", err)
}
return assets.LatestVersion("iOS", "ios"), nil
}
// GetLatestIosBuild returns the latest iOS build
func GetLatestIosBuild() (string, error) { // TODO: add proxy and insecure support
itunes, err := download.NewMacOsXML()
if err != nil {
return "", fmt.Errorf("failed to parse itunes XML: %v", err)
}
return itunes.GetLatestBuild()
}
+358
View File
@@ -0,0 +1,358 @@
// Package dsc implements the `dsc` commands
package dsc
import (
"bytes"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"github.com/blacktop/go-macho/pkg/codesign"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/dyld"
)
// ImportedBy is a struct that contains information about which dyld_shared_cache dylibs import a given dylib
type ImportedBy struct {
DSC []string `json:"dsc,omitempty"`
Apps []string `json:"apps,omitempty"`
}
// Dylib is a struct that contains information about a dyld_shared_cache dylib
type Dylib struct {
Index int `json:"index,omitempty"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
UUID string `json:"uuid,omitempty"`
LoadAddress uint64 `json:"load_address,omitempty"`
}
// Info is a struct that contains information about a dyld_shared_cache file
type Info struct {
Magic string `json:"magic,omitempty"`
UUID string `json:"uuid,omitempty"`
Platform string `json:"platform,omitempty"`
MaxSlide int `json:"max_slide,omitempty"`
SubCacheArrayCount int `json:"num_sub_caches,omitempty"`
SubCacheGroupID int `json:"sub_cache_group_id,omitempty"`
SymSubCacheUUID string `json:"sym_sub_cache_uuid,omitempty"`
Mappings map[string][]dyld.CacheMappingWithSlideInfo `json:"mappings,omitempty"`
CodeSignature map[string]codesign.CodeSignature `json:"code_signature,omitempty"`
Dylibs []Dylib `json:"dylibs,omitempty"`
}
// Symbol is a struct that contains information about a dyld_shared_cache symbol
type Symbol struct {
Address uint64 `json:"address,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Image string `json:"image,omitempty"`
Pattern string `json:"pattern,omitempty"`
}
// String is a struct that contains information about a dyld_shared_cache string
type String struct {
Address uint64 `json:"address,omitempty"`
Image string `json:"image,omitempty"`
String string `json:"string,omitempty"`
}
// GetDylibsThatImport returns a list of dylibs that import the given dylib
func GetDylibsThatImport(f *dyld.File, name string) (*ImportedBy, error) {
var importedBy ImportedBy
image, err := f.Image(name)
if err != nil {
return nil, fmt.Errorf("dylib not in DSC: %v", err)
}
if f.SupportsDylibPrebuiltLoader() {
for _, img := range f.Images {
pbl, err := f.GetDylibPrebuiltLoader(img.Name)
if err != nil {
return nil, err
}
for _, dep := range pbl.Dependents {
if strings.EqualFold(dep.Name, image.Name) {
importedBy.DSC = append(importedBy.DSC, img.Name)
}
}
}
} else {
for _, img := range f.Images {
m, err := img.GetPartialMacho()
if err != nil {
return nil, err
}
for _, imp := range m.ImportedLibraries() {
if strings.EqualFold(imp, image.Name) {
importedBy.DSC = append(importedBy.DSC, img.Name)
}
}
m.Close()
}
}
if f.SupportsPrebuiltLoaderSet() {
if err := f.ForEachLaunchLoaderSet(func(execPath string, pset *dyld.PrebuiltLoaderSet) {
for _, loader := range pset.Loaders {
for _, dep := range loader.Dependents {
if strings.EqualFold(dep.Name, image.Name) {
if execPath != loader.Path {
importedBy.Apps = append(importedBy.Apps, fmt.Sprintf("%s (%s)", execPath, loader.Path))
} else {
importedBy.Apps = append(importedBy.Apps, execPath)
}
}
}
}
}); err != nil {
return nil, err
}
}
return &importedBy, nil
}
// GetInfo returns a Info struct for a given dyld_shared_cache file
func GetInfo(f *dyld.File) (*Info, error) {
info := &Info{
Magic: f.Headers[f.UUID].Magic.String(),
UUID: f.UUID.String(),
Platform: f.Headers[f.UUID].Platform.String(),
MaxSlide: int(f.Headers[f.UUID].MaxSlide),
}
info.Mappings = make(map[string][]dyld.CacheMappingWithSlideInfo)
for u, mp := range f.MappingsWithSlideInfo {
for _, m := range mp {
info.Mappings[u.String()] = append(info.Mappings[u.String()], *m)
}
}
info.CodeSignature = make(map[string]codesign.CodeSignature)
for u, cs := range f.CodeSignatures {
info.CodeSignature[u.String()] = *cs
}
for idx, img := range f.Images {
m, err := img.GetPartialMacho()
if err != nil {
continue
// return fmt.Errorf("failed to create partial MachO for image %s: %v", img.Name, err)
}
info.Dylibs = append(info.Dylibs, Dylib{
Index: idx + 1,
Name: img.Name,
Version: m.SourceVersion().Version.String(),
UUID: m.UUID().String(),
LoadAddress: img.Info.Address,
})
m.Close()
}
return info, nil
}
// GetSymbols returns a list of symbols from a dyld_shared_cache file for a given list of lookup symbol structs
func GetSymbols(f *dyld.File, lookups []Symbol) ([]Symbol, error) {
var syms []Symbol
// group syms by image
sym2imgs := make(map[string][]Symbol)
for _, lookup := range lookups {
if len(lookup.Pattern) == 0 {
return nil, fmt.Errorf("pattern cannot be empty: %v", lookup)
}
if len(lookup.Image) > 0 {
image, err := f.Image(lookup.Image)
if err != nil {
return nil, fmt.Errorf("failed to get image %s: %v", lookup.Image, err)
}
sym2imgs[image.Name] = append(sym2imgs[image.Name], lookup)
} else {
sym2imgs["unknown"] = append(sym2imgs["unknown"], lookup)
}
}
for imageName, lookups := range sym2imgs {
if imageName == "unknown" {
for _, lookup := range lookups {
re, err := regexp.Compile(lookup.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex for %v: %w", lookup, err)
}
for _, image := range f.Images {
m, err := image.GetPartialMacho()
if err != nil {
return nil, err
}
if err := image.ParseLocalSymbols(false); err != nil {
return nil, err
}
for _, lsym := range image.LocalSymbols {
if re.MatchString(lsym.Name) {
var sec string
if lsym.Sect > 0 && int(lsym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[lsym.Sect-1].Seg, m.Sections[lsym.Sect-1].Name)
}
syms = append(syms, Symbol{
Name: lsym.Name,
Address: lsym.Value,
Type: lsym.Type.String(sec),
Image: filepath.Base(image.Name),
})
}
}
if err := image.ParsePublicSymbols(false); err != nil {
return nil, err
}
for _, sym := range image.PublicSymbols {
if re.MatchString(sym.Name) {
syms = append(syms, Symbol{
Name: sym.Name,
Address: sym.Address,
Type: sym.Type,
Image: filepath.Base(image.Name),
})
}
}
}
}
} else { // image is known
image, err := f.Image(imageName)
if err != nil {
return nil, err
}
for _, lookup := range lookups {
re, err := regexp.Compile(lookup.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex for %v: %w", lookup, err)
}
m, err := image.GetPartialMacho()
if err != nil {
return nil, err
}
if err := image.ParseLocalSymbols(false); err != nil {
return nil, err
}
for _, lsym := range image.LocalSymbols {
if re.MatchString(lsym.Name) {
var sec string
if lsym.Sect > 0 && int(lsym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[lsym.Sect-1].Seg, m.Sections[lsym.Sect-1].Name)
}
syms = append(syms, Symbol{
Name: lsym.Name,
Address: lsym.Value,
Type: lsym.Type.String(sec),
Image: filepath.Base(image.Name),
})
}
}
if err := image.ParsePublicSymbols(false); err != nil {
return nil, err
}
for _, sym := range image.PublicSymbols {
if re.MatchString(sym.Name) {
syms = append(syms, Symbol{
Name: sym.Name,
Address: sym.Address,
Type: sym.Type,
Image: filepath.Base(image.Name),
})
}
}
}
}
}
return syms, nil
}
// GetStrings returns a list of strings from a dyld_shared_cache file for a given regex pattern
func GetStrings(f *dyld.File, pattern string) ([]String, error) {
var strs []String
if len(pattern) == 0 {
return nil, fmt.Errorf("pattern cannot be empty")
}
strRE, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}
for _, i := range f.Images {
m, err := i.GetMacho()
if err != nil {
return nil, fmt.Errorf("failed to create MachO for image %s: %v", i.Name, err)
}
// cstrings
for _, sec := range m.Sections {
if sec.Flags.IsCstringLiterals() || sec.Seg == "__TEXT" && sec.Name == "__const" {
uuid, off, err := f.GetOffset(sec.Addr)
if err != nil {
return nil, fmt.Errorf("failed to get offset for %s.%s: %v", sec.Seg, sec.Name, err)
}
dat, err := f.ReadBytesForUUID(uuid, int64(off), sec.Size)
if err != nil {
return nil, fmt.Errorf("failed to read cstrings in %s.%s: %v", sec.Seg, sec.Name, err)
}
csr := bytes.NewBuffer(dat)
for {
pos := sec.Addr + uint64(csr.Cap()-csr.Len())
s, err := csr.ReadString('\x00')
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("failed to read string: %v", err)
}
s = strings.Trim(s, "\x00")
if len(s) > 0 {
if (sec.Seg == "__TEXT" && sec.Name == "__const") && !utils.IsASCII(s) {
continue // skip non-ascii strings when dumping __TEXT.__const
}
if strRE.MatchString(s) {
strs = append(strs, String{
Address: pos,
Image: filepath.Base(i.Name),
String: s,
})
}
}
}
}
}
// objc cfstrings
if cfstrs, err := m.GetCFStrings(); err == nil {
if len(cfstrs) > 0 {
for _, cfstr := range cfstrs {
if strRE.MatchString(cfstr.Name) {
strs = append(strs, String{
Address: cfstr.Address,
Image: filepath.Base(i.Name),
String: cfstr.Name,
})
}
}
}
}
}
return strs, nil
}
+18 -16
View File
@@ -74,27 +74,29 @@ func GetDatabase(ipswPath, entDBPath string) (map[string]string, error) {
}
}
buff := new(bytes.Buffer)
if len(entDBPath) > 0 {
buff := new(bytes.Buffer)
e := gob.NewEncoder(buff)
e := gob.NewEncoder(buff)
// Encoding the map
err := e.Encode(entDB)
if err != nil {
return nil, fmt.Errorf("failed to encode entitlement db to binary: %v", err)
}
// Encoding the map
err := e.Encode(entDB)
if err != nil {
return nil, fmt.Errorf("failed to encode entitlement db to binary: %v", err)
}
of, err := os.Create(entDBPath)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %v", ipswPath+".entDB", err)
}
defer of.Close()
of, err := os.Create(entDBPath)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %v", ipswPath+".entDB", err)
}
defer of.Close()
gzw := gzip.NewWriter(of)
defer gzw.Close()
gzw := gzip.NewWriter(of)
defer gzw.Close()
if _, err := buff.WriteTo(gzw); err != nil {
return nil, fmt.Errorf("failed to write entitlement db to gzip file: %v", err)
if _, err := buff.WriteTo(gzw); err != nil {
return nil, fmt.Errorf("failed to write entitlement db to gzip file: %v", err)
}
}
} else {
log.Info("Found ipsw entitlement database file...")
+349
View File
@@ -0,0 +1,349 @@
// Package extract contains the extract commands.
package extract
import (
"archive/zip"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/blacktop/ipsw/pkg/img4"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/kernelcache"
)
// Config is the extract command configuration.
//
// swagger:model ExtractConfig
type Config struct {
// path to the IPSW
//
// required: true
IPSW string `json:"ipsw,omitempty"`
// url to the remote IPSW
//
// required: true
URL string `json:"url,omitempty"`
// regex pattern to search for in the IPSW
//
// required: true
Pattern string `json:"pattern,omitempty"`
// arches of the DSCs to extract
//
// required: true
Arches []string `json:"arches,omitempty"`
// http proxy to use
//
// required: true
Proxy string `json:"proxy,omitempty"`
// don't verify the certificate chain
//
// required: true
Insecure bool `json:"insecure,omitempty"`
// search the DMGs for files
//
// required: true
DMGs bool `json:"dmgs,omitempty"`
// type of DMG to extract
//
// required: true
// pattern: (app|sys|fs)
DmgType string `json:"dmg_type,omitempty"`
// flatten the extracted files paths (remove the folders)
//
// required: true
Flatten bool `json:"flatten,omitempty"`
// show the progress bar (when using the CLI)
//
// required: false
Progress bool `json:"progress,omitempty"`
// output directory to write extracted files to
//
// required: true
Output string `json:"output,omitempty"`
}
func isURL(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}
func getFolder(c *Config) (*info.Info, string, error) {
c.IPSW = filepath.Clean(c.IPSW)
i, err := info.Parse(c.IPSW)
if err != nil {
return nil, "", fmt.Errorf("failed to parse plists in IPSW: %v", err)
}
folder, err := i.GetFolder()
if err != nil {
log.Errorf("failed to get folder from IPSW metadata: %v", err)
}
return i, folder, nil
}
func getRemoteFolder(c *Config) (*info.Info, *zip.Reader, string, error) {
zr, err := download.NewRemoteZipReader(c.URL, &download.RemoteConfig{
Proxy: c.Proxy,
Insecure: c.Insecure,
})
if err != nil {
return nil, nil, "", fmt.Errorf("unable to download remote zip: %v", err)
}
i, err := info.ParseZipFiles(zr.File)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to parse plists in remote zip: %v", err)
}
folder, err := i.GetFolder()
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get folder from remote zip metadata: %v", err)
}
return i, zr, folder, nil
}
// Kernelcache extracts the kernelcache from an IPSW
func Kernelcache(c *Config) ([]string, error) {
if len(c.IPSW) > 0 {
_, folder, err := getFolder(c)
if err != nil {
return nil, err
}
return kernelcache.Extract(c.IPSW, filepath.Join(filepath.Clean(c.Output), folder))
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
_, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
return kernelcache.RemoteParse(zr, filepath.Join(filepath.Clean(c.Output), folder))
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
// DSC extracts the DSC file from an IPSW
func DSC(c *Config) ([]string, error) {
if len(c.IPSW) > 0 {
_, folder, err := getFolder(c)
if err != nil {
return nil, err
}
return dyld.Extract(c.IPSW, filepath.Join(filepath.Clean(c.Output), folder), c.Arches)
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
sysDMG, err := i.GetSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("only iOS16.x/macOS13.x supported: failed to get SystemOS DMG from remote zip metadata: %v", err)
}
if len(sysDMG) == 0 {
return nil, fmt.Errorf("only iOS16.x/macOS13.x supported: no SystemOS DMG found in remote zip metadata")
}
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_remote_dyld")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store SystemOS DMG: %v", err)
}
defer os.RemoveAll(tmpDIR)
if _, err := utils.SearchZip(zr.File, regexp.MustCompile(fmt.Sprintf("^%s$", sysDMG)), tmpDIR, c.Flatten, true); err != nil {
return nil, fmt.Errorf("failed to extract SystemOS DMG from remote IPSW: %v", err)
}
return dyld.ExtractFromDMG(i, filepath.Join(tmpDIR, sysDMG), filepath.Join(filepath.Clean(c.Output), folder), c.Arches)
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
// DMG extracts the DMG from an IPSW
func DMG(c *Config) ([]string, error) {
if len(c.IPSW) == 0 && len(c.URL) == 0 {
return nil, fmt.Errorf("no IPSW or URL provided")
}
var err error
var i *info.Info
var folder string
var zr *zip.Reader
if len(c.IPSW) > 0 {
i, folder, err = getFolder(c)
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Clean(c.IPSW))
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
defer f.Close()
finfo, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("failed to stat IPSW: %v", err)
}
zr, err = zip.NewReader(f, finfo.Size())
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err = getRemoteFolder(c)
if err != nil {
return nil, err
}
}
var dmgPath string
switch c.DmgType {
case "app":
dmgPath, err = i.GetAppOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find app DMG in IPSW: %v", err)
}
case "sys":
dmgPath, err = i.GetSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find system DMG in IPSW: %v", err)
}
case "fs":
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find filesystem DMG in IPSW: %v", err)
}
}
return utils.SearchZip(zr.File, regexp.MustCompile(dmgPath), filepath.Join(filepath.Clean(c.Output), folder), c.Flatten, c.Progress)
}
// Keybags extracts the keybags from an IPSW
func Keybags(c *Config) (string, error) {
if len(c.IPSW) == 0 && len(c.URL) == 0 {
return "", fmt.Errorf("no IPSW or URL provided")
}
var err error
var i *info.Info
var folder string
var zr *zip.Reader
if len(c.IPSW) > 0 {
i, folder, err = getFolder(c)
if err != nil {
return "", err
}
zr, err := zip.OpenReader(filepath.Clean(c.IPSW))
if err != nil {
return "", fmt.Errorf("failed to open IPSW: %v", err)
}
defer zr.Close()
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return "", fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err = getRemoteFolder(c)
if err != nil {
return "", err
}
}
kbags, err := img4.ParseZipKeyBags(zr.File, i, c.Pattern)
if err != nil {
return "", fmt.Errorf("failed to parse im4p kbags: %v", err)
}
out, err := json.Marshal(kbags)
if err != nil {
return "", fmt.Errorf("failed to marshal im4p kbags: %v", err)
}
fname := filepath.Join(filepath.Join(filepath.Clean(c.Output), folder), "kbags.json")
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
return "", fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
}
if err := os.WriteFile(fname, out, 0660); err != nil {
return "", fmt.Errorf("failed to write %s: %v", filepath.Join(filepath.Join(filepath.Clean(c.Output), folder), "kbags.json"), err)
}
return fname, nil
}
// Search searches for files matching a pattern in an IPSW
func Search(c *Config) ([]string, error) {
var artifacts []string
if len(c.Pattern) == 0 {
return nil, fmt.Errorf("no pattern provided")
}
re, err := regexp.Compile(c.Pattern)
if err != nil {
return nil, fmt.Errorf("failed to compile regexp '%s': %v", c.Pattern, err)
}
if len(c.IPSW) > 0 {
i, folder, err := getFolder(c)
if err != nil {
return nil, err
}
destPath := filepath.Join(filepath.Clean(c.Output), folder)
zr, err := zip.OpenReader(c.IPSW)
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
defer zr.Close()
out, err := utils.SearchZip(zr.File, re, destPath, c.Flatten, false)
if err != nil {
return nil, fmt.Errorf("failed to extract files matching pattern: %v", err)
}
artifacts = append(artifacts, out...)
if c.DMGs { // SEARCH THE DMGs
if appOS, err := i.GetAppOsDmg(); err == nil {
out, err := utils.ExtractFromDMG(c.IPSW, appOS, destPath, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from AppOS %s: %v", appOS, err)
}
artifacts = append(artifacts, out...)
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
out, err := utils.ExtractFromDMG(c.IPSW, systemOS, destPath, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from SystemOS %s: %v", systemOS, err)
}
artifacts = append(artifacts, out...)
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
out, err := utils.ExtractFromDMG(c.IPSW, fsOS, destPath, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from filesystem %s: %v", fsOS, err)
}
artifacts = append(artifacts, out...)
}
}
return artifacts, nil
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
if c.DMGs { // SEARCH THE DMGs
return nil, fmt.Errorf("searching DMGs in remote IPSW is not supported")
}
_, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
artifacts, err = utils.SearchZip(zr.File, re, filepath.Join(filepath.Clean(c.Output), folder), c.Flatten, true)
if err != nil {
return nil, fmt.Errorf("failed to extract files matching pattern '%s' in remote IPSW: %v", c.Pattern, err)
}
return artifacts, nil
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
+96
View File
@@ -0,0 +1,96 @@
// Package mount provides the mount command
package mount
import (
"archive/zip"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/info"
)
var dmgTypes = []string{"fs", "sys", "app"}
// Context is the mount context
type Context struct {
MountPoint string `json:"mount_point" binding:"required"`
DmgPath string `json:"dmg_path,omitempty"` // FIXME: required on linux
AlreadyMounted bool `json:"already_mounted,omitempty"`
}
// Unmount will unmount a DMG and remove the DMG source file
func (c Context) Unmount() error {
if info, err := utils.MountInfo(); err == nil { // darwin only
if image := info.Mount(c.MountPoint); image != nil {
c.DmgPath = filepath.Clean(image.ImagePath)
}
}
if err := utils.Retry(3, 2*time.Second, func() error {
return utils.Unmount(c.MountPoint, false)
}); err != nil {
return fmt.Errorf("failed to unmount %s at %s: %v", c.DmgPath, c.MountPoint, err)
}
// TODO: check if DmgPath is safe before removing (should be in /tmp and should be src of mount)
return os.Remove(filepath.Clean(c.DmgPath))
}
// DmgInIPSW will mount a DMG from an IPSW
func DmgInIPSW(path, typ string) (*Context, error) {
ipswPath := filepath.Clean(path)
i, err := info.Parse(ipswPath)
if err != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
var dmgPath string
switch typ {
case "fs":
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get filesystem DMG: %v", err)
}
case "sys":
dmgPath, err = i.GetSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get SystemOS DMG: %v", err)
}
case "app":
dmgPath, err = i.GetAppOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get AppOS DMG: %v", err)
}
default:
return nil, fmt.Errorf("invalid subcommand: %s; must be one of: '%s'", typ, strings.Join(dmgTypes, "', '"))
}
extractedDMG := filepath.Join(os.TempDir(), dmgPath)
if _, err := os.Stat(extractedDMG); os.IsNotExist(err) {
dmgs, err := utils.Unzip(ipswPath, os.TempDir(), func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
return nil, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return nil, fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
}
mp, am, err := utils.MountFS(extractedDMG)
if err != nil {
return nil, fmt.Errorf("failed to mount %s: %v", extractedDMG, err)
}
return &Context{
DmgPath: extractedDMG,
MountPoint: mp,
AlreadyMounted: am,
}, nil
}
+69
View File
@@ -0,0 +1,69 @@
// Package config is used to load the configuration file
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
)
type daemon struct {
Host string `json:"host"`
Port int `json:"port"`
Socket string `json:"socket"`
Debug bool `json:"debug"`
}
type database struct {
Name string `json:"database"`
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"password"`
SSLMode string `json:"sslmode"`
}
// Config is the configuration struct
type Config struct {
Daemon daemon `json:"daemon"`
Database database `json:"database"`
}
func (c *Config) verify() error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("config: failed to get user home directory: %v", err)
}
if c.Daemon.Host == "" && c.Daemon.Port == 0 && c.Daemon.Socket == "" {
// c.Daemon.Socket = "/var/run/ipsw.sock"
c.Daemon.Socket = filepath.Join(home, ".config", "ipsw", "ipsw.sock")
} else if c.Daemon.Host != "" && c.Daemon.Socket != "" {
return fmt.Errorf("config: host and socket cannot be set at the same time")
} else if c.Daemon.Host != "" && c.Daemon.Port == 0 {
return fmt.Errorf("config: port must be set if host is set")
} else if c.Daemon.Host == "" && c.Daemon.Port != 0 {
c.Daemon.Host = "localhost"
} else if strings.HasPrefix(c.Daemon.Socket, "~/") {
c.Daemon.Socket = filepath.Join(home, c.Daemon.Socket[2:]) // TODO: is this bad practice?
}
return nil
}
// LoadConfig loads the configuration file
func LoadConfig() (*Config, error) {
var c *Config
if err := viper.Unmarshal(&c); err != nil {
return nil, fmt.Errorf("config: failed to unmarshal: %v", err)
}
if err := c.verify(); err != nil {
return nil, fmt.Errorf("config: failed to verify: %v", err)
}
return c, nil
}
+49
View File
@@ -0,0 +1,49 @@
// Package daemon provides the daemon interface and implementation.
package daemon
import (
"github.com/blacktop/ipsw/api/server"
"github.com/blacktop/ipsw/internal/config"
"github.com/gin-gonic/gin"
)
// Daemon is the interface that describes an ipsw daemon.
type Daemon interface {
// Start starts the daemon.
Start() error
// Stop stops the daemon.
Stop() error
}
type daemon struct {
server *server.Server
conf *config.Config
}
// NewDaemon creates a new daemon.
func NewDaemon() Daemon {
return &daemon{}
}
func (d *daemon) Start() (err error) {
d.conf, err = config.LoadConfig()
if err != nil {
return err
}
if d.conf.Daemon.Debug {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
d.server = server.NewServer(&server.Config{
Host: d.conf.Daemon.Host,
Port: d.conf.Daemon.Port,
Socket: d.conf.Daemon.Socket,
Debug: d.conf.Daemon.Debug,
})
return d.server.Start()
}
func (d *daemon) Stop() error {
return d.server.Stop()
}
+30
View File
@@ -0,0 +1,30 @@
// Package db provides a database interface and implementations.
package db
import "github.com/blacktop/ipsw/internal/models"
// Database is the interface that wraps the basic database operations.
type Database interface {
// Connect connects to the database.
Connect() error
// Create creates a new entry in the database.
// It returns ErrAlreadyExists if the key already exists.
Create(i *models.IPSW) error
// Get returns the value for the given key.
// It returns ErrNotFound if the key does not exist.
Get(key uint) (*models.IPSW, error)
// Set sets the value for the given key.
// It overwrites any previous value for that key.
Set(key uint, value *models.IPSW) error
// Delete removes the given key.
// It returns ErrNotFound if the key does not exist.
Delete(key uint) error
// Close closes the database.
// It returns ErrClosed if the database is already closed.
Close() error
}
+85
View File
@@ -0,0 +1,85 @@
package db
import (
"encoding/gob"
"os"
"github.com/blacktop/ipsw/internal/models"
"github.com/pkg/errors"
)
// Memory is a database that stores data in memory.
type Memory struct {
IPSWs map[uint]*models.IPSW
Path string
}
// NewInMemory creates a new in-memory database.
func NewInMemory(path string) Database {
return Memory{
IPSWs: make(map[uint]*models.IPSW),
Path: path,
}
}
// Connect connects to the database.
func (m Memory) Connect() error {
f, err := os.Open(m.Path)
if err != nil {
return err
}
defer f.Close()
return gob.NewDecoder(f).Decode(&m.IPSWs)
}
// Create creates a new entry in the database.
// It returns ErrAlreadyExists if the key already exists.
func (m Memory) Create(i *models.IPSW) error {
m.IPSWs[i.ID] = i
return nil
}
// Get returns the value for the given key.
// It returns ErrNotFound if the key does not exist.
func (m Memory) Get(id uint) (*models.IPSW, error) {
pet, exists := m.IPSWs[id]
if !exists {
return nil, errors.Errorf("no pet found with id: %s", id)
}
return pet, nil
}
// Set sets the value for the given key.
// It overwrites any previous value for that key.
func (m Memory) Set(key uint, value *models.IPSW) error {
m.IPSWs[key] = value
return nil
}
func (m Memory) List(version string) ([]*models.IPSW, error) {
ipsws := []*models.IPSW{}
for _, p := range m.IPSWs {
if p.Version == version {
ipsws = append(ipsws, p)
}
}
return ipsws, nil
}
// Delete removes the given key.
// It returns ErrNotFound if the key does not exist.
func (m Memory) Delete(id uint) error {
delete(m.IPSWs, id)
return nil
}
// Close closes the database.
// It returns ErrClosed if the database is already closed.
func (m Memory) Close() error {
f, err := os.Open(m.Path)
if err != nil {
return err
}
defer f.Close()
return gob.NewEncoder(f).Encode(m.IPSWs)
}
+83
View File
@@ -0,0 +1,83 @@
package db
import (
"fmt"
"github.com/blacktop/ipsw/internal/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// Postgres is a database that stores data in a Postgres database.
type Postgres struct {
URL string
Host string
Port string
User string
Password string
Database string
db *gorm.DB
}
// NewPostgres creates a new Postgres database.
func NewPostgres(host, port, user, password, database string) Database {
return Postgres{
Host: host,
Port: port,
User: user,
Password: password,
Database: database,
}
}
// Connect connects to the database.
func (p Postgres) Connect() (err error) {
p.db, err = gorm.Open(postgres.Open(fmt.Sprintf(
"host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
p.Host, p.Port, p.User, p.Database, p.Password,
)), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to connect postgres database: %w", err)
}
return p.db.AutoMigrate(&models.IPSW{})
}
// Create creates a new entry in the database.
// It returns ErrAlreadyExists if the key already exists.
func (p Postgres) Create(i *models.IPSW) error {
p.db.Create(i)
return nil
}
// Get returns the value for the given key.
// It returns ErrNotFound if the key does not exist.
func (p Postgres) Get(key uint) (*models.IPSW, error) {
i := &models.IPSW{}
p.db.First(&i, key)
return i, nil
}
// Set sets the value for the given key.
// It overwrites any previous value for that key.
func (p Postgres) Set(key uint, value *models.IPSW) error {
p.db.Save(value)
return nil
}
// Delete removes the given key.
// It returns ErrNotFound if the key does not exist.
func (p Postgres) Delete(key uint) error {
p.db.Delete(&models.IPSW{}, key)
return nil
}
// Close closes the database.
// It returns ErrClosed if the database is already closed.
func (p Postgres) Close() error {
db, err := p.db.DB()
if err != nil {
return err
}
return db.Close()
}
+71
View File
@@ -0,0 +1,71 @@
package db
import (
"fmt"
"github.com/blacktop/ipsw/internal/models"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
// Sqlite is a database that stores data in a sqlite database.
type Sqlite struct {
URL string
db *gorm.DB
}
// NewSqlite creates a new Sqlite database.
func NewSqlite(path string) Database {
return Postgres{
URL: path,
}
}
// Connect connects to the database.
func (s *Sqlite) Connect() (err error) {
s.db, err = gorm.Open(sqlite.Open(s.URL), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to connect sqlite database: %w", err)
}
return s.db.AutoMigrate(&models.IPSW{})
}
// Create creates a new entry in the database.
// It returns ErrAlreadyExists if the key already exists.
func (s *Sqlite) Create(i *models.IPSW) error {
s.db.Create(i)
return nil
}
// Get returns the value for the given key.
// It returns ErrNotFound if the key does not exist.
func (s *Sqlite) Get(key uint) (*models.IPSW, error) {
i := &models.IPSW{}
s.db.First(&i, key)
return i, nil
}
// Set sets the value for the given key.
// It overwrites any previous value for that key.
func (s *Sqlite) Set(key uint, value *models.IPSW) error {
s.db.Save(value)
return nil
}
// Delete removes the given key.
// It returns ErrNotFound if the key does not exist.
func (s *Sqlite) Delete(key uint) error {
s.db.Delete(&models.IPSW{}, key)
return nil
}
// Close closes the database.
// It returns ErrClosed if the database is already closed.
func (s *Sqlite) Close() error {
db, err := s.db.DB()
if err != nil {
return err
}
return db.Close()
}
+37 -33
View File
@@ -269,10 +269,10 @@ func (d *Diff) unmountSystemOsDMGs() error {
}
func (d *Diff) parseKernelcache() error {
if err := kernelcache.Extract(d.Old.IPSWPath, d.Old.Folder); err != nil {
if _, err := kernelcache.Extract(d.Old.IPSWPath, d.Old.Folder); err != nil {
return fmt.Errorf("failed to extract kernelcaches from 'Old' IPSW: %v", err)
}
if err := kernelcache.Extract(d.New.IPSWPath, d.New.Folder); err != nil {
if _, err := kernelcache.Extract(d.New.IPSWPath, d.New.Folder); err != nil {
return fmt.Errorf("failed to extract kernelcaches from 'New' IPSW: %v", err)
}
@@ -507,45 +507,49 @@ func (d *Diff) parseEntitlements() (string, error) {
}
func (d *Diff) parseLaunchdPlists() error {
oldFsDMG, err := d.Old.Info.GetFileSystemOsDmg()
if err != nil {
return fmt.Errorf("failed to get 'Old' File System DMG: %v", err)
}
if err := utils.ExtractFromDMG(d.Old.IPSWPath, oldFsDMG, filepath.Join(d.tmpDir, "old"), regexp.MustCompile(`.*/sbin/launchd$`)); err != nil {
return err
}
m, err := macho.Open(filepath.Join(d.tmpDir, "old/sbin/launchd"))
if err != nil {
return err
}
defer m.Close()
getConfig := func(ctx Context) (string, error) {
fsDMG, err := ctx.Info.GetFileSystemOsDmg()
if err != nil {
return "", fmt.Errorf("failed to get File System DMG: %v", err)
}
oldData, err := m.Section("__TEXT", "__config").Data()
if err != nil {
return err
extracted, err := utils.ExtractFromDMG(ctx.IPSWPath, fsDMG, d.tmpDir, regexp.MustCompile(`.*/sbin/launchd$`))
if err != nil {
return "", fmt.Errorf("failed to extract launchd from %s: %v", fsDMG, err)
}
if len(extracted) == 0 {
return "", fmt.Errorf("failed to extract launchd from %s: no files extracted", fsDMG)
} else if len(extracted) > 1 {
return "", fmt.Errorf("failed to extract launchd from %s: too many files extracted", fsDMG)
}
defer os.Remove(filepath.Clean(extracted[0]))
m, err := macho.Open(extracted[0])
if err != nil {
return "", fmt.Errorf("failed to open launchd: %v", err)
}
defer m.Close()
data, err := m.Section("__TEXT", "__config").Data()
if err != nil {
return "", fmt.Errorf("failed to get launchd config: %v", err)
}
return string(data), nil
}
newFsDMG, err := d.New.Info.GetFileSystemOsDmg()
oldConfig, err := getConfig(d.Old)
if err != nil {
return fmt.Errorf("failed to get 'New' File System DMG: %v", err)
return fmt.Errorf("diff: parseLaunchdPlists: failed to get 'Old' config: %v", err)
}
if err := utils.ExtractFromDMG(d.New.IPSWPath, newFsDMG, filepath.Join(d.tmpDir, "new"), regexp.MustCompile(`.*/sbin/launchd$`)); err != nil {
return err
}
m2, err := macho.Open(filepath.Join(d.tmpDir, "new/sbin/launchd"))
newConfig, err := getConfig(d.New)
if err != nil {
return err
return fmt.Errorf("diff: parseLaunchdPlists: failed to get 'New' config: %v", err)
}
defer m2.Close()
newData, err := m.Section("__TEXT", "__config").Data()
if err != nil {
return err
}
out, err := utils.GitDiff(
string(oldData)+"\n",
string(newData)+"\n",
string(oldConfig)+"\n",
string(newConfig)+"\n",
&utils.GitDiffConfig{Color: false, Tool: "git"})
if err != nil {
return err
+45
View File
@@ -0,0 +1,45 @@
// Package models contains the models for the database.
package models
import (
"os"
"time"
"gorm.io/gorm"
)
// IPSW is the model for an IPSW file.
type IPSW struct {
gorm.Model // adds ID, created_at etc.
Version string `json:"version"`
BuildID string `json:"buildid"`
Devices []string `json:"devices"`
Date time.Time `json:"date"`
Kernels []Kernelcache `json:"kernels"`
DSCs []DyldSharedCache `json:"dscs"`
Files FileSystem `json:"files"`
}
// Kernelcache is the model for a kernelcache.
type Kernelcache struct {
gorm.Model
Version string `json:"version"`
}
// DyldSharedCache is the model for a dyld_shared_cache.
type DyldSharedCache struct {
gorm.Model
Version string `json:"version"`
Header string `json:"header"`
}
type FileSystem struct {
gorm.Model
Files []File `json:"files"`
}
type File struct {
gorm.Model
Name string `json:"name"`
Info os.FileInfo `json:"info"`
}
+1
View File
@@ -0,0 +1 @@
package store
+31
View File
@@ -0,0 +1,31 @@
package store
type Local struct {
Folder string
}
func NewLocal(folder string) Store {
return Local{
Folder: folder,
}
}
func (l Local) Connect() error {
panic("not implemented") // TODO: Implement
}
func (l Local) Put(key []byte, value []byte) error {
panic("not implemented") // TODO: Implement
}
func (l Local) Get(key []byte) ([]byte, error) {
panic("not implemented") // TODO: Implement
}
func (l Local) Delete(key []byte) error {
panic("not implemented") // TODO: Implement
}
func (l Local) Close() error {
panic("not implemented") // TODO: Implement
}
+1
View File
@@ -0,0 +1 @@
package store
+9
View File
@@ -0,0 +1,9 @@
package store
type Store interface {
Connect() error
Put(key, value []byte) error
Get(key []byte) ([]byte, error)
Delete(key []byte) error
Close() error
}
+20 -7
View File
@@ -507,6 +507,17 @@ type HdiUtilInfo struct {
Images []image `plist:"images,omitempty" xml:"images,omitempty"`
}
func (i HdiUtilInfo) Mount(mount string) *image {
for _, img := range i.Images {
for _, entry := range img.SystemEntities {
if strings.Contains(entry.MountPoint, mount) {
return &img
}
}
}
return nil
}
func MountInfo() (*HdiUtilInfo, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("hdiutil", "info", "-plist")
@@ -523,25 +534,25 @@ func MountInfo() (*HdiUtilInfo, error) {
return nil, fmt.Errorf("only supported on macOS")
}
func ExtractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp) error {
func ExtractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp) ([]string, error) {
// check if filesystem DMG already exists (due to previous mount command)
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
dmgs, err := Unzip(ipswPath, "", func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
return fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
return nil, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return fmt.Errorf("failed to find %s in IPSW", dmgPath)
return nil, fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
defer os.Remove(dmgs[0])
defer os.Remove(filepath.Clean(dmgs[0]))
}
Indent(log.Info, 2)(fmt.Sprintf("Mounting DMG %s", dmgPath))
mountPoint, alreadyMounted, err := MountFS(dmgPath)
if err != nil {
return fmt.Errorf("failed to IPSW FS dmg: %v", err)
return nil, fmt.Errorf("failed to IPSW FS dmg: %v", err)
}
if alreadyMounted {
Indent(log.Debug, 3)(fmt.Sprintf("%s already mounted", dmgPath))
@@ -556,6 +567,7 @@ func ExtractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp)
}()
}
var artifacts []string
// extract files that match regex pattern
if err := filepath.Walk(mountPoint, func(path string, info os.FileInfo, err error) error {
if err != nil {
@@ -574,13 +586,14 @@ func ExtractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp)
if err := Copy(path, fname); err != nil {
return fmt.Errorf("failed to extract %s: %v", fname, err)
}
artifacts = append(artifacts, fname)
}
return nil
}); err != nil {
return fmt.Errorf("failed to extract File System files from IPSW: %v", err)
return nil, fmt.Errorf("failed to extract File System files from IPSW: %v", err)
}
return nil
return artifacts, nil
}
func PkgUtilExpand(src, dst string) (string, error) {
+11 -9
View File
@@ -179,13 +179,14 @@ func Verify(sha1sum, name string) (bool, error) {
return match, nil
}
// RemoteUnzip unzips a remote file from zip (like partialzip)
func RemoteUnzip(files []*zip.File, pattern *regexp.Regexp, folder string, flat, progress bool) error {
// 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 fmt.Errorf("failed to get current working directory: %v", err)
return nil, fmt.Errorf("failed to get current working directory: %v", err)
}
found := false
@@ -202,21 +203,21 @@ func RemoteUnzip(files []*zip.File, pattern *regexp.Regexp, folder string, flat,
}
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
return fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
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 fmt.Errorf("error opening remote zipped file %s: %v", f.Name, err)
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 = int64(f.UncompressedSize64)
var total = int64(f.UncompressedSize64)
p = mpb.New(
mpb.WithWidth(60),
mpb.WithRefreshRate(180*time.Millisecond),
@@ -242,7 +243,7 @@ func RemoteUnzip(files []*zip.File, pattern *regexp.Regexp, folder string, flat,
Indent(log.Info, 2)(fmt.Sprintf("Extracting %s", strings.TrimPrefix(fname, cwd)))
out, err := os.Create(fname)
if err != nil {
return fmt.Errorf("error creating remote unzipped file destination %s: %v", fname, err)
return nil, fmt.Errorf("error creating remote unzipped file destination %s: %v", fname, err)
}
defer out.Close()
@@ -253,6 +254,7 @@ func RemoteUnzip(files []*zip.File, pattern *regexp.Regexp, folder string, flat,
p.Wait()
}
artifacts = append(artifacts, fname)
} else {
Indent(log.Warn, 2)(fmt.Sprintf("%s already exists", fname))
}
@@ -260,10 +262,10 @@ func RemoteUnzip(files []*zip.File, pattern *regexp.Regexp, folder string, flat,
}
if !found {
return fmt.Errorf("no files found matching %s", pattern.String())
return nil, fmt.Errorf("no files found matching %s", pattern.String())
}
return nil
return artifacts, nil
}
// Unzip - https://stackoverflow.com/a/24792688
+21 -19
View File
@@ -46,13 +46,13 @@ func GetDscPathsInMount(mountPoint string) ([]string, error) {
return matches, nil
}
func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) error {
func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) ([]string, error) {
utils.Indent(log.Info, 2)(fmt.Sprintf("Mounting DMG %s", dmgPath))
var alreadyMounted bool
mountPoint, alreadyMounted, err := utils.MountFS(dmgPath)
if err != nil {
return fmt.Errorf("failed to IPSW FS dmg: %v", err)
return nil, fmt.Errorf("failed to IPSW FS dmg: %v", err)
}
if alreadyMounted {
utils.Indent(log.Debug, 3)(fmt.Sprintf("%s already mounted", dmgPath))
@@ -69,23 +69,23 @@ func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) err
if runtime.GOOS == "darwin" {
if err := os.MkdirAll(destPath, 0750); err != nil {
return fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
return nil, fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
}
} else {
if _, ok := os.LookupEnv("IPSW_IN_DOCKER"); ok {
if err := os.MkdirAll(filepath.Join("/data", destPath), 0750); err != nil {
return fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
return nil, fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
}
} else {
if err := os.MkdirAll(destPath, 0750); err != nil {
return fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
return nil, fmt.Errorf("failed to create destination directory %s: %v", destPath, err)
}
}
}
matches, err := GetDscPathsInMount(mountPoint)
if err != nil {
return err
return nil, err
}
if utils.StrSliceContains(i.Plists.BuildManifest.SupportedProductTypes, "mac") { // Is macOS IPSW
@@ -99,9 +99,9 @@ func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) err
if err := survey.AskOne(prompt, &selMatches); err != nil {
if err == terminal.InterruptErr {
log.Warn("Exiting...")
return nil
return nil, nil
}
return err
return nil, err
}
matches = selMatches
} else {
@@ -114,44 +114,46 @@ func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) err
}
if len(filtered) == 0 {
return fmt.Errorf("no dyld_shared_cache files found matching the specified archs: %v", arches)
return nil, fmt.Errorf("no dyld_shared_cache files found matching the specified archs: %v", arches)
}
matches = filtered
}
}
if len(matches) == 0 {
return fmt.Errorf("failed to find dyld_shared_cache(s) in DMG: %s", dmgPath)
return nil, fmt.Errorf("failed to find dyld_shared_cache(s) in DMG: %s", dmgPath)
}
var artifacts []string
for _, match := range matches {
dyldDest := filepath.Join(destPath, filepath.Base(match))
utils.Indent(log.Info, 3)(fmt.Sprintf("Extracting %s to %s", filepath.Base(match), dyldDest))
if err := utils.Copy(match, dyldDest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %v", match, dyldDest, err)
return nil, fmt.Errorf("failed to copy %s to %s: %v", match, dyldDest, err)
}
artifacts = append(artifacts, dyldDest)
}
return nil
return artifacts, nil
}
// Extract extracts dyld_shared_cache from IPSW
func Extract(ipsw, destPath string, arches []string) error {
func Extract(ipsw, destPath string, arches []string) ([]string, error) {
if runtime.GOOS == "windows" {
return fmt.Errorf("dyld extraction is not supported on Windows (see github.com/blacktop/go-apfs)")
return nil, fmt.Errorf("dyld extraction is not supported on Windows (see github.com/blacktop/go-apfs)")
}
i, err := info.Parse(ipsw)
if err != nil {
return fmt.Errorf("failed to parse IPSW: %v", err)
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
dmgPath, err := i.GetSystemOsDmg()
if err != nil {
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return fmt.Errorf("failed to get File System DMG: %v", err)
return nil, fmt.Errorf("failed to get File System DMG: %v", err)
}
}
@@ -161,10 +163,10 @@ func Extract(ipsw, destPath string, arches []string) error {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
return fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
return nil, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return fmt.Errorf("File System %s NOT found in IPSW", dmgPath)
return nil, fmt.Errorf("File System %s NOT found in IPSW", dmgPath)
}
defer os.Remove(dmgs[0])
}
@@ -234,7 +236,7 @@ func ExtractFromRemoteCryptex(zr *zip.Reader, destPath string, arches []string)
return fmt.Errorf("failed to parse info from cryptex-system-arm64e: %v", err)
}
if err := ExtractFromDMG(i, out.Name(), destPath, arches); err != nil {
if _, err := ExtractFromDMG(i, out.Name(), destPath, arches); err != nil {
return fmt.Errorf("failed to extract dyld_shared_cache from cryptex-system-arm64e: %v", err)
}
+26 -23
View File
@@ -94,51 +94,52 @@ func ParseImg4Data(data []byte) (*CompressedCache, error) {
}
// Extract extracts and decompresses a kernelcache from ipsw
func Extract(ipsw, destPath string) error {
log.Debug("Extracting Kernelcache from IPSW")
func Extract(ipsw, destPath string) ([]string, error) {
kcaches, err := utils.Unzip(ipsw, "", func(f *zip.File) bool {
return strings.Contains(f.Name, "kernelcache")
})
if err != nil {
return errors.Wrap(err, "failed extract kernelcache from ipsw")
return nil, fmt.Errorf("failed to unzip kernelcache: %v", err)
}
i, err := info.Parse(ipsw)
if err != nil {
return fmt.Errorf("failed to parse ipsw info: %v", err)
return nil, fmt.Errorf("failed to parse ipsw info: %v", err)
}
var artifacts []string
for _, kcache := range kcaches {
fname := i.GetKernelCacheFileName(kcache)
// fname := fmt.Sprintf("%s.%s", strings.TrimSuffix(kcache, filepath.Ext(kcache)), strings.Join(i.GetDevicesForKernelCache(kcache), "_"))
fname = filepath.Join(destPath, fname)
fname = filepath.Clean(fname)
content, err := os.ReadFile(kcache)
if err != nil {
return errors.Wrap(err, "failed to read Kernelcache")
return nil, errors.Wrap(err, "failed to read Kernelcache")
}
kc, err := ParseImg4Data(content)
if err != nil {
return errors.Wrap(err, "failed parse compressed kernelcache Img4")
return nil, fmt.Errorf("failed to parse im4p kernelcache data: %v", err)
}
dec, err := DecompressData(kc)
if err != nil {
return errors.Wrap(err, "failed to decompress kernelcache")
return nil, fmt.Errorf("failed to decompress kernelcache data: %v", err)
}
os.Mkdir(destPath, 0750)
err = os.WriteFile(fname, dec, 0660)
if err != nil {
return errors.Wrap(err, "failed to write kernelcache")
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
return nil, fmt.Errorf("failed to create output directory: %v", err)
}
if err := os.WriteFile(fname, dec, 0660); err != nil {
return nil, fmt.Errorf("failed to write decompressed kernelcache: %v", err)
}
utils.Indent(log.Info, 2)("Created " + fname)
os.Remove(kcache)
artifacts = append(artifacts, fname)
}
return nil
return artifacts, nil
}
// Decompress decompresses a compressed kernelcache
@@ -295,11 +296,12 @@ func DecompressData(cc *CompressedCache) ([]byte, error) {
}
// RemoteParse parses plist files in a remote ipsw file
func RemoteParse(zr *zip.Reader, destPath string) error {
func RemoteParse(zr *zip.Reader, destPath string) ([]string, error) {
var artifacts []string
i, err := info.ParseZipFiles(zr.File)
if err != nil {
return err
return nil, err
}
for _, f := range zr.File {
@@ -309,35 +311,36 @@ func RemoteParse(zr *zip.Reader, destPath string) error {
kdata := make([]byte, f.UncompressedSize64)
rc, err := f.Open()
if err != nil {
return fmt.Errorf("failed to open kernelcache %s in zip: %v", f.Name, err)
return nil, fmt.Errorf("failed to open kernelcache %s in zip: %v", f.Name, err)
}
io.ReadFull(rc, kdata)
rc.Close()
kcomp, err := ParseImg4Data(kdata)
if err != nil {
return fmt.Errorf("failed to parse kernelcache im4p %s: %v", f.Name, err)
return nil, fmt.Errorf("failed to parse kernelcache im4p %s: %v", f.Name, err)
}
dec, err := DecompressData(kcomp)
if err != nil {
return fmt.Errorf("failed to decompress kernelcache %s: %v", f.Name, err)
return nil, fmt.Errorf("failed to decompress kernelcache %s: %v", f.Name, err)
}
if err := os.MkdirAll(filepath.Dir(fname), 0750); err != nil {
return fmt.Errorf("failed to create destination directory: %v", err)
return nil, fmt.Errorf("failed to create destination directory: %v", err)
}
utils.Indent(log.Info, 2)(fmt.Sprintf("Writing %s", fname))
if err := os.WriteFile(fname, dec, 0660); err != nil {
return fmt.Errorf("failed to write kernelcache %s: %v", fname, err)
return nil, fmt.Errorf("failed to write kernelcache %s: %v", fname, err)
}
artifacts = append(artifacts, fname)
} else {
log.Warnf("kernelcache already exists: %s", fname)
}
}
}
return nil
return artifacts, nil
}
// Parse parses the compressed kernelcache Img4 data
+36 -27
View File
@@ -147,22 +147,8 @@ func GetKextInfos(m *macho.File) ([]KmodInfoT, error) {
return nil, fmt.Errorf("section __PRELINK_INFO.__kmod_start not found")
}
// KextList lists all the kernel extensions in the kernelcache
func KextList(kernel string, diffable bool) ([]string, error) {
var out []string
m, err := macho.Open(kernel)
if err != nil {
return nil, err
}
defer m.Close()
kextStartAdddrs, err := GetKextStartVMAddrs(m)
if err != nil {
log.Debugf("failed to get kext start addresses: %v", err)
}
if infoSec := m.Section("__PRELINK_INFO", "__info"); infoSec != nil {
func GetKexts(kernel *macho.File) ([]CFBundle, error) {
if infoSec := kernel.Section("__PRELINK_INFO", "__info"); infoSec != nil {
data, err := infoSec.Data()
if err != nil {
@@ -175,18 +161,41 @@ func KextList(kernel string, diffable bool) ([]string, error) {
if err != nil {
return nil, fmt.Errorf("failed to decode __PRELINK_INFO.__info section: %v", err)
}
return prelink.PrelinkInfoDictionary, nil
}
return nil, fmt.Errorf("section __PRELINK_INFO.__info not found")
}
if diffable {
for _, bundle := range prelink.PrelinkInfoDictionary {
out = append(out, fmt.Sprintf("%s (%s)", bundle.ID, bundle.Version))
}
} else {
for _, bundle := range prelink.PrelinkInfoDictionary {
if !bundle.OSKernelResource && len(kextStartAdddrs) > 0 {
out = append(out, fmt.Sprintf("%#x: %s (%s)", kextStartAdddrs[bundle.ModuleIndex]|tagPtrMask, bundle.ID, bundle.Version))
} else {
out = append(out, fmt.Sprintf("%#x: %s (%s)", bundle.ExecutableLoadAddr, bundle.ID, bundle.Version))
}
// KextList lists all the kernel extensions in the kernelcache
func KextList(kernelPath string, diffable bool) ([]string, error) {
var out []string
m, err := macho.Open(kernelPath)
if err != nil {
return nil, err
}
defer m.Close()
bundles, err := GetKexts(m)
if err != nil {
return nil, err
}
kextStartAdddrs, err := GetKextStartVMAddrs(m)
if err != nil {
log.Debugf("failed to get kext start addresses: %v", err)
}
if diffable {
for _, bundle := range bundles {
out = append(out, fmt.Sprintf("%s (%s)", bundle.ID, bundle.Version))
}
} else {
for _, bundle := range bundles {
if !bundle.OSKernelResource && len(kextStartAdddrs) > 0 {
out = append(out, fmt.Sprintf("%#x: %s (%s)", kextStartAdddrs[bundle.ModuleIndex]|tagPtrMask, bundle.ID, bundle.Version))
} else {
out = append(out, fmt.Sprintf("%#x: %s (%s)", bundle.ExecutableLoadAddr, bundle.ID, bundle.Version))
}
}
}
+9 -9
View File
@@ -81,18 +81,18 @@ type sysMaster struct {
}
type sysent struct {
Call uint64 // implementing function
Munge uint64 // system call arguments munger for 32-bit process
ReturnType returnType // system call return types
NArg int16 // number of args
ArgBytes uint16 // Total size of arguments in bytes for 32-bit system calls
Call uint64 `json:"call,omitempty"` // implementing function
Munge uint64 `json:"munge,omitempty"` // system call arguments munger for 32-bit process
ReturnType returnType `json:"return_type,omitempty"` // system call return types
NArg int16 `json:"n_arg,omitempty"` // number of args
ArgBytes uint16 `json:"arg_bytes,omitempty"` // Total size of arguments in bytes for 32-bit system calls
}
type Sysent struct {
Name string
Number int
Proto string
New bool
Name string `json:"name,omitempty"`
Number int `json:"number,omitempty"`
Proto string `json:"proto,omitempty"`
New bool `json:"new,omitempty"`
sysent
}
+1 -1
View File
@@ -405,7 +405,7 @@ func Extract(otaZIP, extractPattern, folder string) error {
// check for matches in the OTA zip
utils.Indent(log.Info, 2)("Searching in OTA zip files...")
if err := utils.RemoteUnzip(zr.File, re, folder, false, false); err != nil {
if _, err := utils.SearchZip(zr.File, re, folder, false, false); err != nil {
log.Errorf("failed to find in OTA zip: %v", err)
}
+1 -1
View File
@@ -151,7 +151,7 @@ func Parse(path string) (*Plists, error) {
zr, err := zip.OpenReader(path)
if err != nil {
return nil, fmt.Errorf("failed to open ipsw zip: %s", err)
return nil, fmt.Errorf("failed to open IPSW zip: %s", err)
}
defer zr.Close()
+3 -5
View File
@@ -6,10 +6,9 @@ import (
"fmt"
"os"
"github.com/jinzhu/gorm"
// importing the sqlite dialects
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/glebarez/sqlite"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func uniqueDevices(d []Device) []Device {
@@ -35,11 +34,10 @@ func ReadDeviceTraitsDB() ([]Device, error) {
for _, releaseType := range []string{"", "-beta"} {
dbFile := fmt.Sprintf("/Applications/Xcode%s.app/Contents/Developer/Platforms/%s.platform/usr/standalone/device_traits.db", releaseType, osType)
if _, err := os.Stat(dbFile); err == nil {
db, err := gorm.Open("sqlite3", dbFile)
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{})
if err != nil {
return nil, errors.Wrapf(err, "unable to open database: %s", dbFile)
}
defer db.Close()
var devices []Device
db.Preload("DeviceTrait").Find(&devices)
+28
View File
@@ -58,6 +58,28 @@ const config = {
},
}),
],
[
"redocusaurus",
/** @type {import('redocusaurus').PresetEntry} */
({
// Plugin Options for loading OpenAPI files
specs: [
{
spec: "api/swagger.json",
route: "/api",
layout: {
title: "ipsw API",
noFooter: true,
},
},
],
// Theme Options for modifying how redoc renders them
theme: {
// Change with your site colors
primaryColor: "#503B9F",
},
}),
],
],
// plugins: [require.resolve("@cmfcmf/docusaurus-search-local")],
themeConfig:
@@ -113,6 +135,12 @@ const config = {
sidebarId: "cli",
label: "CLI",
},
{
label: "API",
to: "/api",
position: "left",
sidebarId: "api",
},
// Right
{
href: "https://github.com/blacktop/ipsw",
+1032 -1
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -25,7 +25,8 @@
"prism-react-renderer": "^1.3.5",
"prism-themes": "^1.9.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"redocusaurus": "^1.6.1"
},
"devDependencies": {
"@docusaurus/eslint-plugin": "2.4.0",
+18
View File
@@ -0,0 +1,18 @@
# NOTE: Only supports options marked as "Supported in Redoc CE"
# See https://redocly.com/docs/cli/configuration/ for more information.
extends:
- recommended
rules:
no-unused-components: error
theme:
# See https://redocly.com/docs/api-reference-docs/configuration/functionality/
openapi:
disableSearch: true
# See https://redocly.com/docs/api-reference-docs/configuration/theming/
theme:
colors:
primary:
main: "#503B9F"
+582
View File
@@ -0,0 +1,582 @@
{
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"title": "The ipsw API.",
"version": "v1.0"
},
"host": "localhost:8080",
"basePath": "/v1",
"paths": {
"/_ping": {
"get": {
"description": "This will return \"OK\" if the daemon is running.",
"tags": [
"Daemon"
],
"summary": "Ping",
"operationId": "getDaemonPing"
},
"head": {
"description": "This will return if 200 the daemon is running.",
"tags": [
"Daemon"
],
"summary": "Ping",
"operationId": "headDaemonPing"
}
},
"/device_list": {
"get": {
"description": "This will return JSON of all XCode devices.",
"produces": [
"application/json"
],
"tags": [
"DeviceList"
],
"summary": "List XCode Devices.",
"operationId": "getDeviceList"
}
},
"/download/ipsw/ios/latest/build": {
"get": {
"description": "Get latest iOS build.",
"tags": [
"Download"
],
"summary": "Latest iOS Build",
"operationId": "getDownloadLatestIPSWsBuild"
}
},
"/download/ipsw/ios/latest/version": {
"get": {
"description": "Get latest iOS version.",
"tags": [
"Download"
],
"summary": "Latest iOS Version",
"operationId": "getDownloadLatestIPSWsVersion"
}
},
"/dsc/imports": {
"get": {
"description": "Get list of dylibs that import a given dylib.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Imports",
"operationId": "getDscImports",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "dylib to search for",
"name": "dylib",
"in": "query",
"required": true
}
]
}
},
"/dsc/info": {
"get": {
"description": "Get info about a given DSC",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Info",
"operationId": "getDscInfo",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/dsc/macho": {
"get": {
"description": "Get MachO info for a given dylib in the DSC.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "MachO",
"operationId": "getDscMacho",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "dylib to search for",
"name": "dylib",
"in": "query",
"required": true
}
]
}
},
"/dsc/str": {
"get": {
"description": "Get strings in the DSC that match a given pattern.",
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Strings",
"operationId": "getDscStrings",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "regex to search for",
"name": "pattern",
"in": "query",
"required": true
}
]
}
},
"/dsc/symaddr": {
"get": {
"description": "Get symbols addresses in the DSC that match a given lookup JSON payload.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"DSC"
],
"summary": "Symbols",
"operationId": "getDscSymbols",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/extract/dmg": {
"get": {
"description": "Extract DMGs from an IPSW.",
"tags": [
"Extract"
],
"summary": "DMG",
"operationId": "getExtractDmg"
}
},
"/extract/dsc": {
"get": {
"description": "Extract dyld_shared_caches from an IPSW.",
"tags": [
"Extract"
],
"summary": "DSC",
"operationId": "getExtractDsc"
}
},
"/extract/kbag": {
"get": {
"description": "Extract KBAGs from an IPSW.",
"tags": [
"Extract"
],
"summary": "KBAG",
"operationId": "getExtractKbags"
}
},
"/extract/kernel": {
"get": {
"description": "Extract kernelcaches from an IPSW.",
"tags": [
"Extract"
],
"summary": "Kernel",
"operationId": "getExtractKernel"
}
},
"/extract/pattern": {
"get": {
"description": "Extract files from an IPSW that match a given pattern.",
"tags": [
"Extract"
],
"summary": "Pattern",
"operationId": "getExtractPattern"
}
},
"/idev/info": {
"get": {
"description": "Get info about USB connected devices.",
"tags": [
"USB"
],
"summary": "Info",
"operationId": "getIdevInfo"
}
},
"/info/ipsw": {
"get": {
"description": "Get IPSW info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "IPSW",
"operationId": "getIpswInfo",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/info/ipsw/remote": {
"get": {
"description": "Get remote IPSW info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "Remote IPSW",
"operationId": "getRemoteIpswInfo",
"parameters": [
{
"type": "string",
"description": "url to IPSW",
"name": "url",
"in": "query",
"required": true
},
{
"type": "string",
"description": "http proxy to use",
"name": "proxy",
"in": "query"
},
{
"type": "boolean",
"description": "ignore TLS errors",
"name": "insecure",
"in": "query"
}
]
}
},
"/info/ota": {
"get": {
"description": "Get OTA info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "OTA",
"operationId": "getOtaInfo",
"parameters": [
{
"type": "string",
"description": "path to OTA",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/info/ota/remote": {
"get": {
"description": "Get remote OTA info.",
"produces": [
"application/json"
],
"tags": [
"Info"
],
"summary": "Remote OTA",
"operationId": "getRemoteOtaInfo",
"parameters": [
{
"type": "string",
"description": "url to OTA",
"name": "url",
"in": "query",
"required": true
},
{
"type": "string",
"description": "http proxy to use",
"name": "proxy",
"in": "query"
},
{
"type": "boolean",
"description": "ignore TLS errors",
"name": "insecure",
"in": "query"
}
]
}
},
"/ipsw/fs/ents": {
"get": {
"description": "Get IPSW Filesystem DMG MachO entitlements.",
"produces": [
"application/json"
],
"tags": [
"IPSW"
],
"summary": "Entitlements",
"operationId": "getIpswFsEntitlements",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/ipsw/fs/files": {
"get": {
"description": "Get IPSW Filesystem DMG file listing.",
"produces": [
"application/json"
],
"tags": [
"IPSW"
],
"summary": "Files",
"operationId": "getIpswFsFiles",
"parameters": [
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/kexts": {
"get": {
"description": "Get kernelcache KEXTs info.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Kexts",
"operationId": "getKernelKexts",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/syscall": {
"get": {
"description": "Get kernelcache syscalls info.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Syscalls",
"operationId": "getKernelSyscalls",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/kernel/version": {
"get": {
"description": "Get kernelcache version.",
"produces": [
"application/json"
],
"tags": [
"Kernel"
],
"summary": "Version",
"operationId": "getKernelVersion",
"parameters": [
{
"type": "string",
"description": "path to kernelcache",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/macho/info": {
"get": {
"description": "Get MachO info.",
"produces": [
"application/json"
],
"tags": [
"MachO"
],
"summary": "Info",
"operationId": "getMachoInfo",
"parameters": [
{
"type": "string",
"description": "path to MachO",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "architecture to get info for in universal MachO",
"name": "arch",
"in": "query"
}
]
}
},
"/mount/{type}": {
"post": {
"description": "Mount a DMG inside a given IPSW.",
"produces": [
"application/json"
],
"tags": [
"Mount"
],
"summary": "Mount",
"operationId": "postMount",
"parameters": [
{
"type": "string",
"description": "type of DMG to mount",
"name": "type",
"in": "path",
"required": true
},
{
"type": "string",
"description": "path to IPSW",
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/unmount": {
"post": {
"description": "Unmount a previously mounted DMG.",
"produces": [
"application/json"
],
"tags": [
"Mount"
],
"summary": "Unmount",
"operationId": "postUnmount",
"parameters": [
{
"type": "string",
"description": "mount point of DMG",
"name": "mount_point",
"in": "path",
"required": true
},
{
"type": "string",
"description": "path to DMG",
"name": "dmg_path",
"in": "query"
}
]
}
},
"/version": {
"get": {
"description": "This will return the daemon version info.",
"tags": [
"Daemon"
],
"summary": "Version",
"operationId": "getDaemonVersion"
}
}
}
}