639 lines
17 KiB
Go
639 lines
17 KiB
Go
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.")
|
||
}
|