Files
software-station/internal/gitea/client.go
Sudo-Ivan 4c60e3cf4a
All checks were successful
renovate / renovate (push) Successful in 2m8s
CI / build (push) Successful in 10m24s
Update asset verification and user experience
- Added SRI hash injection during frontend build to improve security.
- Updated ESLint configuration to include 'navigator' as a global variable.
- Introduced a new `settingsStore` to manage user preferences for asset verification.
- Enhanced `SoftwareCard` and `VerificationModal` components to display contributor information and security checks.
- Updated `verificationStore` to handle expanded toast notifications for detailed verification steps.
- Implemented a new `CodeBlock` component for displaying code snippets with syntax highlighting.
- Improved API documentation and added new endpoints for fetching software and asset details.
2025-12-27 16:29:05 -06:00

415 lines
9.5 KiB
Go

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)
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
}