package main import ( "encoding/json" "flag" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "time" ) func authInfo(host string, port int, user, pswd string) bool { linkAuthInfo := fmt.Sprintf( "http://%s:%d/auth/info?user=%s&pswd=%v", host, port, user, pswd, ) urlAuthInfo, err := url.Parse(linkAuthInfo) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } resp, err := http.Get(urlAuthInfo.String()) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } var result map[string]any err = json.Unmarshal(body, &result) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } var resultAuthInfo map[string]any err = json.Unmarshal(body, &resultAuthInfo) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } return true } func authLogin(host string, port int, user, pswd string) bool { linkAuthLogin := fmt.Sprintf( "http://%s:%d/auth/login?user=%s&pswd=%v", host, port, user, pswd, ) urlAuthLogin, err := url.Parse(linkAuthLogin) if err != nil { fmt.Println("error", err) time.Sleep(10 * time.Second) return false } resp, err := http.Get(urlAuthLogin.String()) if err != nil { fmt.Println("error", err) time.Sleep(10 * time.Second) return false } body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("error", err) time.Sleep(10 * time.Second) return false } var result map[string]any err = json.Unmarshal(body, &result) if err != nil { fmt.Println("error", err) time.Sleep(10 * time.Second) return false } success, ok := result["success"].(bool) if !ok || !success { fmt.Println("login failed, retrying...", result) time.Sleep(10 * time.Second) return false } fmt.Println("login successful") return true } type App struct { BundleID string `json:"bundleID"` Version string `json:"version"` } func search(host string, port int, query string, limit int) ([]App, error) { linkSearch := fmt.Sprintf( "http://%s:%d/search?query=%s&limit=%d", host, port, query, limit, ) urlSearch, err := url.Parse(linkSearch) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return nil, err } resp, err := http.Get(urlSearch.String()) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return nil, err } body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return nil, err } var resultSearch map[string]any err = json.Unmarshal(body, &resultSearch) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return nil, err } apps, ok := resultSearch["apps"].([]any) if !ok { return nil, fmt.Errorf("failed to get apps") } appsList := make([]App, len(apps)) for i, app := range apps { app, ok := app.(map[string]any) if !ok { return nil, fmt.Errorf("failed to get app") } appsList[i] = App{ BundleID: app["bundleID"].(string), Version: app["version"].(string), } } return appsList, nil } type WriteCounter struct { Total int64 Downloaded int64 } func (wc *WriteCounter) Write(p []byte) (int, error) { n := len(p) wc.Downloaded += int64(n) wc.PrintProgress() return n, nil } func (wc *WriteCounter) PrintProgress() { const MB = 1024 * 1024 downloadedMB := float64(wc.Downloaded) / MB if wc.Total <= 0 { fmt.Printf("\rDownloading... %.2f MB", downloadedMB) return } totalMB := float64(wc.Total) / MB percentage := float64(wc.Downloaded) / float64(wc.Total) * 100 fmt.Printf("\rDownloading... %.2f%% (%.2f/%.2f MB)", percentage, downloadedMB, totalMB) } func download(host string, port int, bundleID, query, version, udid string) bool { linkDownload := fmt.Sprintf( "http://%s:%d/download?bundleID=%s", host, port, bundleID, ) urlDownload, err := url.Parse(linkDownload) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } resp, err := http.Get(urlDownload.String()) if err != nil { fmt.Println("error", err) time.Sleep(3 * time.Second) return false } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Printf("error: server returned status %s\n", resp.Status) return false } fileName := filepath.Join("apps", fmt.Sprintf("%s_%s_%s.ipa", udid, query, version)) out, err := os.Create(fileName) if err != nil { fmt.Println("error creating file:", err) return false } defer out.Close() counter := &WriteCounter{Total: resp.ContentLength} _, err = io.Copy(out, io.TeeReader(resp.Body, counter)) if err != nil { fmt.Println("\nerror downloading:", err) return false } fmt.Print("\n") // New line after download completion return true } func checkVersionInDownloaded(udid, query, version string) bool { path := filepath.Join("apps", fmt.Sprintf("%s_%s_%s.ipa", udid, query, version)) _, err := os.Stat(path) return !os.IsNotExist(err) } func main() { bundleID := flag.String("b", "com.zhiliaoapp.musically", "Bundle ID of the app to download from IPAGo server") host := flag.String("h", "", "Host IPAGo server") port := flag.Int("p", 0, "Port IPAGo server") udid := flag.String("u", "", "UDID of the device to download the app to") user := flag.String("user", "", "User to login to IPAGo server") pswd := flag.String("pswd", "", "Password to login to IPAGo server") query := flag.String("q", "", "Query to search for apps on IPAGo server (example: TikTok)") limit := flag.Int("l", 1, "Limit of apps to search for on IPAGo server") outputPath := flag.String("o", "", "Output path to save the app") flag.Parse() if *outputPath == "" { fmt.Println("output path is required") flag.PrintDefaults() os.Exit(1) } os.MkdirAll(*outputPath, 0755) if *host == "" { fmt.Println("host is required") flag.PrintDefaults() os.Exit(1) } if *port == 0 { fmt.Println("port is required") flag.PrintDefaults() os.Exit(1) } if *udid == "" { fmt.Println("UDID is required") flag.PrintDefaults() os.Exit(1) } if *user == "" { fmt.Println("user is required") flag.PrintDefaults() os.Exit(1) } if *pswd == "" { fmt.Println("password is required") flag.PrintDefaults() os.Exit(1) } if *query == "" { fmt.Println("query is required") flag.PrintDefaults() os.Exit(1) } userQuery := url.QueryEscape(*user) pswdQuery := url.QueryEscape(*pswd) for { if !authInfo(*host, *port, userQuery, pswdQuery) { continue } if !authLogin(*host, *port, userQuery, pswdQuery) { continue } searchResult, err := search(*host, *port, *query, *limit) if err != nil { continue } // TODO: handle multiple apps with the same bundleID srFirst := searchResult[0] if srFirst.BundleID != *bundleID { fmt.Println("bundleID mismatch, expected", *bundleID, "got", srFirst.BundleID) time.Sleep(120 * time.Minute) continue } if checkVersionInDownloaded(*udid, *query, srFirst.Version) { fmt.Println("version already downloaded, skipping...") time.Sleep(120 * time.Minute) continue } if !download(*host, *port, srFirst.BundleID, *query, srFirst.Version, *udid) { continue } } }