fdroidgo/main.go

639 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
)
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"`
}
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 <uses-sdk>
type UsesSdk struct {
MinSdkVersion string `xml:"minSdkVersion,attr"`
TargetSdkVersion string `xml:"targetSdkVersion,attr"`
}
// Structure for <uses-permission>
type UsesPermission struct {
Name string `xml:"name,attr"`
}
// Structure for <uses-feature>
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 {
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 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 {
log.Fatal(err)
}
defer file.Close()
// Parse the TOML file into the config struct
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(),
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 // Укажите алиас для ключа
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.")
}