package main import ( "archive/zip" "encoding/json" "fmt" "io" "io/ioutil" "os" "os/exec" "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" "flag" ) type Repo struct { Timestamp int64 `json:"timestamp"` Name string `json:"name"` Version int64 `json:"version"` Icon string `json:"icon"` Address string `json:"address"` Description string `json:"description"` } type Requests struct { Install []string `json:"install"` Uninstall []string `json:"uninstall"` } type Localized struct { ENUS Icon `json:"en-US"` } type Icon struct { Icon string `json:"icon"` } type App struct { AuthorName string `json:"authorName"` Categories []string `json:"categories"` SuggestedVersionCode string `json:"suggestedVersionCode"` IssueTracker string `json:"issueTracker"` License string `json:"license"` Name string `json:"name"` SourceCode string `json:"sourceCode"` Summary string `json:"summary"` WebSite string `json:"webSite"` Added int64 `json:"added"` PackageName string `json:"packageName"` LastUpdated int64 `json:"lastUpdated"` Localized Localized `json:"localized,omitempty"` } type Index struct { Repo Repo `json:"repo"` Requests Requests `json:"requests"` Apps []App `json:"apps"` Packages map[string][]Package `json:"packages"` } type Config struct { Name string `toml:"name"` 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"` GithubToken string `toml:"github_token"` RelicConfig string `toml:"relic_config"` } 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"` PreRelease bool `json:"prerelease"` } 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 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") if apkExists(iconPath) { return iconPath } // 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 contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } func processAPK(filePath string,indexCache Index) Package { fmt.Printf("Processing APK: %s\n", filePath) for _, pkgs := range indexCache.Packages { for _, pkg := range pkgs { if pkg.ApkName == filePath { log.Printf("Warning: using %s metadata from cache",filePath) for _, app := range indexCache.Apps { if app.PackageName == pkg.PackageName { pkg.ApplicationName = app.Name break } } return pkg } } } 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) } var directories []string // Iterate through the files in the ZIP archive for _, file := range zipReader.File { // Check if the file is a directory and starts with 'lib/' if file.IsDir { // Check if the directory is directly under 'lib/' if strings.HasPrefix(file.Name, "lib/") && strings.Count(file.Name, "/") == 1 { // Extract the directory name without the 'lib/' prefix dirName := strings.TrimPrefix(file.Name, "lib/") directories = append(directories, strings.TrimSuffix(dirName, "/")) } } else { // If it's a file, check if it belongs to a directory under 'lib/' if strings.HasPrefix(file.Name, "lib/") { // Extract the directory name dirName := strings.Split(file.Name, "/")[1] // Get the first subdirectory under 'lib' if !contains(directories, dirName) { directories = append(directories, dirName) } } } } 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(), NativeCode: directories, } 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,indexCache Index) []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(filePath) { fmt.Printf("APK already exists: %s\n", filePath) }else{ err = downloadAPK(asset.BrowserDownloadURL, filePath) if err != nil { fmt.Printf("Error downloading APK: %v\n", err) os.Exit(1) } } packageInfo := processAPK(filePath,indexCache) packages = append(packages,packageInfo) } return packages } func getReleases(repo string,config Config) ([]Release, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo) req, err := http.NewRequest("GET",url,nil) if err != nil { return nil, err } if config.GithubToken != "" { req.Header.Set("Authorization", "token "+config.GithubToken) } client := http.Client{} resp,err := client.Do(req) 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,config Config) (Repository, error) { fmt.Println("Fetching repository info") url := fmt.Sprintf("https://api.github.com/repos/%s", repo) req, err := http.NewRequest("GET",url,nil) if err != nil { return Repository{}, err } if config.GithubToken != "" { req.Header.Set("Authorization", "token "+config.GithubToken) } client := &http.Client{} resp,err := client.Do(req) 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 { data, err := json.MarshalIndent(index, "", " ") if err != nil { return err } return ioutil.WriteFile(filename, data, 0644) } func createJar(zipFileName string, files []string) error { zipFile, err := os.Create(zipFileName) if err != nil { return err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() for _, file := range files { err := addFileToZip(zipWriter, file) if err != nil { return err } } return nil } func addFileToZip(zipWriter *zip.Writer, file string) error { fileToZip, err := os.Open(file) if err != nil { return err } defer fileToZip.Close() w, err := zipWriter.Create(file) if err != nil { return err } _, err = io.Copy(w, fileToZip) return err } func signJarWithJarsigner(jarFile string, keystore string, password string, alias string) (error, string) { cmd := exec.Command("jarsigner", "-sigalg","SHA1withRSA","-digestalg","SHA1","-keystore", keystore, "-storetype", "PKCS12", "-storepass", password, jarFile, alias) output, err := cmd.CombinedOutput() // Сначала получаем вывод и ошибку return err, string(output) // Возвращаем ошибку и вывод как строку } func signJarWithRelic(jarFile string, keystore string, password string, alias string,relicConfig string) (error, string) { cmd := exec.Command("relic", "sign","--digest","SHA1","--key-alias",alias,"--file",jarFile,"--output",jarFile,"--key",keystore,"--config",relicConfig) //cmd.Env = append(cmd.Env, "KEYSTORE_PASSWORD="+password) output, err := cmd.CombinedOutput() // Сначала получаем вывод и ошибку return err, string(output) // Возвращаем ошибку и вывод как строку } func createAppFromReleases(releases []Release, repo string, index int,indexCache Index,config Config) (App, []Package,error) { // Получаем информацию о репозитории repository, err := getRepositoryInfo(repo,config) if err != nil { return App{}, []Package{}, err } // Предположим, что мы берем данные из первого релиза if len(releases) == 0 { return App{}, []Package{}, nil } release := releases[index] packages := handleRelease(release,indexCache) if len(packages) == 0 { return createAppFromReleases(releases,repo,index+1,indexCache,config) //return App{}, []Package{}, nil } //fmt.Printf("Debug: ",repository.License) if repository.License == nil { repository.License = &License { SPDXID: "", } } 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 indexCache Index var packages map[string][]Package packages = make(map[string][]Package) var config Config configPath := flag.String("config", "config.toml", "Path to the configuration file") useRelic := flag.Bool("relic",false,"Use relic instead that jarsigner") // Parse the command-line flags flag.Parse() file, err := os.Open(*configPath) fileJson,errJson := os.Open("index-v1.json") if err != nil { log.Fatal(err) } if errJson != nil { log.Printf("Error opening index cache so cache will not used: %s",errJson) indexCache = Index{} } defer file.Close() defer fileJson.Close() // Parse the TOML file into the config struct if errJson := json.NewDecoder(fileJson).Decode(&indexCache); err != nil { log.Printf("Error decoding index cache so cache will not used: %s",errJson) } 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,config) 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,0,indexCache,config) if err != nil { log.Fatal("Failed to create app from releases: ",err) } if len(app_packages) != 0 { 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(), Name: config.Name, Icon: "icon.png", Version: 1002, Address: config.Address, Description: config.Description, }, Requests: Requests{ Install: []string{}, Uninstall: []string{}, }, Apps: apps, Packages: packages, } // Генерация JSON файла err = generateJSON(index, "index-v1.json") if err != nil { fmt.Println("Error generating JSON:", err) return } fmt.Println("JSON file generated successfully.") // Создание JAR файла err = createJar("index-v1.jar", []string{"index-v1.json"}) if err != nil { fmt.Println("Error creating JAR:", err) return } fmt.Println("JAR file created successfully.") // Подпись JAR файла p12File := config.KeyStorePath // Укажите путь к вашему P12 файлу password := config.KeyStorePassword // Укажите пароль для P12 файла alias := config.KeyStoreAlias // Укажите алиас для ключа if *useRelic == true { err, output := signJarWithRelic("index-v1.jar", p12File, password, alias,config.RelicConfig) if err != nil { fmt.Println("Error signing JAR:", err, output) return } fmt.Println("JAR file signed successfully.") }else{ err, output := signJarWithJarsigner("index-v1.jar", p12File, password, alias) if err != nil { fmt.Println("Error signing JAR:", err, output) return } fmt.Println("JAR file signed successfully.") } }