Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
*.glb binary
*.mesh binary
*.png binary
prebuilt/* binary
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
open-pull-requests-limit: 5
86 changes: 86 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Verify tag matches Cone version
run: |
source_version=$(sed -n 's/^[[:space:]]*Version = "\([^"]*\)"/\1/p' utils/glb.go)
test "${GITHUB_REF_NAME}" = "v${source_version}"
- run: go test -race ./...
- run: go vet ./...

build:
needs: verify
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: arm64
goarch: arm64
goarm: ""
- target: armv7
goarch: arm
goarm: "7"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Build release archive
env:
BUILD_GOARCH: ${{ matrix.goarch }}
BUILD_GOARM: ${{ matrix.goarm }}
TARGET: ${{ matrix.target }}
run: |
version="${GITHUB_REF_NAME#v}"
package="cone-v${version}-linux-${TARGET}"
mkdir -p "dist/${package}"
CGO_ENABLED=0 GOOS=linux GOARCH="${BUILD_GOARCH}" GOARM="${BUILD_GOARM}" \
go build -buildvcs=false -trimpath -ldflags='-s -w' -o "dist/${package}/cone" .
go run github.com/google/go-licenses@v1.6.0 save ./... --save_path="dist/${package}/licenses"
mkdir -p "dist/${package}/licenses/fonts"
cp web/fonts/*.txt web/fonts/FONTS.md "dist/${package}/licenses/fonts/"
cp LICENSE README.md THIRD_PARTY_NOTICES.md "dist/${package}/"
tar -C dist -czf "dist/${package}.tar.gz" "${package}"
- uses: actions/upload-artifact@v4
with:
name: cone-${{ matrix.target }}
path: dist/*.tar.gz
if-no-files-found: error

publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: cone-*
path: dist
merge-multiple: true
- name: Create checksums
run: cd dist && sha256sum ./*.tar.gz > SHA256SUMS
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" \
--verify-tag \
--generate-notes \
--title "Cone ${GITHUB_REF_NAME}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
/output/
/batch-output/
/data/
/dist/
*~
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ Cone welcomes focused bug fixes and performance improvements.
4. Run `go test -race ./...`, `go vet ./...`, and `go build ./...`.
5. Open a pull request explaining the behavior change and how it was tested.

For mesh-output changes, include a small synthetic regression test. Do not commit copyrighted texture packs or generated Roblox assets.
For mesh-output changes, include a small synthetic regression test. Do not
commit copyrighted texture packs, generated Roblox assets, or compiled release
binaries. Tagged-release automation builds binaries from the reviewed source.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Cone

[![CI](https://github.com/qaustria/AutoPack-Go/actions/workflows/ci.yml/badge.svg)](https://github.com/qaustria/AutoPack-Go/actions/workflows/ci.yml)
[![Latest release](https://img.shields.io/github/v/release/qaustria/AutoPack-Go)](https://github.com/qaustria/AutoPack-Go/releases/latest)
[![License: MIT](https://img.shields.io/badge/license-MIT-orange.svg)](LICENSE)

Cone ports Minecraft 1.8.9 texture packs to Roblox. It finds supported item and block textures, resizes them to 512×512, creates edge-expanded texture variants, builds greedy-meshed item geometry, uploads the assets through Roblox Open Cloud, and returns the compressed JSON used by the game.

![Cone web interface](docs/cone-ui.png)
Expand Down Expand Up @@ -76,9 +80,15 @@ uses the same port-history database and Discord notification path as a normal
website conversion.

```bash
export CONE_BATCH_TOKEN="$(openssl rand -hex 32)"
go run . batch 'https://docs.google.com/spreadsheets/d/SHEET_ID/edit#gid=0'
```

The web server and batch command must receive the same `CONE_BATCH_TOKEN`.
Without it, administrative batch metadata is rejected so public clients cannot
forge batch progress in Discord notifications. Keep the token private and use a
separate value from every Roblox or Discord credential.

The sheet must be shared as **Anyone with the link → Viewer**, and download-link
cells must contain their complete `https://...` URL. `ROBLOX_API_KEY` and
`ROBLOX_USER_ID` select the Roblox account used for the import. The command
Expand Down Expand Up @@ -125,8 +135,25 @@ go build ./...

## Raspberry Pi

The repository includes ARMv7 and ARM64 builds used by `deploy-pi.sh`. Install
nginx once, then deploy without compiling on the Pi:
Tagged releases provide ARMv7 and ARM64 archives with SHA-256 checksums and all
required third-party license files. `deploy-pi.sh` downloads and verifies the
release matching its configured Cone version, so the Pi does not compile Go.

Store server-only values outside the checkout:

```bash
sudo install -d -m 0700 /etc/cone
sudoedit /etc/cone/cone.env
```

Example `/etc/cone/cone.env`:

```bash
CONE_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/REPLACE_ME
CONE_BATCH_TOKEN=REPLACE_WITH_OPENSSL_RAND_HEX_32
```

Then install nginx once and deploy:

```bash
sudo apt-get update && sudo apt-get install -y nginx
Expand All @@ -135,10 +162,16 @@ git pull --ff-only origin master
./deploy-pi.sh
```

Set `CONE_VERSION` only when intentionally deploying a different published
version, for example `sudo CONE_VERSION=1.5.0 ./deploy-pi.sh`.

## Security

Never commit `.env.go`. See [SECURITY.md](SECURITY.md) for credential handling and private vulnerability reporting.

Release archives include dependency licenses described in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

## License

[MIT](LICENSE)
4 changes: 4 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ Please report security issues through a [private GitHub security advisory](https
- The public web interface sends Roblox credentials only with the active conversion request.
- Cone does not write request credentials to server storage or logs.
- “Remember on this device” uses the browser's local storage and can be turned off.
- Browser local storage is not appropriate on shared or untrusted devices; turn
remembering off there and remove saved site data afterward.
- Self-hosted CLI credentials belong in the ignored `.env.go` file or environment variables.
- Always deploy Cone behind HTTPS before accepting credentials over a network.
- Administrative batch labels require a separate `CONE_BATCH_TOKEN`; never
reuse a Roblox API key or Discord webhook token for it.

Rotate a credential immediately if it is accidentally committed, pasted into an issue, or exposed in logs.

Expand Down
18 changes: 18 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Third-party notices

Cone depends on open-source packages under MIT, BSD-2-Clause,
BSD-3-Clause, and Apache-2.0 licenses. Every GitHub release archive includes
the exact dependency license files generated from the released Go dependency
graph under its `licenses/` directory.

Direct dependencies include:

- `github.com/klauspost/compress` — Apache-2.0
- `github.com/robloxapi/rbxfile` — MIT
- `go.etcd.io/bbolt` — MIT

Transitive dependencies and their license files are included automatically in
each release archive. Bundled web-font notices and full license texts are in
`web/fonts/`.

Cone's own source code is licensed under the MIT License in `LICENSE`.
31 changes: 27 additions & 4 deletions batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func runBatch(ctx context.Context, args []string) error {
if err := validateBatchEndpoint(endpoint); err != nil {
return err
}
batchToken := strings.TrimSpace(os.Getenv("CONE_BATCH_TOKEN"))
if len(batchToken) < 32 || len(batchToken) > 256 {
return errors.New("CONE_BATCH_TOKEN must contain between 32 and 256 characters")
}
if err := os.MkdirAll(outputDir, 0o750); err != nil {
return fmt.Errorf("create batch output directory: %w", err)
}
Expand Down Expand Up @@ -133,7 +137,7 @@ func runBatch(ctx context.Context, args []string) error {
fmt.Printf("[%d/%d] FAILED %s: %v\n", index+1, len(queue), entry.Name, err)
continue
}
result, err := submitBatchPackWhenAvailable(ctx, client, endpoint, credentials, zipPath, filename, index+1, len(queue), func(progress ProgressEvent) {
result, err := submitBatchPackWhenAvailable(ctx, client, endpoint, credentials, batchToken, zipPath, filename, index+1, len(queue), func(progress ProgressEvent) {
message := progress.Message
if message == "" {
message = progress.Name
Expand All @@ -150,6 +154,12 @@ func runBatch(ctx context.Context, args []string) error {
})
_ = os.Remove(zipPath)
if err != nil {
if batchAuthenticationFailed(err) {
return fmt.Errorf(
"batch paused at [%d/%d] %s because Roblox rejected the credentials: %w; replace the API key or use an unmoderated owner, then rerun the same command to resume",
index+1, len(queue), entry.Name, err,
)
}
failed++
fmt.Printf("[%d/%d] FAILED %s: %v\n", index+1, len(queue), entry.Name, err)
continue
Expand Down Expand Up @@ -201,10 +211,22 @@ func (err *batchServerError) Error() string {
return "Cone returned " + err.Status + ": " + err.Message
}

func submitBatchPackWhenAvailable(ctx context.Context, client *http.Client, endpoint string, credentials robloxCredentials, zipPath, filename string, batchIndex, batchTotal int, progress ProgressFunc) (batchSubmitResult, error) {
// Authentication failures affect every remaining pack, so continuing would
// only hammer Roblox and hide the real problem behind hundreds of failures.
// The completed checkpoint stays intact and the same command can resume once
// working credentials are supplied.
func batchAuthenticationFailed(err error) bool {
var serverError *batchServerError
if !errors.As(err, &serverError) {
return false
}
return serverError.StatusCode == http.StatusUnauthorized || serverError.StatusCode == http.StatusForbidden
}

func submitBatchPackWhenAvailable(ctx context.Context, client *http.Client, endpoint string, credentials robloxCredentials, batchToken, zipPath, filename string, batchIndex, batchTotal int, progress ProgressFunc) (batchSubmitResult, error) {
deadline := time.Now().Add(30 * time.Minute)
for {
result, err := submitBatchPack(ctx, client, endpoint, credentials, zipPath, filename, batchIndex, batchTotal, progress)
result, err := submitBatchPack(ctx, client, endpoint, credentials, batchToken, zipPath, filename, batchIndex, batchTotal, progress)
var serverError *batchServerError
if !errors.As(err, &serverError) || serverError.StatusCode != http.StatusTooManyRequests {
return result, err
Expand All @@ -227,7 +249,7 @@ func submitBatchPackWhenAvailable(ctx context.Context, client *http.Client, endp
}
}

func submitBatchPack(ctx context.Context, client *http.Client, endpoint string, credentials robloxCredentials, zipPath, filename string, batchIndex, batchTotal int, progress ProgressFunc) (batchSubmitResult, error) {
func submitBatchPack(ctx context.Context, client *http.Client, endpoint string, credentials robloxCredentials, batchToken, zipPath, filename string, batchIndex, batchTotal int, progress ProgressFunc) (batchSubmitResult, error) {
file, err := os.Open(zipPath)
if err != nil {
return batchSubmitResult{}, fmt.Errorf("open queued pack: %w", err)
Expand Down Expand Up @@ -265,6 +287,7 @@ func submitBatchPack(ctx context.Context, client *http.Client, endpoint string,
request.Header.Set(robloxUserIDHeader, credentials.UserID)
request.Header.Set(batchIndexHeader, strconv.Itoa(batchIndex))
request.Header.Set(batchTotalHeader, strconv.Itoa(batchTotal))
request.Header.Set(batchTokenHeader, batchToken)
response, err := client.Do(request)
if err != nil {
_ = reader.CloseWithError(err)
Expand Down
26 changes: 26 additions & 0 deletions batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -67,6 +68,26 @@ func TestMediaFireFolderKeyAndSourceIdentity(t *testing.T) {
}
}

func TestBatchAuthenticationFailed(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{name: "unauthorized", err: &batchServerError{StatusCode: http.StatusUnauthorized}, want: true},
{name: "forbidden", err: fmt.Errorf("submit pack: %w", &batchServerError{StatusCode: http.StatusForbidden}), want: true},
{name: "bad pack", err: &batchServerError{StatusCode: http.StatusBadRequest}, want: false},
{name: "download", err: errors.New("download failed"), want: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := batchAuthenticationFailed(test.err); got != test.want {
t.Fatalf("batchAuthenticationFailed() = %t, want %t", got, test.want)
}
})
}
}

func TestRunBatchQueuesSequentiallyAndResumes(t *testing.T) {
zipBytes := testBatchZIP(t)
zipPath := filepath.Join(t.TempDir(), "pack.zip")
Expand Down Expand Up @@ -95,6 +116,10 @@ func TestRunBatchQueuesSequentiallyAndResumes(t *testing.T) {
http.Error(response, "bad credentials", http.StatusUnauthorized)
return
}
if request.Header.Get(batchTokenHeader) != "test-batch-token-with-at-least-32-characters" {
http.Error(response, "bad batch token", http.StatusForbidden)
return
}
current := active.Add(1)
defer active.Add(-1)
for {
Expand Down Expand Up @@ -126,6 +151,7 @@ func TestRunBatchQueuesSequentiallyAndResumes(t *testing.T) {
t.Setenv("ROBLOX_API_KEY", "test-api-key")
t.Setenv("ROBLOX_USER_ID", "12345")
t.Setenv("ROBLOX_GROUP_ID", "")
t.Setenv("CONE_BATCH_TOKEN", "test-batch-token-with-at-least-32-characters")
t.Setenv("CONE_BATCH_ENDPOINT", server.URL+"/api/convert")
temporary := t.TempDir()
statePath := filepath.Join(temporary, "state", "queue.json")
Expand Down
Loading
Loading