mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 23:17:51 -04:00
core: drop curl dependencies, weather: bump API timeouts (#2430)
This commit is contained in:
1 parent
67968aef79
commit
662dffc141
14 files changed
+523
-139
No files matched your search
@@ -3,19 +3,19 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dlOutput string
|
||||
var dlUserAgent string
|
||||
var dlHeaders []string
|
||||
var dlTimeout int
|
||||
var dlConnectTimeout int
|
||||
var dlIPv4Only bool
|
||||
|
||||
var dlCmd = &cobra.Command{
|
||||
@@ -33,65 +33,52 @@ var dlCmd = &cobra.Command{
|
||||
func init() {
|
||||
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
||||
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
||||
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
|
||||
dlCmd.Flags().StringArrayVarP(&dlHeaders, "header", "H", nil, "Extra request header as 'Name: value' (repeatable)")
|
||||
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Total request timeout in seconds")
|
||||
dlCmd.Flags().IntVar(&dlConnectTimeout, "connect-timeout", 5, "Connection timeout in seconds")
|
||||
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
||||
}
|
||||
|
||||
func runDownload(url string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
func parseHeaders(raw []string) (map[string]string, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case dlUserAgent != "":
|
||||
req.Header.Set("User-Agent", dlUserAgent)
|
||||
default:
|
||||
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
transport := &http.Transport{DialContext: dialer.DialContext}
|
||||
if dlIPv4Only {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp4", addr)
|
||||
headers := make(map[string]string, len(raw))
|
||||
for _, entry := range raw {
|
||||
name, value, found := strings.Cut(entry, ":")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("invalid header %q: expected 'Name: value'", entry)
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("invalid header %q: empty name", entry)
|
||||
}
|
||||
headers[name] = strings.TrimSpace(value)
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
func runDownload(url string) error {
|
||||
headers, err := parseHeaders(dlHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if dlOutput == "" {
|
||||
_, err = io.Copy(os.Stdout, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
if dir := filepath.Dir(dlOutput); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir failed: %w", err)
|
||||
}
|
||||
opts := netfetch.Options{
|
||||
Headers: headers,
|
||||
UserAgent: dlUserAgent,
|
||||
Timeout: time.Duration(dlTimeout) * time.Second,
|
||||
ConnectTimeout: time.Duration(dlConnectTimeout) * time.Second,
|
||||
IPv4Only: dlIPv4Only,
|
||||
}
|
||||
|
||||
f, err := os.Create(dlOutput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create failed: %w", err)
|
||||
if dlOutput == "" {
|
||||
return netfetch.ToWriter(context.Background(), url, opts, os.Stdout)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
os.Remove(dlOutput)
|
||||
return fmt.Errorf("write failed: %w", err)
|
||||
if err := netfetch.ToFile(context.Background(), url, opts, dlOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(dlOutput)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||
@@ -412,27 +413,11 @@ func updateDMSBinary() error {
|
||||
}
|
||||
|
||||
fmt.Println("Fetching latest release version...")
|
||||
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
|
||||
output, err := cmd.Output()
|
||||
version, err := netfetch.LatestReleaseTag(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch latest release: %w", err)
|
||||
}
|
||||
|
||||
version := ""
|
||||
for line := range strings.SplitSeq(string(output), "\n") {
|
||||
if strings.Contains(line, "\"tag_name\"") {
|
||||
parts := strings.Split(line, "\"")
|
||||
if len(parts) >= 4 {
|
||||
version = parts[3]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest version")
|
||||
}
|
||||
|
||||
fmt.Printf("Latest version: %s\n", version)
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "dms-update-*")
|
||||
@@ -448,14 +433,12 @@ func updateDMSBinary() error {
|
||||
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
|
||||
|
||||
fmt.Println("Downloading dms binary...")
|
||||
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
if err := netfetch.ToFile(context.Background(), binaryURL, netfetch.Options{Timeout: 5 * time.Minute}, binaryPath); err != nil {
|
||||
return fmt.Errorf("failed to download binary: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Downloading checksum...")
|
||||
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
if err := netfetch.ToFile(context.Background(), checksumURL, netfetch.Options{Timeout: 30 * time.Second}, checksumPath); err != nil {
|
||||
return fmt.Errorf("failed to download checksum: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||
)
|
||||
@@ -661,17 +662,10 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
||||
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
|
||||
}
|
||||
|
||||
// Get latest release version
|
||||
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
`curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
|
||||
versionOutput, err := latestVersionCmd.Output()
|
||||
version, err := netfetch.LatestReleaseTag(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest DMS version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(string(versionOutput))
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest DMS version")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -687,8 +681,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
||||
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
|
||||
gzPath := filepath.Join(tmpDir, "dms.gz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
if err := netfetch.ToFile(ctx, downloadURL, netfetch.Options{Timeout: 5 * time.Minute}, gzPath); err != nil {
|
||||
return fmt.Errorf("failed to download DMS binary: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package distros
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
@@ -267,12 +268,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
@@ -505,12 +506,22 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
Progress: 0.18,
|
||||
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
|
||||
CommandInfo: fmt.Sprintf("install keyring for %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||
keyring, err := fetchDearmoredKey(ctx, baseURL+"/Release.key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
tmpKeyring, err := writeTempKeyring(keyring)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("install -m 0644 %s %s", tmpKeyring, keyringPath))
|
||||
err = d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20)
|
||||
os.Remove(tmpKeyring)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||
)
|
||||
|
||||
const (
|
||||
crc24Init = 0xB704CE
|
||||
crc24Poly = 0x1864CFB
|
||||
)
|
||||
|
||||
func crc24(data []byte) uint32 {
|
||||
crc := uint32(crc24Init)
|
||||
for _, b := range data {
|
||||
crc ^= uint32(b) << 16
|
||||
for range 8 {
|
||||
crc <<= 1
|
||||
if crc&0x1000000 != 0 {
|
||||
crc ^= crc24Poly
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc & 0xFFFFFF
|
||||
}
|
||||
|
||||
// dearmorPGP converts an ASCII-armored key (RFC 4880 section 6.2) into the
|
||||
// binary form apt wants behind signed-by, matching `gpg --batch --dearmor`.
|
||||
func dearmorPGP(armored []byte) ([]byte, error) {
|
||||
lines := strings.Split(strings.ReplaceAll(string(armored), "\r\n", "\n"), "\n")
|
||||
|
||||
start := -1
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "-----BEGIN PGP ") {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
return nil, fmt.Errorf("no PGP armor header found")
|
||||
}
|
||||
|
||||
// Armor headers ("Version: ...") run until the first blank line; a block
|
||||
// with no headers starts its payload immediately.
|
||||
for start < len(lines) && strings.Contains(lines[start], ": ") {
|
||||
start++
|
||||
}
|
||||
for start < len(lines) && strings.TrimSpace(lines[start]) == "" {
|
||||
start++
|
||||
}
|
||||
|
||||
var payload strings.Builder
|
||||
checksum := ""
|
||||
ended := false
|
||||
for _, line := range lines[start:] {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case trimmed == "":
|
||||
continue
|
||||
case strings.HasPrefix(trimmed, "-----END PGP "):
|
||||
ended = true
|
||||
case strings.HasPrefix(trimmed, "="):
|
||||
checksum = trimmed[1:]
|
||||
default:
|
||||
payload.WriteString(trimmed)
|
||||
}
|
||||
if ended {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ended {
|
||||
return nil, fmt.Errorf("unterminated PGP armor block")
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(payload.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 in PGP armor: %w", err)
|
||||
}
|
||||
if len(decoded) == 0 {
|
||||
return nil, fmt.Errorf("PGP armor block is empty")
|
||||
}
|
||||
|
||||
if checksum == "" {
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
want, err := base64.StdEncoding.DecodeString(checksum)
|
||||
if err != nil || len(want) != 3 {
|
||||
return nil, fmt.Errorf("invalid PGP armor checksum %q", checksum)
|
||||
}
|
||||
got := crc24(decoded)
|
||||
if uint32(want[0])<<16|uint32(want[1])<<8|uint32(want[2]) != got {
|
||||
return nil, fmt.Errorf("PGP armor checksum mismatch")
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func fetchDearmoredKey(ctx context.Context, url string) ([]byte, error) {
|
||||
armored, err := netfetch.Bytes(ctx, url, netfetch.Options{Timeout: 30 * time.Second})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch %s: %w", url, err)
|
||||
}
|
||||
|
||||
keyring, err := dearmorPGP(armored)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dearmor key from %s: %w", url, err)
|
||||
}
|
||||
return keyring, nil
|
||||
}
|
||||
|
||||
// writeTempKeyring stages the keyring somewhere the unprivileged process can
|
||||
// write, for a privileged install into /etc/apt/keyrings.
|
||||
func writeTempKeyring(keyring []byte) (string, error) {
|
||||
f, err := os.CreateTemp("", "dms-repo-key-*.gpg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp keyring: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(keyring); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("failed to write temp keyring: %w", err)
|
||||
}
|
||||
if err := f.Chmod(0o644); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("failed to chmod temp keyring: %w", err)
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The golden file was produced by `gpg --batch --dearmor` against the armored
|
||||
// fixture, so this pins dearmorPGP to gpg's output byte for byte.
|
||||
func TestDearmorPGPMatchesGPGGolden(t *testing.T) {
|
||||
armored, err := os.ReadFile("testdata/obs-release.asc")
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v", err)
|
||||
}
|
||||
want, err := os.ReadFile("testdata/obs-release.gpg")
|
||||
if err != nil {
|
||||
t.Fatalf("read golden: %v", err)
|
||||
}
|
||||
|
||||
got, err := dearmorPGP(armored)
|
||||
if err != nil {
|
||||
t.Fatalf("dearmorPGP: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("got %d bytes, want %d", len(got), len(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDearmorPGPAcceptsCRLF(t *testing.T) {
|
||||
armored, err := os.ReadFile("testdata/obs-release.asc")
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v", err)
|
||||
}
|
||||
want, err := dearmorPGP(armored)
|
||||
if err != nil {
|
||||
t.Fatalf("dearmorPGP: %v", err)
|
||||
}
|
||||
|
||||
crlf := []byte(strings.ReplaceAll(string(armored), "\n", "\r\n"))
|
||||
got, err := dearmorPGP(crlf)
|
||||
if err != nil {
|
||||
t.Fatalf("dearmorPGP on CRLF input: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("CRLF input decoded differently")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDearmorPGPRejects(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"no header": "mQINBAAA\n",
|
||||
"unterminated": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBAAA\n",
|
||||
"bad base64": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n!!!!\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"empty payload": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"checksum mismatch": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBAAA\n=AAAA\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
}
|
||||
|
||||
for name, input := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := dearmorPGP([]byte(input)); err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=kr/Y
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,145 @@
|
||||
package netfetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultUserAgent = "DankMaterialShell/1.0 (Linux)"
|
||||
|
||||
type Options struct {
|
||||
Headers map[string]string
|
||||
UserAgent string
|
||||
Timeout time.Duration
|
||||
ConnectTimeout time.Duration
|
||||
IPv4Only bool
|
||||
}
|
||||
|
||||
func (o Options) client() *http.Client {
|
||||
connect := o.ConnectTimeout
|
||||
if connect <= 0 {
|
||||
connect = 5 * time.Second
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: connect}
|
||||
transport := &http.Transport{DialContext: dialer.DialContext}
|
||||
if o.IPv4Only {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp4", addr)
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func (o Options) request(ctx context.Context, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
agent := o.UserAgent
|
||||
if agent == "" {
|
||||
agent = DefaultUserAgent
|
||||
}
|
||||
req.Header.Set("User-Agent", agent)
|
||||
|
||||
for name, value := range o.Headers {
|
||||
req.Header.Set(name, value)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Open issues the GET and hands back the body on a 2xx. The caller closes it,
|
||||
// which is also what releases the timeout.
|
||||
func Open(ctx context.Context, url string, opts Options) (io.ReadCloser, error) {
|
||||
if opts.Timeout <= 0 {
|
||||
return open(ctx, url, opts)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
|
||||
body, err := open(ctx, url, opts)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
return &cancelOnClose{ReadCloser: body, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
type cancelOnClose struct {
|
||||
io.ReadCloser
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *cancelOnClose) Close() error {
|
||||
err := c.ReadCloser.Close()
|
||||
c.cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func open(ctx context.Context, url string, opts Options) (io.ReadCloser, error) {
|
||||
req, err := opts.request(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := opts.client().Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// Bytes reads the whole response into memory.
|
||||
func Bytes(ctx context.Context, url string, opts Options) ([]byte, error) {
|
||||
body, err := Open(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
return io.ReadAll(body)
|
||||
}
|
||||
|
||||
// ToWriter streams the response into w.
|
||||
func ToWriter(ctx context.Context, url string, opts Options, w io.Writer) error {
|
||||
body, err := Open(ctx, url, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
_, err = io.Copy(w, body)
|
||||
return err
|
||||
}
|
||||
|
||||
// ToFile streams the response to path, creating parent directories and leaving
|
||||
// nothing behind on failure.
|
||||
func ToFile(ctx context.Context, url string, opts Options, path string) error {
|
||||
if dir := filepath.Dir(path); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create failed: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := ToWriter(ctx, url, opts, f); err != nil {
|
||||
os.Remove(path)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package netfetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const latestReleaseURL = "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest"
|
||||
|
||||
// LatestReleaseTag returns the tag of the newest DankMaterialShell release.
|
||||
func LatestReleaseTag(ctx context.Context) (string, error) {
|
||||
body, err := Bytes(ctx, latestReleaseURL, Options{Timeout: 10 * time.Second})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &release); err != nil {
|
||||
return "", fmt.Errorf("failed to parse latest release: %w", err)
|
||||
}
|
||||
if release.TagName == "" {
|
||||
return "", fmt.Errorf("latest release has no tag")
|
||||
}
|
||||
return release.TagName, nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
@@ -26,7 +27,7 @@ type VersionFetcher interface {
|
||||
GetLatestVersion(dmsPath string) (string, error)
|
||||
}
|
||||
|
||||
// DefaultVersionFetcher is the default implementation that uses git/curl
|
||||
// DefaultVersionFetcher is the default implementation that uses git and the GitHub API
|
||||
type DefaultVersionFetcher struct{}
|
||||
|
||||
func (d *DefaultVersionFetcher) GetCurrentVersion(dmsPath string) (string, error) {
|
||||
@@ -135,29 +136,7 @@ func (d *DefaultVersionFetcher) GetLatestVersion(dmsPath string) (string, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Add timeout to prevent hanging when GitHub is down
|
||||
cmd := exec.Command("curl", "-s", "--max-time", "5", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch latest release: %w", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &result); err != nil {
|
||||
for line := range strings.SplitSeq(string(output), "\n") {
|
||||
if strings.Contains(line, "\"tag_name\"") {
|
||||
parts := strings.Split(line, "\"")
|
||||
if len(parts) >= 4 {
|
||||
return parts[3], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to parse latest version: %w", err)
|
||||
}
|
||||
|
||||
return result.TagName, nil
|
||||
return netfetch.LatestReleaseTag(context.Background())
|
||||
}
|
||||
|
||||
// defaultFetcher is used by the public functions
|
||||
|
||||
@@ -14,7 +14,7 @@ Singleton {
|
||||
// file:// url of the current album's downloaded animated cover, or empty.
|
||||
property string animatedArtUrl: ""
|
||||
|
||||
readonly property var _curlCmd: ["curl", "-sS", "--fail", "--connect-timeout", "5", "--max-time", "15", "--compressed"]
|
||||
readonly property var _fetchCmd: [Proc.dmsBin, "dl", "--connect-timeout", "5", "--timeout", "15"]
|
||||
readonly property string _artDir: Paths.strip(Paths.cache) + "/applemusic-art"
|
||||
property string _token: ""
|
||||
// artist\nalbum (lowercased) -> file url or "" for a known miss; never refetched this session.
|
||||
@@ -90,7 +90,7 @@ Singleton {
|
||||
// US storefront only (search default and the catalog paths below); albums absent there become misses.
|
||||
function _search(key, serial) {
|
||||
const term = encodeURIComponent(_artist + " " + _album);
|
||||
Proc.runCommand(null, _curlCmd.concat(["https://itunes.apple.com/search?media=music&entity=album&limit=1&term=" + term]), (output, exitCode) => {
|
||||
Proc.runCommand(null, _fetchCmd.concat(["https://itunes.apple.com/search?media=music&entity=album&limit=1&term=" + term]), (output, exitCode) => {
|
||||
if (serial !== _serial)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
@@ -124,8 +124,8 @@ Singleton {
|
||||
|
||||
// The anonymous web-player JWT sits in the main JS bundle referenced by any album page.
|
||||
function _fetchToken(pageUrl, callback) {
|
||||
const script = "p=$(curl -sSfL --compressed \"$1\" | grep -oE '/assets/index~[a-zA-Z0-9]+\\.js' | head -1) && curl -sSfL --compressed \"https://music.apple.com$p\" | grep -oE '\"eyJ[A-Za-z0-9._-]+\"' | head -1 | tr -d '\"'";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "sh", pageUrl], (output, exitCode) => {
|
||||
const script = "p=$(\"$0\" dl \"$1\" | grep -oE '/assets/index~[a-zA-Z0-9]+\\.js' | head -1) && \"$0\" dl \"https://music.apple.com$p\" | grep -oE '\"eyJ[A-Za-z0-9._-]+\"' | head -1 | tr -d '\"'";
|
||||
Proc.runCommand(null, ["sh", "-c", script, Proc.dmsBin, pageUrl], (output, exitCode) => {
|
||||
const token = (output || "").trim();
|
||||
if (exitCode !== 0 || token === "") {
|
||||
log.warn("failed to obtain web-player token");
|
||||
@@ -137,7 +137,7 @@ Singleton {
|
||||
|
||||
function _fetchEditorialVideo(key, serial, albumId) {
|
||||
const url = "https://amp-api.music.apple.com/v1/catalog/us/albums/" + albumId + "?extend=editorialVideo";
|
||||
Proc.runCommand(null, _curlCmd.concat(["-H", "Authorization: Bearer " + _token, "-H", "Origin: https://music.apple.com", url]), (output, exitCode) => {
|
||||
Proc.runCommand(null, _fetchCmd.concat(["-H", "Authorization: Bearer " + _token, "-H", "Origin: https://music.apple.com", url]), (output, exitCode) => {
|
||||
if (serial !== _serial)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
@@ -163,7 +163,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function _fetchMaster(key, serial, m3u8Url) {
|
||||
Proc.runCommand(null, _curlCmd.concat([m3u8Url]), (output, exitCode) => {
|
||||
Proc.runCommand(null, _fetchCmd.concat([m3u8Url]), (output, exitCode) => {
|
||||
if (serial !== _serial)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
@@ -221,7 +221,7 @@ Singleton {
|
||||
|
||||
// The rendition playlist is BYTERANGE segments over one progressive mp4 (EXT-X-MAP).
|
||||
function _fetchVariant(key, serial, variantUrl) {
|
||||
Proc.runCommand(null, _curlCmd.concat([variantUrl]), (output, exitCode) => {
|
||||
Proc.runCommand(null, _fetchCmd.concat([variantUrl]), (output, exitCode) => {
|
||||
if (serial !== _serial)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
@@ -245,8 +245,8 @@ Singleton {
|
||||
// Per-process temp then atomic rename, so a concurrent download for the same
|
||||
// album (rapid track flip-flop) can't interleave writes into one file; the
|
||||
// temp is removed on failure rather than stranded as a .part.
|
||||
const script = 'mkdir -p "${1%/*}" && { test -s "$1" || { t="$1.$$.part"; curl -sSf --connect-timeout 5 --max-time 60 -o "$t" "$2" && mv -f "$t" "$1" || { rm -f "$t"; exit 1; }; }; }';
|
||||
Proc.runCommand(null, ["sh", "-c", script, "sh", path, url], (output, exitCode) => {
|
||||
const script = 'mkdir -p "${1%/*}" && { test -s "$1" || { t="$1.$$.part"; "$0" dl --connect-timeout 5 --timeout 60 -o "$t" "$2" >/dev/null && mv -f "$t" "$1" || { rm -f "$t"; exit 1; }; }; }';
|
||||
Proc.runCommand(null, ["sh", "-c", script, Proc.dmsBin, path, url], (output, exitCode) => {
|
||||
if (serial !== _serial)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
|
||||
@@ -17,7 +17,8 @@ Singleton {
|
||||
readonly property var _artHashDenylist: ["764a730860c5b8a7bbee690ee5a443672ae37dc8"]
|
||||
|
||||
function djb2Hash(str) {
|
||||
if (!str) return "";
|
||||
if (!str)
|
||||
return "";
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
||||
@@ -27,7 +28,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function _directArtworkUrl(player) {
|
||||
if (!player) return "";
|
||||
if (!player)
|
||||
return "";
|
||||
|
||||
let artUrl = player.trackArtUrl || "";
|
||||
if (artUrl !== "") {
|
||||
@@ -36,7 +38,8 @@ Singleton {
|
||||
|
||||
if (player.metadata && player.metadata["mpris:artUrl"]) {
|
||||
artUrl = player.metadata["mpris:artUrl"].toString();
|
||||
if (artUrl !== "") return artUrl;
|
||||
if (artUrl !== "")
|
||||
return artUrl;
|
||||
}
|
||||
|
||||
// YouTube publishes no artUrl; derive the thumbnail from the video id.
|
||||
@@ -102,7 +105,7 @@ Singleton {
|
||||
_commit(localFileUrl, artKey, targetUrl);
|
||||
loading = false;
|
||||
} else {
|
||||
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
|
||||
const dlCmd = "\"$0\" dl -o \"$1\" \"$2\" >/dev/null && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
|
||||
|
||||
// YouTube: try the 16:9 maxres thumbnail before falling back.
|
||||
if (targetUrl.includes("img.youtube.com/vi/")) {
|
||||
@@ -111,7 +114,7 @@ Singleton {
|
||||
const mqUrl = "https://img.youtube.com/vi/" + videoId + "/mqdefault.jpg";
|
||||
const tmpPath = filePath + ".tmp";
|
||||
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, Proc.dmsBin, tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
|
||||
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
||||
return;
|
||||
|
||||
@@ -119,7 +122,7 @@ Singleton {
|
||||
_commit(localFileUrl, artKey, targetUrl);
|
||||
loading = false;
|
||||
} else {
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, Proc.dmsBin, tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
|
||||
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
||||
return;
|
||||
|
||||
@@ -130,7 +133,7 @@ Singleton {
|
||||
}, 50, 15000);
|
||||
} else {
|
||||
const tmpPath = filePath + ".tmp";
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
|
||||
Proc.runCommand(null, ["sh", "-c", dlCmd, Proc.dmsBin, tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
|
||||
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
||||
return;
|
||||
|
||||
@@ -194,12 +197,24 @@ Singleton {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
ignoreUnknownSignals: true
|
||||
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||
function onMetadataChanged() { root._updateArtUrl(); }
|
||||
function onIsPlayingChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
function onTrackTitleChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
function onTrackArtistChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
function onTrackArtUrlChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
function onMetadataChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ Singleton {
|
||||
|
||||
// ionice is util-linux only; the BSDs get plain nice
|
||||
readonly property var lowPriorityCmd: Qt.platform.os === "linux" ? ["nice", "-n", "19", "ionice", "-c3"] : ["nice", "-n", "19"]
|
||||
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
||||
readonly property var fetchCmd: [Proc.dmsBin, "dl", "--connect-timeout", "8", "--timeout", "20"]
|
||||
|
||||
property var weatherIcons: ({
|
||||
"0": "clear_day",
|
||||
@@ -564,7 +564,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function getLocationFromCity(city) {
|
||||
cityGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([getGeocodingUrl(city)]);
|
||||
cityGeocodeFetcher.command = lowPriorityCmd.concat(fetchCmd, [getGeocodingUrl(city)]);
|
||||
cityGeocodeFetcher.running = true;
|
||||
}
|
||||
|
||||
@@ -610,21 +610,21 @@ Singleton {
|
||||
|
||||
function tryNominatim(lat, lon, reqId) {
|
||||
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
|
||||
nominatimFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url]);
|
||||
nominatimFetcher.command = lowPriorityCmd.concat(fetchCmd, ["--user-agent", "DankMaterialShell Weather Widget", url]);
|
||||
nominatimFetcher.reqId = reqId;
|
||||
nominatimFetcher.running = true;
|
||||
}
|
||||
|
||||
function tryPhoton(lat, lon, reqId) {
|
||||
const url = "https://photon.komoot.io/reverse?lat=" + lat + "&lon=" + lon + "&lang=en";
|
||||
photonFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]);
|
||||
photonFetcher.command = lowPriorityCmd.concat(fetchCmd, [url]);
|
||||
photonFetcher.reqId = reqId;
|
||||
photonFetcher.running = true;
|
||||
}
|
||||
|
||||
function tryBigDataCloud(lat, lon, reqId) {
|
||||
const url = "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=" + lat + "&longitude=" + lon + "&localityLanguage=zh";
|
||||
bigDataCloudFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]);
|
||||
bigDataCloudFetcher.command = lowPriorityCmd.concat(fetchCmd, [url]);
|
||||
bigDataCloudFetcher.reqId = reqId;
|
||||
bigDataCloudFetcher.running = true;
|
||||
}
|
||||
@@ -659,8 +659,7 @@ Singleton {
|
||||
|
||||
root.lastFetchTime = now;
|
||||
root.weather.loading = true;
|
||||
const weatherCmd = lowPriorityCmd.concat(["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "150k", "--compressed"]);
|
||||
weatherFetcher.command = weatherCmd.concat([apiUrl]);
|
||||
weatherFetcher.command = lowPriorityCmd.concat(root.fetchCmd, [apiUrl]);
|
||||
weatherFetcher.running = true;
|
||||
}
|
||||
|
||||
@@ -817,7 +816,7 @@ Singleton {
|
||||
Process {
|
||||
id: ipLocationFetcher
|
||||
running: false
|
||||
command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ip-api.com/json/"])
|
||||
command: lowPriorityCmd.concat(fetchCmd, ["http://ip-api.com/json/"])
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
|
||||
Reference in new issue
Block a user