package gitea import ( "bufio" "crypto/tls" "encoding/json" "fmt" "net" "net/http" "net/url" "strings" "time" "software-station/internal/models" ) func CheckSourceSecurity(serverURL, token, owner, repo string) *models.SourceSecurity { u, err := url.Parse(serverURL) if err != nil { return nil } domain := u.Hostname() security := &models.SourceSecurity{ Domain: domain, } // TLS Check conf := &tls.Config{ InsecureSkipVerify: false, MinVersion: tls.VersionTLS12, } // Use port 443 for HTTPS domains port := "443" if u.Port() != "" { port = u.Port() } dialer := &net.Dialer{Timeout: 5 * time.Second} conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(domain, port), conf) if err == nil { security.TLSValid = true _ = conn.Close() } return security } func FetchUserGPGKeys(server, token, username string) ([]string, error) { url := fmt.Sprintf("%s/api/v1/users/%s/gpg_keys", server, username) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gitea gpg api returned status %d", resp.StatusCode) } var giteaKeys []struct { PublicKey string `json:"public_key"` } if err := json.NewDecoder(resp.Body).Decode(&giteaKeys); err != nil { return nil, err } keys := make([]string, len(giteaKeys)) for i, k := range giteaKeys { keys[i] = k.PublicKey } return keys, nil } func FetchContributors(server, token, owner, repo string) ([]models.Contributor, error) { url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ContributorsSuffix) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode) } var giteaContributors []struct { Username string `json:"username"` AvatarURL string `json:"avatar_url"` } if err := json.NewDecoder(resp.Body).Decode(&giteaContributors); err != nil { return nil, err } contributors := make([]models.Contributor, len(giteaContributors)) for i, c := range giteaContributors { keys, _ := FetchUserGPGKeys(server, token, c.Username) contributors[i] = models.Contributor{ Username: c.Username, AvatarURL: c.AvatarURL, GPGKeys: keys, } } return contributors, nil } func DetectOS(filename string) string { lower := strings.ToLower(filename) if strings.HasSuffix(lower, ".whl") { if strings.Contains(lower, "win") || strings.Contains(lower, "windows") { return models.OSWindows } if strings.Contains(lower, "macosx") || strings.Contains(lower, "darwin") { return models.OSMacOS } if strings.Contains(lower, "linux") { return models.OSLinux } return models.OSUnknown } if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".zip") { if strings.Contains(lower, "win") || strings.Contains(lower, "windows") { return models.OSWindows } if strings.Contains(lower, "mac") || strings.Contains(lower, "darwin") || strings.Contains(lower, "osx") { return models.OSMacOS } if strings.Contains(lower, "linux") { return models.OSLinux } if strings.Contains(lower, "freebsd") { return models.OSFreeBSD } if strings.Contains(lower, "openbsd") { return models.OSOpenBSD } return models.OSUnknown } osMap := []struct { patterns []string suffixes []string os string }{ { patterns: []string{"windows"}, suffixes: []string{".exe", ".msi"}, os: models.OSWindows, }, { patterns: []string{"linux"}, suffixes: []string{".deb", ".rpm", ".appimage", ".flatpak"}, os: models.OSLinux, }, { patterns: []string{"mac", "darwin"}, suffixes: []string{".dmg", ".pkg"}, os: models.OSMacOS, }, { patterns: []string{"freebsd"}, os: models.OSFreeBSD, }, { patterns: []string{"openbsd"}, os: models.OSOpenBSD, }, { patterns: []string{"android"}, suffixes: []string{".apk"}, os: models.OSAndroid, }, { patterns: []string{"arm", "aarch64"}, os: models.OSARM, }, } for _, entry := range osMap { for _, p := range entry.patterns { if strings.Contains(lower, p) { return entry.os } } for _, s := range entry.suffixes { if strings.HasSuffix(lower, s) { return entry.os } } } return models.OSUnknown } func FetchRepoInfo(server, token, owner, repo string) (string, []string, string, bool, string, error) { url := fmt.Sprintf("%s%s/%s/%s", server, RepoAPIPath, owner, repo) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", nil, "", false, "", err } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} resp, err := client.Do(req) if err != nil { return "", nil, "", false, "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", nil, "", false, "", fmt.Errorf("gitea api returned status %d", resp.StatusCode) } var info struct { Description string `json:"description"` Topics []string `json:"topics"` DefaultBranch string `json:"default_branch"` Licenses []string `json:"licenses"` Private bool `json:"private"` AvatarURL string `json:"avatar_url"` } if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return "", nil, "", false, "", err } license := "" if len(info.Licenses) > 0 { license = info.Licenses[0] } if license == "" { // Try to detect license from file if API returns nothing license = detectLicenseFromFile(server, token, owner, repo, info.DefaultBranch) } return info.Description, info.Topics, license, info.Private, info.AvatarURL, nil } func detectLicenseFromFile(server, token, owner, repo, defaultBranch string) string { branches := []string{"main", "master"} if defaultBranch != "" { branches = append([]string{defaultBranch}, "main", "master") } seen := make(map[string]bool) var finalBranches []string for _, b := range branches { if !seen[b] { seen[b] = true finalBranches = append(finalBranches, b) } } for _, branch := range finalBranches { url := fmt.Sprintf("%s/%s/%s/raw/branch/%s/LICENSE", server, owner, repo, branch) req, err := http.NewRequest("GET", url, nil) if err != nil { continue } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} resp, err := client.Do(req) if err != nil { continue } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { scanner := bufio.NewScanner(resp.Body) for i := 0; i < 5 && scanner.Scan(); i++ { line := strings.ToUpper(scanner.Text()) if strings.Contains(line, "MIT LICENSE") { return "MIT" } if strings.Contains(line, "GNU GENERAL PUBLIC LICENSE") || strings.Contains(line, "GPL") { return "GPL" } if strings.Contains(line, "APACHE LICENSE") { return "Apache-2.0" } if strings.Contains(line, "BSD") { return "BSD" } } return "LICENSE" } } return "" } func IsSBOM(filename string) bool { lower := strings.ToLower(filename) return strings.Contains(lower, "sbom") || strings.Contains(lower, "cyclonedx") || strings.Contains(lower, "spdx") || strings.HasSuffix(lower, ".cdx.json") || strings.HasSuffix(lower, ".spdx.json") } func FetchReleases(server, token, owner, repo string) ([]models.Release, error) { url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ReleasesSuffix) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode) } var giteaReleases []struct { TagName string `json:"tag_name"` Body string `json:"body"` CreatedAt time.Time `json:"created_at"` Assets []struct { Name string `json:"name"` Size int64 `json:"size"` URL string `json:"browser_download_url"` } `json:"assets"` } if err := json.NewDecoder(resp.Body).Decode(&giteaReleases); err != nil { return nil, err } contributors, _ := FetchContributors(server, token, owner, repo) security := CheckSourceSecurity(server, token, owner, repo) var releases []models.Release for _, gr := range giteaReleases { var assets []models.Asset var checksumsURL string for _, ga := range gr.Assets { if ga.Name == "SHA256SUMS" { checksumsURL = ga.URL continue } assets = append(assets, models.Asset{ Name: ga.Name, Size: ga.Size, URL: ga.URL, OS: DetectOS(ga.Name), IsSBOM: IsSBOM(ga.Name), }) } if checksumsURL != "" { checksums, err := fetchAndParseChecksums(checksumsURL, token) if err == nil { for i := range assets { if sha, ok := checksums[assets[i].Name]; ok { assets[i].SHA256 = sha } } } } releases = append(releases, models.Release{ TagName: gr.TagName, Body: gr.Body, CreatedAt: gr.CreatedAt, Assets: assets, Contributors: contributors, Security: security, }) } return releases, nil } func fetchAndParseChecksums(url, token string) (map[string]string, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } if token != "" { req.Header.Set("Authorization", "token "+token) } client := &http.Client{Timeout: DefaultTimeout} 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 fetch checksums: %d", resp.StatusCode) } checksums := make(map[string]string) scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } parts := strings.Fields(line) if len(parts) >= 2 { checksums[parts[1]] = parts[0] } } return checksums, nil }