Skip to content

Commit 4eee434

Browse files
committed
golang > python
better now
1 parent b2ab151 commit 4eee434

File tree

4 files changed

+181
-15
lines changed

4 files changed

+181
-15
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# SteamDownloaderAPI
2+
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/SyNdicateFoundation/SteamDownloaderAPI)](https://goreportcard.com/report/github.com/SyNdicateFoundation/SteamDownloaderAPI)
4+
[![Go Build](https://github.com/SyNdicateFoundation/SteamDownloaderAPI/actions/workflows/go.yml/badge.svg)](https://github.com/SyNdicateFoundation/SteamDownloaderAPI/actions/workflows/go.yml)
5+
6+
A powerful Go-based reverse proxy for the Steam Workshop that dynamically injects download buttons into workshop and collection pages, allowing you to download items directly using a backend `steamcmd` instance.
7+
8+
***
9+
10+
## Overview
11+
12+
This project acts as a middleman between you and the Steam Community website. When you browse a Steam Workshop page through this proxy, it intelligently modifies the page on-the-fly to add "Download" buttons next to the standard "Subscribe" buttons. Clicking these new buttons triggers a backend process that uses `steamcmd` to download the workshop files to your server.
13+
14+
This is perfect for server administrators, content curators, or anyone who needs to obtain workshop files without subscribing to them through the Steam client.
15+
16+
***
17+
18+
## Features
19+
20+
- **Reverse Proxy for Steam Workshop**: Forwards requests to `steamcommunity.com` seamlessly.
21+
- **Dynamic Content Injection**: Injects "Download" and "Download Collection" buttons directly into the HTML using `goquery`.
22+
- **URL Rewriting**: All asset URLs (CSS, JS, images) are rewritten to be served through the proxy, ensuring pages render correctly.
23+
- **`steamcmd` Integration**: Leverages a `steamcmd` wrapper to handle the actual download logic.
24+
- **Configurable Startup**: Use command-line flags to easily configure server settings.
25+
26+
***
27+
28+
## How It Works
29+
30+
1. The user navigates to a proxied Steam Workshop URL (e.g., `http://localhost:8080/workshop/filedetails/?id=123456789`).
31+
2. The `SteamProxyHandler` receives the request and forwards it to the official Steam servers.
32+
3. Before sending the response back to the user, the `ModifyResponse` function intercepts it.
33+
4. The HTML body is parsed. The proxy finds all "Subscribe" buttons and injects new `<a>` tags next to them, pointing to the downloader API endpoints.
34+
5. All other URLs within the page (`href`, `src`, `srcset`) are rewritten to be relative, ensuring all subsequent requests for assets also go through the proxy.
35+
6. The modified, uncompressed HTML is sent to the user's browser.
36+
7. When the user clicks a "Download" button, a request is sent to an API endpoint like `/api/workshop/:app_id/:workshop_id`.
37+
8. The API handler calls the `steamcmd` wrapper, which executes the necessary commands (`workshop_download_item`) to download the files to the server.
38+
39+
***
40+
41+
## API Endpoints
42+
43+
- `GET /api/workshop/:app_id/:workshop_id`
44+
- Triggers a download for a single workshop item.
45+
- **`app_id`**: The ID of the game (e.g., `4000` for Garry's Mod).
46+
- **`workshop_id`**: The ID of the workshop file.
47+
48+
- `GET /api/collection/:app_id/:collection_id`
49+
- Triggers a download for all items within a collection.
50+
- **`app_id`**: The ID of the game.
51+
- **`collection_id`**: The ID of the workshop collection.
52+
53+
***
54+
55+
## Setup and Installation
56+
57+
### Prerequisites
58+
59+
- [Go](https://golang.org/doc/install) (version 1.18 or newer)
60+
- A working installation of [`steamcmd`](https://developer.valvesoftware.com/wiki/SteamCMD) on the server where this API will run (or let the app install it for you).
61+
62+
### Steps
63+
64+
1. **Clone the repository:**
65+
```sh
66+
git clone https://github.com/SyNdicateFoundation/SteamDownloaderAPI.git
67+
cd SteamDownloaderAPI
68+
```
69+
70+
2. **Install dependencies:**
71+
```sh
72+
go mod tidy
73+
```
74+
75+
3. **Build the application:**
76+
```sh
77+
go build -o steamdownloaderapi ./cmd/main.go
78+
```
79+
80+
***
81+
82+
## Configuration & Usage
83+
84+
The application is configured at startup using command-line flags.
85+
86+
### Available Flags
87+
88+
- `-listenhost`: The hostname or IP address for the server to listen on. (Default: `0.0.0.0`)
89+
- `-listenport`: The port for the server to listen on. (Default: `8080`)
90+
- `-steamcmdpath`: The directory path for `steamcmd`. (Default: `steamcmd`)
91+
- `-installsteamcmd`: If `true`, the application will install or update `steamcmd` on startup. (Default: `true`)
92+
- `-debug`: Enables debug mode for more verbose logging. (Default: `false`)
93+
- `-steamuser`: Your Steam username. Required for downloading certain content. (Default: `""`, will login as anonymous)
94+
- `-steampassword`: Your Steam password. (Default: `""`)
95+
96+
### Running the Server
97+
98+
Once built, you can run the application from your terminal.
99+
100+
**Basic startup (installs steamcmd to a 'steamcmd' folder and runs on port 8080):**
101+
```sh
102+
./steamdownloaderapi -steamuser your_username -steampassword your_password

cmd/server/main.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"flag"
55
"log"
6+
"net"
67
"net/http"
78
"os"
89

@@ -11,19 +12,32 @@ import (
1112
"github.com/gin-gonic/gin"
1213
)
1314

14-
func main() {
15-
steamCmdPath := flag.String("steamcmdpath", "steamcmd", "Path to the steamcmd directory")
16-
port := flag.String("port", "8080", "Port for the server to listen on")
17-
install := flag.Bool("install", true, "Install or update steamcmd on startup")
15+
var (
16+
steamCmdPath, listenHost, listenPort, steamUser, steamPassword string
17+
installSteamCmd, debugMode bool
18+
)
19+
20+
func init() {
21+
flag.StringVar(&steamCmdPath, "steamcmdpath", "steamcmd", "Path to the steamcmd directory")
22+
flag.BoolVar(&installSteamCmd, "installsteamcmd", true, "Install steamcmd")
23+
flag.BoolVar(&debugMode, "debug", false, "Install steamcmd")
24+
flag.StringVar(&listenHost, "listenhost", "0.0.0.0", "Hostname for the server to listen on")
25+
flag.StringVar(&listenPort, "listenport", "8080", "Port for the server to listen on")
26+
flag.StringVar(&steamUser, "steamuser", "", "Steam username")
27+
flag.StringVar(&steamPassword, "steampassword", "", "Steam password")
28+
1829
flag.Parse()
30+
}
31+
32+
func main() {
1933

20-
s, err := steamcmd.New(*steamCmdPath)
34+
s, err := steamcmd.New(steamCmdPath, steamUser, steamPassword)
2135
if err != nil {
2236
log.Fatalf("❌ SteamCMD initialization error: %v", err)
2337
}
2438

25-
if *install {
26-
if err := os.MkdirAll(*steamCmdPath, 0755); err != nil {
39+
if installSteamCmd {
40+
if err := os.MkdirAll(steamCmdPath, 0755); err != nil {
2741
log.Fatalf("❌ Failed to create steamcmd directory: %v", err)
2842
}
2943

@@ -34,6 +48,12 @@ func main() {
3448

3549
router := gin.Default()
3650

51+
gin.SetMode(gin.ReleaseMode)
52+
53+
if debugMode {
54+
gin.SetMode(gin.DebugMode)
55+
}
56+
3757
h := handler.New(s)
3858
defer h.Cleanup()
3959

@@ -56,8 +76,11 @@ func main() {
5676
router.GET(route, h.UnsupportedPageHandler)
5777
}
5878

59-
log.Printf("🚀 Server starting on http://0.0.0.0:%s", *port)
60-
if err := router.Run(":" + *port); err != nil {
79+
listenAddr := net.JoinHostPort(listenHost, listenPort)
80+
81+
log.Printf("🚀 Server starting on http://%s", listenAddr)
82+
83+
if err := router.Run(listenAddr); err != nil {
6184
log.Fatalf("❌ Failed to start server: %v", err)
6285
}
6386
}

github/workflows/go-build.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Create GitHub Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout Code
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: '1.21'
24+
25+
- name: Run GoReleaser
26+
uses: goreleaser/goreleaser-action@v5
27+
with:
28+
version: latest
29+
args: release --clean
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

internal/steamcmd/steamcmd.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import (
1515
type SteamCMD struct {
1616
InstallPath string
1717
ExePath string
18+
username string
19+
password string
1820
}
1921

20-
func New(installPath string) (*SteamCMD, error) {
22+
func New(installPath string, username string, password string) (*SteamCMD, error) {
2123
exeName := "steamcmd"
2224
if runtime.GOOS == "windows" {
2325
exeName += ".exe"
@@ -33,6 +35,8 @@ func New(installPath string) (*SteamCMD, error) {
3335
return &SteamCMD{
3436
InstallPath: installPath,
3537
ExePath: absExePath,
38+
username: username,
39+
password: password,
3640
}, nil
3741
}
3842

@@ -99,10 +103,17 @@ func (s *SteamCMD) finalizeInstallation() error {
99103
}
100104

101105
func (s *SteamCMD) DownloadWorkshopItem(appID, workshopID int, validate bool) error {
102-
args := []string{
103-
"+login", "anonymous",
104-
"+workshop_download_item", fmt.Sprint(appID), fmt.Sprint(workshopID),
106+
loginInfo := []string{"+login", "anonymous"}
107+
108+
if s.username != "" {
109+
loginInfo = append(loginInfo, "+login", s.username, s.password)
105110
}
111+
112+
args := append(
113+
loginInfo,
114+
[]string{"+workshop_download_item", fmt.Sprint(appID), fmt.Sprint(workshopID)}...,
115+
)
116+
106117
if validate {
107118
args = append(args, "validate")
108119
}
@@ -114,13 +125,12 @@ func (s *SteamCMD) DownloadWorkshopItem(appID, workshopID int, validate bool) er
114125
cmd.Stderr = os.Stderr
115126

116127
if err := cmd.Run(); err != nil {
117-
118-
log.Printf(" Retrying download for WorkshopID %d...", workshopID)
119128
time.Sleep(2 * time.Second)
120129
if errRetry := cmd.Run(); errRetry != nil {
121130
return fmt.Errorf("steamcmd execution failed after retry: %w", errRetry)
122131
}
123132
}
133+
124134
return nil
125135
}
126136

0 commit comments

Comments
 (0)