Skip to content
 
 

Latest commit

 

History

1,562 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Logo Image

Discord GitHub Releases Go Report Card

Pterodactyl Wings

Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.

In addition, Wings ships with a built-in SFTP server allowing your system to remain free of Pterodactyl specific dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.

Fork Notes

This repository is a fork of pterodactyl/wings with a small set of daemon-side fixes and hardening changes. For the Rust rewrite, see Wings-rs.

Notable changes in this fork include:

  • File download requests return a proper not-found response when the target file does not exist, instead of bubbling up as an internal server error.
  • File downloads use stricter path and file-type validation before serving content.
  • File downloads and stream responses use http.ServeContent, enabling HTTP range and conditional request handling.
  • File downloads are capped to the file size observed when the request starts, matching the fixed-byte behavior used by Wings-rs-style downloads so later file growth does not extend the response.
  • Download responses include safer headers such as X-Content-Type-Options: nosniff.

Download the latest prebuilt Wings

No Git clone, Go installation, build, or source patch is needed. On Linux, run this command in a download directory to save the correct amd64/arm64 binary as ./wings (replacing that local file if it already exists):

(case "$(uname -m)" in x86_64) wings_arch=amd64 ;; aarch64|arm64) wings_arch=arm64 ;; *) printf 'Unsupported architecture\n' >&2; exit 1 ;; esac; curl -fL --retry 3 -o wings "https://github.com/Tomaxikz/daemon/releases/download/dev-latest/wings_linux_${wings_arch}" && chmod 0755 wings)

This downloads the newest published development release, which already includes this fork's changes. It is a prerelease, not a stable release: use the dev-latest URL above, not GitHub's stable-only releases/latest/download URL. Versioned downloads and SHA256SUMS are available on the releases page.

For an existing Wings installation, keep a rollback copy of the installed binary, then install the successfully downloaded file:

sudo install -m 0755 wings /usr/local/bin/wings

Restart your Wings service separately when ready to activate the new binary. These commands do not change your configuration or apply any source patches.

Release pipeline

.github/workflows/push.yaml owns testing, packaging and publishing. Go versions come from go.mod. Pull requests run with read-only repository permissions; only the final publishing job on this repository has contents: write.

Before a release can publish, the workflow must complete:

  • Native Linux amd64 and arm64 tests, race tests, release/debug builds, and binary version smoke tests.
  • Clean application and builds of betterfiles.patch, betterconsole.patch, nsm.patch, and tomaxikz.patch against supported upstream commit 6987d5e.
  • Clean, repeat, and upgrade runs of all three smart installers, using source files from the same checkout. The upgrade fixtures use patches from 60f5d30.
  • Release-tool regression tests, source consistency checks, and bundle checksum verification. CodeQL remains a separate scheduled analysis workflow.

Every bundle contains both architectures' normal/debug binaries, all four patches, all three installers, README.md, RELEASE.json, and portable SHA256SUMS. Installers attached to releases fetch their source files from that exact commit; the repository copies continue to default to develop. TOMAXIKZ_RAW_BASE still allows an explicit source override.

Pushes to develop publish a versioned prerelease named dev-<full commit SHA> before updating the existing dev-latest download URLs. Versioned releases are never overwritten by this pipeline. Publishing is serialized across branches, and an older run cannot promote itself when develop has advanced. The alias is temporarily hidden while its assets are replaced and verified. Ordinary failures restore the previous alias; a forced runner termination may require rerunning the publishing job. Versioned snapshots remain available during alias updates.

For a stable release, push a tag such as v1.2.3. Tags such as v1.2.3-rc.1 publish prereleases. Both use the same checks and bundling process. Older stable versions do not displace a newer stable version as GitHub's latest release. There are no automatic release-branch commits or requests to Pterodactyl's upstream CDN.

No custom publishing secret is needed: the workflow uses its repository-scoped GITHUB_TOKEN. The repository's rolling dev-latest release must remain mutable; enabling repository-wide release immutability is incompatible with this alias. The workflow itself refuses to move or replace versioned releases. To roll back, download a binary from a previous versioned release, replace Wings, and restart it.

Local validation on Linux with Go from go.mod:

python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v
python3 .github/scripts/validate_installers.py betterfiles
python3 .github/scripts/validate_installers.py betterconsole
python3 .github/scripts/validate_installers.py networkstats
python3 .github/scripts/validate_installers.py full
python3 .github/scripts/release.py build --arch amd64 --directory /tmp/wings-build

The build command requires the matching native Linux architecture. Publishing is restricted to trusted GitHub workflow events; local validation never publishes.

Sponsors

I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's development. Interested in becoming a sponsor?

Company About
Infraly, LLC Infraly is an infrastructure company powering the next generation of online services. Through their brands, Infraly delivers cutting-edge solutions across multiple markets. Their vertically integrated approach provides unmatched performance, scalability, and reliability, giving our customers full control.
Hosturly Hosturly is an enterprise hosting provider. They provide cost-effective, high-performance, and reliable services, including VPS, Web, Dedicated, and Colocation.
Physgun Physgun is a game server hosting provider. Most providers rent rack space and rebrand a panel. At Physgun, they engineer the performance, write the features, and staff the support. Physgun truly is game hosting perfected!
WISP WISP is an industry-leading SaaS platform for game server management, designed for hosting companies, gaming organizations, and enthusiasts. WISP combines modern, intuitive interfaces with powerful tools, making server deployment and administration seamless, scalable, and efficient.
Buildurly Buildurly is a hardware procurement company. They deliver tailored, enterprise-grade hardware solutions designed around your unique needs. From sourcing to delivery, Buildurly's white-glove service ensures a seamless, worry-free, professional experience.
indifferent broccoli indifferent broccoli is a game server hosting and rental company. With them, you get top-notch computer power for your gaming sessions. They destroy lag, latency, and complexity--letting you focus on the fun stuff.

Documentation

Advanced file search (Better Files / Wings-rs V2)

The existing authenticated POST /api/servers/{server}/files/search accepts the BFM V2 JSON contract. The legacy GET search remains unchanged. For example:

{
  "root": "/plugins",
  "path_filter": {
    "include": ["**/*.yml"],
    "exclude": ["**/cache/**"],
    "case_insensitive": true
  },
  "size_filter": {"min": 0, "max": 10485760},
  "content_filter": {
    "query": "enabled",
    "max_search_size": 2097152,
    "include_unmatched": false,
    "case_insensitive": true
  },
  "per_page": 100
}

All three filters may be omitted or null. Empty, omitted or null root means /; omitted or null per_page defaults to 100. Results use the existing directory entry fields: name, size, size_physical, directory, file, symlink, mime, created, and modified. Only regular files are returned. A file at /plugins/a/config.yml has name: "a/config.yml" for this root; another at /plugins/b/config.yml remains a separate result. Empty results are exactly {"results":[]}. Ordering is unspecified; per_page caps results, not a pageable cursor. Physical size and creation/change time fall back to logical size and modification time when that metadata is unavailable.

Compatibility follows the BFM-consumed subset of Calagopus Wings' search implementation at d3eced7:

  • Globs use gitignore-style rules against paths from the server filesystem root, not the requested search directory. *.yml matches basenames at any depth; /plugins/*.yml is root-anchored. **, character classes, single-component brace alternatives, escaped leading #/!, comments and ordered ! negation are supported. Exclusions win over includes. Excluded directories are pruned, so later rules cannot re-include their descendants. A trailing / matches directories only; cache/** matches descendants, not cache itself.
  • Empty/omitted/null include and exclude arrays apply no restriction. Other include arrays require a positive matching rule (an array containing only comments or negative rules matches no files). Path and content case-insensitive matching fold ASCII only. Glob ? matches one byte, not one Unicode character.
  • Size min is inclusive and max is exclusive. Omitted/null bounds are unbounded. Equal bounds match nothing; negative or reversed bounds are errors.
  • Content queries are literal UTF-8, not regular expressions. The first 128 bytes provide the upstream UTF-8 heuristic: an incomplete trailing character is accepted, NUL bytes are allowed, and invalid UTF-8 after that prefix does not disqualify a file. Matching streams through bounded buffers, preserves matches across buffer boundaries, and stops on the first match.
  • max_search_size defaults to 2 MiB when omitted/null. Files larger than it are not content-searched. Explicit zero is preserved. include_unmatched: true includes these oversized/unsearched files (even binary files), not searched nonmatches or files rejected by the UTF-8 head check. Empty/omitted/null queries match eligible files passing that check, including empty files. Both content booleans default to false, as does path case_insensitive.

Search uses Wings' sandbox and denylist, refuses symlink roots and skips symlinks and special files. Directory traversal uses bounded batches and pinned directory handles. Unopenable regular files are skipped; read failures abort the request. Resource limits deliberately bound the upstream-compatible subset:

Limit Value
JSON body 1 MiB
Results Default 100; maximum 500 (larger values capped); zero returns []
Include + exclude patterns 128 total; 64 KiB combined; 4096 UTF-8 bytes each
Brace expansion 64 combinations per pattern; no nested/empty or cross-directory alternatives
Root / content query 4096 UTF-8 bytes each; root path components at most 255 bytes
Entries / directories / depth below root 250,000 / 16,384 including root / 256
Content size threshold Default 2 MiB; accepted range 0–1 GiB per file
Total file reads 1 GiB per request, including up to 128 MIME bytes per unsearched file
Search execution 30 seconds, also canceled when the request context ends

Per-file content reads stay within max_search_size, even if the file grows. Unknown fields (including match_context), V1 POST payloads, malformed JSON and wrong types return 400; an oversized body returns 413. Invalid roots, filters or exhausted traversal/read budgets return 422, forbidden roots 403, missing roots 404, and cancellation/timeouts 408. Execution errors return no partial result list. Archive/backup mounting and optional match context are unsupported; this endpoint searches only the live server filesystem. Unsupported or invalid globs are rejected, never silently dropped.

BFM detects advanced search from the actual GET /openapi.json response:

const properties = openapi.paths?.['/api/servers/{server}/files/search']?.post
    ?.requestBody?.content?.['application/json']?.schema?.properties;
const canSearchV2 = ['path_filter', 'size_filter', 'content_filter', 'per_page']
    .every((key) => Object.prototype.hasOwnProperty.call(properties ?? {}, key));

A version string, legacy GET operation or schema-less POST is insufficient. The inline schema describes the nested contract and response; x-search-limits reports execution bounds. No Panel API changes or capability-check weakening are needed. Rebuild and replace the daemon binary, then restart it when deploying. BFM's backend capability cache lasts 60 seconds and its frontend cache 30 seconds; allow both to expire (up to about 90 seconds in sequence) and reload the file manager.

Source installations can use the updated betterfiles.patch or tomaxikz.patch. betterfiles_patch.sh already downloads these handler, OpenAPI and test files in place, so its logic does not need changing. Use files from the same updated revision/release, then rebuild; updating documentation alone changes no running daemon. Binary installations need only the newly built daemon, not a source patch.

Multipart folder uploads (Better Files)

POST /upload/file accepts the existing signed upload URL and directory query parameter. A request without a paths field keeps the normal flat-upload behavior. Resumable HEAD/PATCH uploads keep their existing contract.

Folder batches add one multipart text field named paths, containing a JSON array of relative paths. Its entries match the repeated files parts in order. The field may appear before or after the file parts. Do not rely on a filename such as mods/a/config.yml: Go's multipart parser strips its directory components.

const form = new FormData();
form.append('files', firstConfigFile, 'config.yml');
form.append('files', secondConfigFile, 'config.yml');
form.append('paths', JSON.stringify(['mods/a/config.yml', 'mods/b/config.yml']));

const url = new URL(signedUploadUrl);
url.searchParams.set('directory', '/uploads');
await fetch(url, { method: 'POST', body: form });
// Let the browser supply Content-Type and its multipart boundary.

This writes /uploads/mods/a/config.yml and /uploads/mods/b/config.yml. Missing parent directories are created with server ownership. Paths use /, are relative to directory, and must have the same basename as the corresponding file part. They must not contain empty, . or .. components, backslashes, NUL, a leading slash, or a Windows drive prefix. Paths are literal JSON strings, not URL-encoded values. Invalid UTF-8, paths over 4096 bytes, components over 255 bytes, duplicate destinations, and file/directory conflicts are rejected. Duplicate basenames in different directories are supported.

Folder batches accept at most 512 files per request. File bytes must fit both the configured upload limit (per file and in total) and the server's disk quota. The HTTP body is additionally bounded by the upload limit plus 8 MiB for multipart metadata. The client-supplied total_size does not override these checks. Ignored files and directories are enforced, and writes refuse symlinks, hardlinks and non-regular targets. Disk growth is reserved before overwriting each file.

Use one fresh single-use upload token for the entire POST request. Tokens are consumed even when validation fails; never reuse one for a retry or for resumable uploads. Success remains an empty 200 response. Invalid input/limits return 400, denied paths 403, unavailable/consumed tokens 404, an active same-file upload 409, and an oversized HTTP body 413. Batch writes are not transactional: validation is performed before writing, but a runtime failure may leave earlier files written. Retry with a fresh token after inspecting the error.

BFM must detect support from GET /openapi.json before sending paths:

const support = openapi.paths?.['/upload/file']?.post?.['x-betterfiles-folder-upload'];
const canUploadFolders = support?.version === 1 && support.paths_field === 'paths';
// support.max_files is 512; split larger selections into separate requests/tokens.

Checking for POST /upload/file alone is insufficient: older Wings versions may ignore paths and flatten the batch. If the capability is absent, keep the existing per-directory multipart uploads or per-file resumable uploads.

Source installations can use betterfiles_patch.sh, betterfiles.patch, or the full tomaxikz.patch, then rebuild and replace Wings. The smart installer updates recognized upload implementations in place; custom upload modifications require a reviewed merge. The new supporting files are server/filesystem/upload.go, server/filesystem/upload_test.go, and router/router_upload_batch_test.go.

Reporting Issues

Please use the pterodactyl/panel repository to report any issues or make feature requests for Wings. In addition, the security policy listed within that repository also applies to Wings.

About

The server control plane for Pterodactyl Panel. Written from the ground-up with security, speed, and stability in mind.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages