From 71c1312df9f59f6833c4793e06a7c6cfd56cffd4 Mon Sep 17 00:00:00 2001 From: doesnm Date: Tue, 20 May 2025 19:05:15 +0000 Subject: [PATCH] Now repository can have apps and allowed to install them --- apktest.go | 28 ++++ go.mod | 5 + go.sum | 4 + main.go | 482 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 512 insertions(+), 7 deletions(-) create mode 100644 apktest.go diff --git a/apktest.go b/apktest.go new file mode 100644 index 0000000..94190e1 --- /dev/null +++ b/apktest.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/xml" + "fmt" + "github.com/avast/apkparser" + "os" +) + +func main() { + enc := xml.NewEncoder(os.Stdout) + //enc.Indent("", "\t") + zipErr, resErr, manErr := apkparser.ParseApk(os.Args[1], enc) + if zipErr != nil { + fmt.Fprintf(os.Stderr, "Failed to open the APK: %s", zipErr.Error()) + os.Exit(1) + return + } + + if resErr != nil { + fmt.Fprintf(os.Stderr, "Failed to parse resources: %s", resErr.Error()) + } + if manErr != nil { + fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s", manErr.Error()) + os.Exit(1) + return + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index fa50d9b..30ae00a 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,8 @@ module fdroidgo go 1.24.2 require github.com/BurntSushi/toml v1.5.0 + +require ( + github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 // indirect + github.com/klauspost/compress v1.18.0 // indirect +) diff --git a/go.sum b/go.sum index ff7fd09..4d4e1e6 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 h1:gFlKwiEFHiSUH79HIhSa2jJmM6gZg4xQaZIkMjUS73c= +github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56/go.mod h1:3F9A8btIerUcuy7Fmno+g/nIk4ELKJ6NCs2/KK1bvLs= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= diff --git a/main.go b/main.go index c63a616..594a951 100644 --- a/main.go +++ b/main.go @@ -8,9 +8,20 @@ import ( "io/ioutil" "os" "os/exec" - "github.com/BurntSushi/toml" - "time" - "log" + "github.com/BurntSushi/toml" + "time" + "log" + "net/http" + "strings" + "bytes" + "encoding/xml" + "github.com/avast/apkparser" + "strconv" + "crypto/sha256" + "encoding/hex" + "path/filepath" + "net/url" + "path" ) type Repo struct { @@ -55,6 +66,7 @@ type Index struct { Repo Repo `json:"repo"` Requests Requests `json:"requests"` Apps []App `json:"apps"` + Packages map[string][]Package `json:"packages"` } type Config struct { @@ -62,6 +74,389 @@ type Config struct { Icon string `toml:"icon"` Address string `toml:"address"` Description string `toml:"description"` + KeyStorePath string `toml:"keystore_path"` + KeyStorePassword string `toml:"keystore_password"` + KeyStoreAlias string `toml:"keystore_alias"` + BinarySources []string `toml:"binarySources"` +} + +type User struct { + Name *string `json:"name,omitempty"` + Login string `json:"login"` +} + +type ReleaseAsset struct { + BrowserDownloadURL string `json:"browser_download_url"` + ID int `json:"id"` + Size int `json:"size"` + CreatedAt time.Time `json:"created_at"` + Uploader *User `json:"uploader,omitempty"` +} + + +type Release struct { + TagName string `json:"tag_name"` + Name *string `json:"name,omitempty"` + CreatedAt time.Time `json:"created_at"` + PublishedAt *time.Time `json:"published_at,omitempty"` + Author User `json:"author"` + Assets []ReleaseAsset `json:"assets"` +} + +type Repository struct { + License *License `json:"license"` + HTMLURL string `json:"html_url"` + IssuesUrl string `json:"issues_url"` + Homepage string `json:"homepage"` + Description string `json:"description"` +} + +type License struct { + Name string `json:"name"` + SPDXID string `json:"spdx_id"` +} + +type Package struct { + Added int64 `json:"added"` + ApkName string `json:"apkName"` + Hash string `json:"hash"` + HashType string `json:"hashType"` + MinSdkVersion int `json:"minSdkVersion"` + NativeCode []string `json:"nativecode"` + PackageName string `json:"packageName"` + Sig string `json:"sig"` + Signer string `json:"signer"` + Size int64 `json:"size"` + TargetSdkVersion int `json:"targetSdkVersion"` + UsesPermission [][]*string `json:"uses-permission"` + UsesPermissionSdk23 [][]string `json:"uses-permission-sdk-23"` + VersionCode int64 `json:"versionCode"` + VersionName string `json:"versionName"` + ApplicationName string `json:"-"` +} +type Application struct { + Label string `xml:"label,attr"` + Icon string `xml:"icon,attr"` + // Add other application attributes as needed +} +type Manifest struct { + PackageName string `xml:"package,attr"` + VersionCode int64 `xml:"versionCode,attr"` + VersionName string `xml:"versionName,attr"` + UsesSdk UsesSdk `xml:"uses-sdk"` + UsesPermission []UsesPermission `xml:"uses-permission"` + Application Application `xml:"application"` +} + +// Structure for + +type UsesSdk struct { + MinSdkVersion string `xml:"minSdkVersion,attr"` + TargetSdkVersion string `xml:"targetSdkVersion,attr"` +} +// Structure for +type UsesPermission struct { + Name string `xml:"name,attr"` +} + +// Structure for + + +func apkExists(filePath string) bool { + _, err := os.Stat(filePath) + return !os.IsNotExist(err) +} + +// Function to check if the URL is an APK +func isAPK(url string) bool { + return strings.HasSuffix(url, ".apk") +} + +// Function to download the APK file +func downloadAPK(url string, filePath string) error { + fmt.Printf("Downloading %s",url) + response, err := http.Get(url) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download file: %s", response.Status) + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, response.Body) + return err +} + +func getResourceTypeId(resourceType string) int { + switch resourceType { + case "string": + return 0 + case "drawable": + return 1 + case "layout": + return 2 + case "mipmap": + return 3 + case "color": + return 4 + case "id": + return 5 + case "style": + return 6 + case "anim": + return 7 + case "menu": + return 8 + case "xml": + return 9 + case "raw": + return 10 + case "font": + return 11 + case "transition": + return 12 + case "drawable-v21": + return 13 + default: + return -1 // Invalid resource type + } +} + + +func saveIconFile(f *apkparser.ZipReaderFile, packageName string) string { + // Create the directory if it does not exist + err := os.MkdirAll(packageName + "/en-US", os.ModePerm) + if err != nil { + fmt.Println("Error creating directory:", err) + return "" + } + + // Define the path for the icon file + + iconPath := filepath.Join(packageName, "en-US") + iconPath = filepath.Join(iconPath,"icon.png") + // Create a new file to save the icon + outFile, err := os.Create(iconPath) + if err != nil { + fmt.Println("Error creating icon file:", err) + return "" + } + defer outFile.Close() + + // Open the zip file for reading + err = f.Open() + if err != nil { + fmt.Println("Error opening zip file:", err) + return "" + } + defer f.Close() + + // Copy the contents of the zip file to the new file + if _, err := io.Copy(outFile, f); err != nil { + fmt.Println("Error copying icon file:", err) + return "" + } + + return iconPath +} + +func processAPK(filePath string) Package { + fmt.Printf("Processing APK: %s\n", filePath) + var buf bytes.Buffer + enc := xml.NewEncoder(&buf) + + zipFile, err := os.Open(filePath) + if err != nil { + fmt.Println("Error opening ZIP file:", err) + os.Exit(1) + return Package{} + } + defer zipFile.Close() + + // Get the file info + fileInfo, err := zipFile.Stat() + if err != nil { + fmt.Println("Error getting file info:", err) + os.Exit(1) + return Package{} + } + + // Create a zip.Reader + zipReader, zipErr := apkparser.OpenZipReader(zipFile) + p,resErr := apkparser.NewParser(zipReader,enc) + + manErr := p.ParseXml("AndroidManifest.xml") + if zipErr != nil { + fmt.Fprintf(os.Stderr, "Failed to open the APK: %s\n", zipErr.Error()) + os.Exit(1) + } + + if resErr != nil { + fmt.Fprintf(os.Stderr, "Failed to parse resources: %s\n", resErr.Error()) + } + if manErr != nil { + fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s\n", manErr.Error()) + os.Exit(1) + } + + manifestXML := buf.String() + var manifest Manifest + if err := xml.Unmarshal([]byte(manifestXML), &manifest); err != nil { + fmt.Fprintf(os.Stderr, "Failed to unmarshal manifest XML: %s\n", err.Error()) + os.Exit(1) + } + + minsdk, err := strconv.Atoi(manifest.UsesSdk.MinSdkVersion) + if err != nil { + log.Fatal("Can't parse minsdk: ", manifest.UsesSdk.MinSdkVersion) + os.Exit(1) + } + targetsdk, err := strconv.Atoi(manifest.UsesSdk.TargetSdkVersion) + if err != nil { + log.Fatal("Can't parse targetsdk: ", manifest.UsesSdk.TargetSdkVersion) + os.Exit(1) + } + + // Calculate SHA-256 hash of the APK file + hash, err := calculateSHA256(filePath) + if err != nil { + log.Fatalf("Failed to calculate SHA-256 hash: %s\n", err.Error()) + os.Exit(1) + } + + packageInfo := Package{ + ApkName: filePath, + VersionCode: manifest.VersionCode, + VersionName: manifest.VersionName, + MinSdkVersion: minsdk, + TargetSdkVersion: targetsdk, + PackageName: manifest.PackageName, + UsesPermission: make([][]*string, len(manifest.UsesPermission)), + ApplicationName: manifest.Application.Label, + Hash: hash, + HashType: "SHA-256", + Size: fileInfo.Size(), + } + + for i, perm := range manifest.UsesPermission { + packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], &perm.Name) + packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], nil) + } + + if !strings.HasPrefix(manifest.Application.Icon, "@") { + iconFile := zipReader.File[manifest.Application.Icon] + saveIconFile(iconFile,manifest.PackageName) + } + return packageInfo +} + +// calculateSHA256 computes the SHA-256 hash of the given file. +func calculateSHA256(filePath string) (string, error) { + fmt.Println("Calculating SHA256") + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", err + } + + hash := hasher.Sum(nil) + return hex.EncodeToString(hash), nil +} + +func getFilenameFromURL(urlStr string) (string, error) { + // Parse the URL + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + // Get the path from the URL and extract the filename + filename := path.Base(parsedURL.Path) + return filename, nil +} +// Function to handle downloading and processing APKs from a release +func handleRelease(release Release) []Package { + var packages []Package + for _, asset := range release.Assets { + if !isAPK(asset.BrowserDownloadURL) { + //fmt.Printf("Skipping non-APK file: %s\n", asset.BrowserDownloadURL) + continue + } + + filePath,err := getFilenameFromURL(asset.BrowserDownloadURL) + if apkExists(asset.BrowserDownloadURL) { + fmt.Printf("APK already exists: %s\n", asset.BrowserDownloadURL) + continue + } + + err = downloadAPK(asset.BrowserDownloadURL, filePath) + if err != nil { + fmt.Printf("Error downloading APK: %v\n", err) + os.Exit(1) + continue + } + + packageInfo := processAPK(filePath) + packages = append(packages,packageInfo) + } + return packages +} +func getReleases(repo string) ([]Release, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get releases: %s", resp.Status) + os.Exit(1) + } + + var releases []Release +// Декодируем JSON-ответ в структуру Release +// Декодируем JSON-ответ в pструктуру Release + if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { + return nil, err + } + + return releases, nil +} + + +func getRepositoryInfo(repo string) (Repository, error) { + fmt.Println("Fetching repository info") + url := fmt.Sprintf("https://api.github.com/repos/%s", repo) + resp, err := http.Get(url) + if err != nil { + return Repository{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return Repository{}, fmt.Errorf("failed to get repository info: %s", resp.Status) + } + + var repository Repository + if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil { + return Repository{}, err + } + + return repository, nil } func generateJSON(index Index, filename string) error { @@ -114,7 +509,48 @@ func signJarWithJarsigner(jarFile string, keystore string, password string, alia return err, string(output) // Возвращаем ошибку и вывод как строку } +func createAppFromReleases(releases []Release, repo string) (App, []Package,error) { + // Получаем информацию о репозитории + repository, err := getRepositoryInfo(repo) + if err != nil { + return App{}, []Package{}, err + } + + // Предположим, что мы берем данные из первого релиза + if len(releases) == 0 { + return App{}, []Package{}, nil + } + + release := releases[0] + packages := handleRelease(release) + + app := App{ + AuthorName: release.Author.Login, + Categories: []string{"fdroid"}, + SuggestedVersionCode: release.TagName, + IssueTracker: repository.HTMLURL + "/issues", + License: repository.License.SPDXID, + Name: packages[0].ApplicationName, + SourceCode: repository.HTMLURL, + Summary: repository.Description, + WebSite: repository.Homepage, + Added: time.Now().Unix(), + PackageName: packages[0].PackageName, + LastUpdated: release.CreatedAt.Unix(), + Localized: Localized{ + ENUS: Icon { + Icon: "icon.png", + }, + }, + } + + return app, packages, nil +} + func main() { + var apps []App + var packages map[string][]Package + packages = make(map[string][]Package) var config Config file, err := os.Open("config.toml") if err != nil { @@ -126,6 +562,37 @@ func main() { if _, err := toml.NewDecoder(file).Decode(&config); err != nil { log.Fatal(err) } + +for _, source := range config.BinarySources { + fmt.Printf("Processing %s\n",source) + // Извлекаем имя репозитория из ссылки + repo := strings.TrimPrefix(source, "github.com/") + if repo == source { // Если не удалось извлечь, пропускаем + fmt.Printf("Invalid repository format: %s\n", source) + continue + } + + // Получаем релизы для репозитория + fmt.Println("Fetching releases") + releases, err := getReleases(repo) + if err != nil { + fmt.Printf("Error fetching releases for %s: %v\n", repo, err) + os.Exit(1) + continue + } + + + + var app App + fmt.Println("Creating app object from repository info and apk") + app,app_packages, err := createAppFromReleases(releases,repo) + if err != nil { + log.Fatal("Failed to create app from releases: ",err) + } + apps = append(apps,app) + packages[app_packages[0].PackageName] = append(packages[app_packages[0].PackageName], app_packages...) +} + index := Index { Repo: Repo{ Timestamp: time.Now().Unix(), @@ -139,7 +606,8 @@ func main() { Install: []string{}, Uninstall: []string{}, }, - Apps: []App{}, + Apps: apps, + Packages: packages, } // Генерация JSON файла err = generateJSON(index, "index-v1.json") @@ -158,9 +626,9 @@ func main() { fmt.Println("JAR file created successfully.") // Подпись JAR файла - p12File := "../../keystore.p12" // Укажите путь к вашему P12 файлу - password := "u86viXB0FRlTY2K1buzGqWHdDu6pIgu8520R3IJgXAE=" // Укажите пароль для P12 файла - alias := "curious-invention.aeza.network" // Укажите алиас для ключа + p12File := config.KeyStorePath // Укажите путь к вашему P12 файлу + password := config.KeyStorePassword // Укажите пароль для P12 файла + alias := config.KeyStoreAlias // Укажите алиас для ключа err, output := signJarWithJarsigner("index-v1.jar", p12File, password, alias) if err != nil {