mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-05 19:51:59 +02:00
Merge branch 'main' into fixes-v3
This commit is contained in:
2
.github/docker/entrypoint.sh
vendored
2
.github/docker/entrypoint.sh
vendored
@@ -140,7 +140,7 @@ fi
|
||||
fi
|
||||
# Make a location and node for the panel
|
||||
php artisan p:location:make -n --short local --long Local
|
||||
php artisan p:node:make -n --name local --description "Development Node" --locationId 1 --fqdn localhost --internal-fqdn $ELYTRA_INTERNAL_IP --public 1 --scheme http --proxy 0 --maxMemory 1024 --maxDisk 10240 --overallocateMemory 0 --overallocateDisk 0
|
||||
php artisan p:node:make -n --name local --description "Development Node" --locationId 1 --fqdn localhost --internal-fqdn $ELYTRA_INTERNAL_IP --public 1 --scheme http --proxy 0 --maxMemory 1024 --maxDisk 10240 --overallocateMemory 0 --overallocateDisk 0 --daemonType elytra
|
||||
|
||||
echo "Adding dummy allocations..."
|
||||
if [ "$DB_CONNECTION" = "mysql" ] || [ "$DB_CONNECTION" = "mariadb" ]; then
|
||||
|
||||
BIN
.github/server-menu.png
vendored
BIN
.github/server-menu.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 792 KiB After Width: | Height: | Size: 137 KiB |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.git/
|
||||
.vscode/
|
||||
.env
|
||||
.github/workflows/ci.yaml
|
||||
|
||||
|
||||
# Elytra binary
|
||||
@@ -56,5 +57,4 @@ nix/docker/wings/etc/
|
||||
nix/docker/wings/lib/
|
||||
nix/docker/maria/mariadb_data/
|
||||
nix/mariadb/
|
||||
wings/
|
||||
mariadb_data/
|
||||
|
||||
862
LICENSE.md
862
LICENSE.md
@@ -1,661 +1,201 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Pyrodactyl-oss, and Pyro, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -66,4 +66,4 @@ Pterodactyl® Copyright © 2015 - 2022 Dane Everitt and contributors.
|
||||
|
||||
Pyrodactyl™ Copyright © 2023-Present Pyro Inc. and contributors.
|
||||
|
||||
AGPL-3.0-or-later
|
||||
Apache-2.0
|
||||
|
||||
@@ -24,7 +24,9 @@ class MakeNodeCommand extends Command
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the wings listening port.}
|
||||
{--daemonSFTPPort= : Enter the wings SFTP listening port.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
{--daemonBase= : Enter the base folder.}
|
||||
{--daemonType= : Enter the daemon Backend.}
|
||||
{--backupDisk= : Enter the Backup type}';
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
|
||||
@@ -64,6 +66,8 @@ class MakeNodeCommand extends Command
|
||||
$data['daemonListen'] = $this->option('daemonListeningPort') ?? $this->ask('Enter the wings listening port', '8080');
|
||||
$data['daemonSFTP'] = $this->option('daemonSFTPPort') ?? $this->ask('Enter the wings SFTP listening port', '2022');
|
||||
$data['daemonBase'] = $this->option('daemonBase') ?? $this->ask('Enter the base folder', '/var/lib/pterodactyl/volumes');
|
||||
$data['daemonType'] = $this->option('daemonType') ?? $this->ask('Enter the daemon backend', 'elytra');
|
||||
$data['backupDisk'] = $this->option('backupDisk') ?? $this->ask('Enter the Backup Disk', 'rustic_local');
|
||||
|
||||
$node = $this->creationService->handle($data);
|
||||
$this->line('Successfully created a new node on the location ' . $data['location_id'] . ' with the name ' . $data['name'] . ' and has an id of ' . $node->id . '.');
|
||||
|
||||
11
app/Contracts/Daemon/Daemon.php
Normal file
11
app/Contracts/Daemon/Daemon.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Contracts\Daemon;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
|
||||
interface Daemon
|
||||
{
|
||||
public function getConfiguration(Node $node): array;
|
||||
public function getAutoDeploy(Node $node, string $token): string;
|
||||
}
|
||||
34
app/Enums/Captcha/Captchas.php
Normal file
34
app/Enums/Captcha/Captchas.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Captcha;
|
||||
|
||||
enum Captchas: string
|
||||
{
|
||||
case NONE = 'none';
|
||||
case TURNSTILE = 'turnstile';
|
||||
case HCAPTCHA = 'hcaptcha';
|
||||
case RECAPTCHA = 'recaptcha';
|
||||
|
||||
private const DESCRIPTION_MAP = [
|
||||
self::NONE->value => 'Disabled',
|
||||
self::TURNSTILE->value => 'Cloudflare Turnstile',
|
||||
self::HCAPTCHA->value => 'HCaptcha',
|
||||
self::RECAPTCHA->value => 'Google ReCaptcha',
|
||||
];
|
||||
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$result[$case->value] = self::DESCRIPTION_MAP[$case->value];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
52
app/Enums/Daemon/Adapters.php
Normal file
52
app/Enums/Daemon/Adapters.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Daemon;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
enum Adapters: string
|
||||
{
|
||||
|
||||
case ADAPTER_WINGS = 'wings';
|
||||
case ADAPTER_WINGS_S3 = 's3';
|
||||
case ADAPTER_ELYTRA = 'elytra';
|
||||
case ADAPTER_RUSTIC_LOCAL = 'rustic_local';
|
||||
case ADAPTER_RUSTIC_S3 = 'rustic_s3';
|
||||
|
||||
private const ELYTRA = [
|
||||
self::ADAPTER_ELYTRA, // NOTE: This is local storage without Rustic
|
||||
self::ADAPTER_RUSTIC_LOCAL,
|
||||
self::ADAPTER_RUSTIC_S3,
|
||||
];
|
||||
|
||||
private const WINGS = [
|
||||
self::ADAPTER_WINGS, // NOTE: This is local storage
|
||||
self::ADAPTER_WINGS_S3,
|
||||
];
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value', 'value');
|
||||
}
|
||||
|
||||
public static function all_sorted(): array
|
||||
{
|
||||
return ['elytra' => self::all_elytra(), 'wings' => self::all_wings()];
|
||||
}
|
||||
|
||||
|
||||
public static function all_elytra(): array
|
||||
{
|
||||
return array_column(self::ELYTRA, "value");
|
||||
}
|
||||
|
||||
public static function all_wings(): array
|
||||
{
|
||||
return array_column(self::WINGS, "value");
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
39
app/Enums/Daemon/DaemonType.php
Normal file
39
app/Enums/Daemon/DaemonType.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Daemon;
|
||||
|
||||
enum DaemonType: string
|
||||
{
|
||||
case WINGS = 'wings';
|
||||
case ELYTRA = 'elytra';
|
||||
|
||||
private const CLASS_MAP = [
|
||||
self::WINGS->value => \Pterodactyl\Models\Daemons\Wings::class,
|
||||
self::ELYTRA->value => \Pterodactyl\Models\Daemons\Elytra::class,
|
||||
];
|
||||
|
||||
private const RESOURCE_MAP = [
|
||||
self::WINGS->value => \Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra\ResourceUtilizationController::class,
|
||||
self::ELYTRA->value => \Pterodactyl\Http\Controllers\Api\Client\Servers\Wings\ResourceUtilizationController::class,
|
||||
];
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value', 'value');
|
||||
}
|
||||
|
||||
public static function allResources(): array
|
||||
{
|
||||
return self::RESOURCE_MAP;
|
||||
}
|
||||
|
||||
public static function allClass(): array
|
||||
{
|
||||
return self::CLASS_MAP;
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
65
app/Enums/Limits/ResourceLimit.php
Normal file
65
app/Enums/Limits/ResourceLimit.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Limits;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
|
||||
/**
|
||||
* A basic resource throttler for individual servers. This is applied in addition
|
||||
* to existing rate limits and allows the code to slow down speedy users that might
|
||||
* be creating resources a little too quickly for comfort. This throttle generally
|
||||
* only applies to creation flows, and not general view/edit/delete flows.
|
||||
*/
|
||||
enum ResourceLimit
|
||||
{
|
||||
case Allocation;
|
||||
case Backup;
|
||||
case Database;
|
||||
case Schedule;
|
||||
case Subuser;
|
||||
case Websocket;
|
||||
case FilePull;
|
||||
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return mb_strtolower("api.client:server-resource:{$this->name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a middleware that will throttle the specific resource by server. This
|
||||
* throttle applies to any user making changes to that resource on the specific
|
||||
* server, it is NOT per-user.
|
||||
*/
|
||||
public function middleware(): string
|
||||
{
|
||||
return ThrottleRequests::using($this->throttleKey());
|
||||
}
|
||||
|
||||
public function limit(): Limit
|
||||
{
|
||||
return match ($this) {
|
||||
self::Backup => Limit::perMinutes(15, 3),
|
||||
self::Database => Limit::perMinute(2),
|
||||
self::FilePull => Limit::perMinutes(10, 5),
|
||||
self::Subuser => Limit::perMinutes(15, 10),
|
||||
self::Websocket => Limit::perMinute(5),
|
||||
default => Limit::perMinute(2),
|
||||
};
|
||||
}
|
||||
|
||||
public static function boot(): void
|
||||
{
|
||||
foreach (self::cases() as $case) {
|
||||
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
|
||||
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
|
||||
|
||||
return $case->limit()->by($server->uuid);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
55
app/Enums/Subdomain/Features.php
Normal file
55
app/Enums/Subdomain/Features.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Subdomain;
|
||||
|
||||
use Pterodactyl\Services\Subdomain\Features\FactorioSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\MinecraftSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\RustSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\ScpSlSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\TeamSpeakSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\VintageStorySubdomainFeature;
|
||||
|
||||
|
||||
enum Features: string
|
||||
{
|
||||
|
||||
case FACTORIO = "subdomain_factorio";
|
||||
case MINECRAFT = "subdomain_minecraft";
|
||||
case RUST = "subdomain_rust";
|
||||
case SCPSL = "subdomain_scpsl";
|
||||
case TEAMSPEAK = "subdomain_teamspeak";
|
||||
case VINTAGESTORY = "subdomain_vintagestory";
|
||||
|
||||
private const CLASS_MAP = [
|
||||
self::FACTORIO->value => FactorioSubdomainFeature::class,
|
||||
self::MINECRAFT->value => MinecraftSubdomainFeature::class,
|
||||
self::RUST->value => RustSubdomainFeature::class,
|
||||
self::SCPSL->value => ScpSlSubdomainFeature::class,
|
||||
self::TEAMSPEAK->value => TeamSpeakSubdomainFeature::class,
|
||||
self::VINTAGESTORY->value => VintageStorySubdomainFeature::class,
|
||||
];
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$result[$case->value] = $case->getClassName();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return self::CLASS_MAP[$this->value];
|
||||
}
|
||||
|
||||
public static function getClass(string $provider): string
|
||||
{
|
||||
return self::from($provider)->getClassName();
|
||||
}
|
||||
}
|
||||
68
app/Enums/Subdomain/Providers.php
Normal file
68
app/Enums/Subdomain/Providers.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Enums\Subdomain;
|
||||
|
||||
use Pterodactyl\Services\Dns\Providers\CloudflareProvider;
|
||||
use Pterodactyl\Services\Dns\Providers\HetznerProvider;
|
||||
use Pterodactyl\Services\Dns\Providers\Route53Provider;
|
||||
|
||||
enum Providers: string
|
||||
{
|
||||
|
||||
case CLOUDFLARE = 'cloudflare';
|
||||
case HETZNER = 'hetzner';
|
||||
case ROUTE53 = 'route53';
|
||||
|
||||
private const CLASS_MAP = [
|
||||
self::CLOUDFLARE->value => CloudflareProvider::class,
|
||||
self::HETZNER->value => HetznerProvider::class,
|
||||
self::ROUTE53->value => Route53Provider::class,
|
||||
];
|
||||
|
||||
private const DESCRIPTION_MAP = [
|
||||
self::CLOUDFLARE->value => 'Cloudflare DNS service',
|
||||
self::HETZNER->value => 'Hetzner DNS Console',
|
||||
self::ROUTE53->value => 'AWS Route53 Dns Service',
|
||||
];
|
||||
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$result[$case->value] = self::getClass($case->value);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns providers with name and description
|
||||
*/
|
||||
public static function allWithDescriptions(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$result[$case->value] = [
|
||||
"name" => $case->value,
|
||||
"description" => self::DESCRIPTION_MAP[$case->value]
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return self::CLASS_MAP[$this->value];
|
||||
}
|
||||
|
||||
public static function getClass(string $provider): string
|
||||
{
|
||||
return self::from($provider)->getClassName();
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,7 @@ class NodeAutoDeployController extends Controller
|
||||
private ApiKeyRepository $repository,
|
||||
private Encrypter $encrypter,
|
||||
private KeyCreationService $keyCreationService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates a new API key for the logged-in user with only permission to read
|
||||
|
||||
@@ -8,28 +8,47 @@ use Pterodactyl\Models\Node;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NodeController extends Controller
|
||||
{
|
||||
/**
|
||||
* NodeController constructor.
|
||||
*/
|
||||
public function __construct(private ViewFactory $view)
|
||||
{
|
||||
}
|
||||
/**
|
||||
* NodeController constructor.
|
||||
*/
|
||||
public function __construct(private ViewFactory $view) {}
|
||||
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->with('location')->withCount('servers')
|
||||
)
|
||||
->allowedFilters(['uuid', 'name'])
|
||||
->allowedSorts(['id'])
|
||||
->paginate(25);
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->with('location')->withCount('servers')
|
||||
)
|
||||
->allowedFilters(['uuid', 'name'])
|
||||
->allowedSorts(['id'])
|
||||
->paginate(25);
|
||||
|
||||
return $this->view->make('admin.nodes.index', ['nodes' => $nodes]);
|
||||
}
|
||||
foreach ($nodes as $node) {
|
||||
$stats = app('Pterodactyl\Repositories\Eloquent\NodeRepository')->getUsageStatsRaw($node);
|
||||
// NOTE: Pre-creating stats so we donn't do it in the blade
|
||||
|
||||
$memoryPercent = ($stats['memory']['value'] / $stats['memory']['base_limit']) * 100;
|
||||
$diskPercent = ($stats['disk']['value'] / $stats['disk']['base_limit']) * 100;
|
||||
|
||||
$node->memory_percent = round($memoryPercent);
|
||||
$node->memory_color = $memoryPercent < 50 ? '#50af51' : ($memoryPercent < 70 ? '#e0a800' : '#d9534f');
|
||||
$node->allocated_memory = humanizeSize($stats['memory']['value'] * 1024 * 1024);
|
||||
$node->total_memory = humanizeSize($stats['memory']['max'] * 1024 * 1024);
|
||||
|
||||
$node->disk_percent = round($diskPercent);
|
||||
$node->disk_color = $diskPercent < 50 ? '#50af51' : ($diskPercent < 70 ? '#e0a800' : '#d9534f');
|
||||
$node->allocated_disk = humanizeSize($stats['disk']['value'] * 1024 * 1024);
|
||||
$node->total_disk = humanizeSize($stats['disk']['max'] * 1024 * 1024);
|
||||
}
|
||||
|
||||
|
||||
return $this->view->make('admin.nodes.index', ['nodes' => $nodes]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ use Pterodactyl\Services\Helpers\SoftwareVersionService;
|
||||
use Pterodactyl\Repositories\Eloquent\LocationRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Pterodactyl\Enums\Daemon\DaemonType;
|
||||
use Pterodactyl\Enums\Daemon\Adapters;
|
||||
|
||||
class NodeViewController extends Controller
|
||||
{
|
||||
@@ -39,10 +41,11 @@ class NodeViewController extends Controller
|
||||
public function index(Request $request, Node $node): View
|
||||
{
|
||||
$node = $this->repository->loadLocationAndServerCount($node);
|
||||
$stats = $this->repository->getUsageStats($node);
|
||||
|
||||
return $this->view->make('admin.nodes.view.index', [
|
||||
'node' => $node,
|
||||
'stats' => $this->repository->getUsageStats($node),
|
||||
'stats' => $stats,
|
||||
'version' => $this->versionService,
|
||||
]);
|
||||
}
|
||||
@@ -55,6 +58,8 @@ class NodeViewController extends Controller
|
||||
return $this->view->make('admin.nodes.view.settings', [
|
||||
'node' => $node,
|
||||
'locations' => $this->locationRepository->all(),
|
||||
'daemonTypes' => DaemonType::all(),
|
||||
'backupDisks' => Adapters::all_sorted(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ class NodeViewController extends Controller
|
||||
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
|
||||
|
||||
switch (DB::getPdo()->getAttribute(DB::getPdo()::ATTR_DRIVER_NAME)) {
|
||||
case 'mysql':
|
||||
default:
|
||||
return $this->view->make('admin.nodes.view.allocation', [
|
||||
'node' => $node,
|
||||
'allocations' => Allocation::query()->where('node_id', $node->id)
|
||||
|
||||
@@ -9,6 +9,8 @@ use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Enums\Daemon\Adapters;
|
||||
use Pterodactyl\Enums\Daemon\DaemonType;
|
||||
use Illuminate\View\Factory as ViewFactory;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Nodes\NodeUpdateService;
|
||||
@@ -45,8 +47,7 @@ class NodesController extends Controller
|
||||
protected NodeUpdateService $updateService,
|
||||
protected SoftwareVersionService $versionService,
|
||||
protected ViewFactory $view,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Displays create new node page.
|
||||
@@ -60,7 +61,7 @@ class NodesController extends Controller
|
||||
return redirect()->route('admin.locations');
|
||||
}
|
||||
|
||||
return $this->view->make('admin.nodes.new', ['locations' => $locations]);
|
||||
return $this->view->make('admin.nodes.new', ['locations' => $locations, 'daemonTypes' => DaemonType::all(), 'backupDisks' => Adapters::all_sorted()]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,8 +25,7 @@ class CreateServerController extends Controller
|
||||
private NodeRepository $nodeRepository,
|
||||
private ServerCreationService $creationService,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Displays the create server page.
|
||||
|
||||
@@ -16,9 +16,7 @@ class ServerController extends Controller
|
||||
/**
|
||||
* ServerController constructor.
|
||||
*/
|
||||
public function __construct(private ViewFactory $view)
|
||||
{
|
||||
}
|
||||
public function __construct(private ViewFactory $view) {}
|
||||
|
||||
/**
|
||||
* Returns all the servers that exist on the system using a paginated result set. If
|
||||
|
||||
@@ -10,6 +10,7 @@ use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Servers\EnvironmentService;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Repositories\Eloquent\NestRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
||||
@@ -34,8 +35,7 @@ class ServerViewController extends Controller
|
||||
private ServerRepository $repository,
|
||||
private EnvironmentService $environmentService,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the index view for a server.
|
||||
|
||||
@@ -12,6 +12,7 @@ use Pterodactyl\Services\Captcha\CaptchaManager;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
|
||||
use Pterodactyl\Http\Requests\Admin\Settings\CaptchaSettingsFormRequest;
|
||||
use Pterodactyl\Enums\Captcha\Captchas;
|
||||
|
||||
class CaptchaController extends Controller
|
||||
{
|
||||
@@ -32,13 +33,9 @@ class CaptchaController extends Controller
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
|
||||
return $this->view->make('admin.settings.captcha', [
|
||||
'providers' => [
|
||||
'none' => 'Disabled',
|
||||
'turnstile' => 'Cloudflare Turnstile',
|
||||
'hcaptcha' => 'hCaptcha',
|
||||
'recaptcha' => 'Google reCAPTCHA',
|
||||
],
|
||||
'providers' => Captchas::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -51,13 +48,13 @@ class CaptchaController extends Controller
|
||||
public function update(CaptchaSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
$values = $request->normalize();
|
||||
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
// Encrypt secret keys before storing
|
||||
if (in_array($key, \Pterodactyl\Providers\SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
|
||||
$value = $this->encrypter->encrypt($value);
|
||||
}
|
||||
|
||||
|
||||
$this->settings->set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use Pterodactyl\Services\Subdomain\SubdomainManagementService;
|
||||
use Pterodactyl\Exceptions\Dns\DnsProviderException;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Pterodactyl\Http\Requests\Admin\Settings\DomainFormRequest;
|
||||
use Pterodactyl\Enums\Subdomain\Providers;
|
||||
|
||||
class DomainsController extends Controller
|
||||
{
|
||||
@@ -220,20 +221,7 @@ class DomainsController extends Controller
|
||||
*/
|
||||
private function getAvailableProviders(): array
|
||||
{
|
||||
return [
|
||||
'cloudflare' => [
|
||||
'name' => 'Cloudflare',
|
||||
'description' => 'Cloudflare DNS service',
|
||||
],
|
||||
'hetzner' => [
|
||||
'name' => 'Hetzner',
|
||||
'description' => 'Hetzner DNS Console',
|
||||
],
|
||||
'route53' => [
|
||||
'name' => 'Route53',
|
||||
'description' => 'AWS Route53 Dns Service',
|
||||
],
|
||||
];
|
||||
return Providers::allWithDescriptions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,11 +229,7 @@ class DomainsController extends Controller
|
||||
*/
|
||||
private function getProviderClass(string $provider): string
|
||||
{
|
||||
$providers = [
|
||||
'cloudflare' => \Pterodactyl\Services\Dns\Providers\CloudflareProvider::class,
|
||||
'hetzner' => \Pterodactyl\Services\Dns\Providers\HetznerProvider::class,
|
||||
'route53' => \Pterodactyl\Services\Dns\Providers\Route53Provider::class,
|
||||
];
|
||||
$providers = Providers::all();
|
||||
|
||||
if (!isset($providers[$provider])) {
|
||||
throw new \Exception("Unsupported DNS provider: {$provider}");
|
||||
@@ -254,4 +238,3 @@ class DomainsController extends Controller
|
||||
return $providers[$provider];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class ClientController extends ClientApiController
|
||||
'name',
|
||||
'description',
|
||||
'external_id',
|
||||
'daemonType',
|
||||
AllowedFilter::custom('*', new MultiFieldServerFilter()),
|
||||
]);
|
||||
|
||||
|
||||
53
app/Http/Controllers/Api/Client/ServerController.php
Normal file
53
app/Http/Controllers/Api/Client/ServerController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\GetServerRequest;
|
||||
use Pterodactyl\Enums\Daemon\DaemonType;
|
||||
|
||||
class ServerController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* ServerController constructor.
|
||||
*/
|
||||
public function __construct(private GetUserPermissionsService $permissionsService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an individual server into a response that can be consumed by a
|
||||
* client using the API.
|
||||
*/
|
||||
public function index(GetServerRequest $request, Server $server): array
|
||||
{
|
||||
$server->loadMissing('node');
|
||||
|
||||
$daemonType = $server->node?->daemonType;
|
||||
return $this->fractal->item($server)
|
||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
||||
->addMeta([
|
||||
'daemonType' => $daemonType,
|
||||
'is_server_owner' => $request->user()->id === $server->owner_id,
|
||||
'user_permissions' => $this->permissionsService->handle($server, $request->user()),
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function resources(GetServerRequest $request, Server $server): array
|
||||
{
|
||||
$server->loadMissing('node');
|
||||
|
||||
$daemonType = $server->node?->daemonType ?? 'elytra';
|
||||
$controllers = DaemonType::allResources();
|
||||
$controllerClass = $controllers[$daemonType];
|
||||
|
||||
$controller = app($controllerClass);
|
||||
return $controller->__invoke($request, $server);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Models\ActivityLog;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Transformers\Api\Client\ActivityLogTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
|
||||
class ActivityLogController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* Returns the activity logs for a server.
|
||||
*/
|
||||
public function __invoke(ClientApiRequest $request, Server $server): array
|
||||
{
|
||||
$this->authorize(Permission::ACTION_ACTIVITY_READ, $server);
|
||||
|
||||
$activity = QueryBuilder::for($server->activity())
|
||||
->with('actor')
|
||||
->allowedSorts(['timestamp'])
|
||||
->allowedFilters([AllowedFilter::partial('event')])
|
||||
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
|
||||
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
// painful so for now we'll execute a simpler query.
|
||||
$subusers = $server->subusers()->pluck('user_id')->merge($server->owner_id);
|
||||
|
||||
$builder->select('activity_logs.*')
|
||||
->leftJoin('users', function (JoinClause $join) {
|
||||
$join->on('users.id', 'activity_logs.actor_id')
|
||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
||||
})
|
||||
->where(function (Builder $builder) use ($subusers) {
|
||||
$builder->whereNull('users.id')
|
||||
->orWhere('users.root_admin', 0)
|
||||
->orWhereIn('users.id', $subusers);
|
||||
});
|
||||
})
|
||||
->paginate(min($request->query('per_page', 25), 100))
|
||||
->appends($request->query());
|
||||
|
||||
return $this->fractal->collection($activity)
|
||||
->transformWith($this->getTransformer(ActivityLogTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
@@ -19,6 +19,8 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
|
||||
|
||||
use Pterodactyl\Enums\Daemon\Adapters;
|
||||
|
||||
class BackupsController extends ClientApiController
|
||||
{
|
||||
public function __construct(
|
||||
@@ -44,14 +46,14 @@ class BackupsController extends ClientApiController
|
||||
|
||||
$rusticBackupSum = $server->backups()
|
||||
->where('is_successful', true)
|
||||
->whereIn('disk', [Backup::ADAPTER_RUSTIC_LOCAL, Backup::ADAPTER_RUSTIC_S3])
|
||||
->whereIn('disk', Adapters::all_elytra())
|
||||
->sum('bytes');
|
||||
|
||||
$rusticSumMb = round($rusticBackupSum / 1024 / 1024, 2);
|
||||
|
||||
$legacyBackupSum = $server->backups()
|
||||
->where('is_successful', true)
|
||||
->whereNotIn('disk', [Backup::ADAPTER_RUSTIC_LOCAL, Backup::ADAPTER_RUSTIC_S3])
|
||||
->whereNotIn('disk', Adapters::all_elytra())
|
||||
->sum('bytes');
|
||||
|
||||
$legacyUsageMb = round($legacyBackupSum / 1024 / 1024, 2);
|
||||
@@ -101,7 +103,7 @@ class BackupsController extends ClientApiController
|
||||
'backup_create',
|
||||
[
|
||||
'operation' => 'create',
|
||||
'adapter' => $request->input('adapter', config('backups.default')),
|
||||
'adapter' => $request->input('adapter', $server->node->backupDisk),
|
||||
'ignored' => $request->input('ignored', ''),
|
||||
'name' => $request->input('name'),
|
||||
],
|
||||
@@ -407,7 +409,7 @@ class BackupsController extends ClientApiController
|
||||
[
|
||||
'operation' => 'delete',
|
||||
'backup_uuid' => $backup->uuid,
|
||||
'adapter_type' => $backup->getElytraAdapterType(),
|
||||
'adapter_type' => $backup->disk,
|
||||
'snapshot_id' => $backup->snapshot_id,
|
||||
'checksum' => $backup->checksum,
|
||||
],
|
||||
@@ -434,4 +436,4 @@ class BackupsController extends ClientApiController
|
||||
'backup_count' => count($backupUuids),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\BadResponseException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class CommandController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* CommandController constructor.
|
||||
*/
|
||||
public function __construct(private DaemonCommandRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command to a running server.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function index(SendCommandRequest $request, Server $server): Response
|
||||
{
|
||||
try {
|
||||
$this->repository->setServer($server)->send($request->input('command'));
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$previous = $exception->getPrevious();
|
||||
|
||||
if ($previous instanceof BadResponseException) {
|
||||
if (
|
||||
$previous->getResponse() instanceof ResponseInterface
|
||||
&& $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY
|
||||
) {
|
||||
throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception);
|
||||
}
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
Activity::event('server:console.command')->property('command', $request->input('command'))->log();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Services\Databases\DatabasePasswordService;
|
||||
use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\RotatePasswordRequest;
|
||||
|
||||
class DatabaseController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* DatabaseController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private DeployServerDatabaseService $deployDatabaseService,
|
||||
private DatabaseManagementService $managementService,
|
||||
private DatabasePasswordService $passwordService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the databases that belong to the given server.
|
||||
*/
|
||||
public function index(GetDatabasesRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->collection($server->databases)
|
||||
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database for the given server and return it.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||
*/
|
||||
public function store(StoreDatabaseRequest $request, Server $server): array
|
||||
{
|
||||
$database = $this->deployDatabaseService->handle($server, $request->validated());
|
||||
|
||||
Activity::event('server:database.create')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($database)
|
||||
->parseIncludes(['password'])
|
||||
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the password for the given server model and returns a fresh instance to
|
||||
* the caller.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
|
||||
{
|
||||
$this->passwordService->handle($database);
|
||||
$database->refresh();
|
||||
|
||||
Activity::event('server:database.rotate-password')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($database)
|
||||
->parseIncludes(['password'])
|
||||
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a database from the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response
|
||||
{
|
||||
$this->managementService->delete($database);
|
||||
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -123,4 +123,5 @@ class ElytraJobsController extends ClientApiController
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\FileObjectTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\PullFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
|
||||
|
||||
class FileController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* FileController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private NodeJWTService $jwtService,
|
||||
private DaemonFileRepository $fileRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a listing of files in a given directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function directory(ListFilesRequest $request, Server $server): array
|
||||
{
|
||||
$contents = $this->fileRepository
|
||||
->setServer($server)
|
||||
->getDirectory($request->get('directory') ?? '/');
|
||||
|
||||
return $this->fractal->collection($contents)
|
||||
->transformWith($this->getTransformer(FileObjectTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of a specified file for the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function contents(GetFileContentsRequest $request, Server $server): Response
|
||||
{
|
||||
$response = $this->fileRepository->setServer($server)->getContent(
|
||||
$request->get('file'),
|
||||
config('pterodactyl.files.max_edit_size')
|
||||
);
|
||||
|
||||
Activity::event('server:file.read')->property('file', $request->get('file'))->log();
|
||||
|
||||
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a one-time token with a link that the user can use to
|
||||
* download a given file.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function download(GetFileContentsRequest $request, Server $server): array
|
||||
{
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser($request->user())
|
||||
->setClaims([
|
||||
'file_path' => rawurldecode($request->get('file')),
|
||||
'server_uuid' => $server->uuid,
|
||||
])
|
||||
->handle($server->node, $request->user()->id . $server->uuid);
|
||||
|
||||
Activity::event('server:file.download')->property('file', $request->get('file'))->log();
|
||||
|
||||
return [
|
||||
'object' => 'signed_url',
|
||||
'attributes' => [
|
||||
'url' => sprintf(
|
||||
'%s/download/file?token=%s',
|
||||
$server->node->getConnectionAddress(),
|
||||
$token->toString()
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of the specified file to the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
|
||||
|
||||
Activity::event('server:file.write')->property('file', $request->get('file'))->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new folder on the server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function create(CreateFolderRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->createDirectory($request->input('name'), $request->input('root', '/'));
|
||||
|
||||
Activity::event('server:file.create-directory')
|
||||
->property('name', $request->input('name'))
|
||||
->property('directory', $request->input('root'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file on the remote machine.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function rename(RenameFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->renameFiles($request->input('root'), $request->input('files'));
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file on the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function copy(CopyFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->copyFile($request->input('location'));
|
||||
|
||||
Activity::event('server:file.copy')->property('file', $request->input('location'))->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function compress(CompressFilesRequest $request, Server $server): array
|
||||
{
|
||||
$file = $this->fileRepository->setServer($server)->compressFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($file)
|
||||
->transformWith($this->getTransformer(FileObjectTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
set_time_limit(300);
|
||||
|
||||
$this->fileRepository->setServer($server)->decompressFile(
|
||||
$request->input('root'),
|
||||
$request->input('file')
|
||||
);
|
||||
|
||||
Activity::event('server:file.decompress')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('file'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes files or folders for the server in the given root directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->deleteFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates file permissions for file(s) in the given root directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->chmodFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that a file be downloaded from a remote location by Wings.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pull(PullFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->pull(
|
||||
$request->input('url'),
|
||||
$request->input('directory'),
|
||||
$request->safe(['filename', 'use_header', 'foreground'])
|
||||
);
|
||||
|
||||
Activity::event('server:file.pull')
|
||||
->property('directory', $request->input('directory'))
|
||||
->property('url', $request->input('url'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\UploadFileRequest;
|
||||
|
||||
class FileUploadController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* FileUploadController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private NodeJWTService $jwtService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an url where files can be uploaded to.
|
||||
*/
|
||||
public function __invoke(UploadFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
return new JsonResponse([
|
||||
'object' => 'signed_url',
|
||||
'attributes' => [
|
||||
'url' => $this->getUploadUrl($server, $request->user()),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an url where files can be uploaded to.
|
||||
*/
|
||||
protected function getUploadUrl(Server $server, User $user): string
|
||||
{
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser($user)
|
||||
->setClaims(['server_uuid' => $server->uuid])
|
||||
->handle($server->node, $user->id . $server->uuid);
|
||||
|
||||
return sprintf(
|
||||
'%s/upload/file?token=%s',
|
||||
$server->node->getConnectionAddress(),
|
||||
$token->toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest;
|
||||
|
||||
class PowerController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* PowerController constructor.
|
||||
*/
|
||||
public function __construct(private DaemonPowerRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a power action to a server.
|
||||
*/
|
||||
public function index(SendPowerRequest $request, Server $server): Response
|
||||
{
|
||||
$this->repository->setServer($server)->send(
|
||||
$request->input('signal')
|
||||
);
|
||||
|
||||
Activity::event(strtolower("server:power.{$request->input('signal')}"))->log();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Cache\Repository;
|
||||
use Pterodactyl\Transformers\Api\Client\StatsTransformer;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\GetServerRequest;
|
||||
|
||||
class ResourceUtilizationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* ResourceUtilizationController constructor.
|
||||
*/
|
||||
public function __construct(private Repository $cache, private DaemonServerRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current resource utilization for a server. This value is cached for up to
|
||||
* 20 seconds at a time to ensure that repeated requests to this endpoint do not cause
|
||||
* a flood of unnecessary API calls.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function __invoke(GetServerRequest $request, Server $server): array
|
||||
{
|
||||
$key = "resources:$server->uuid";
|
||||
$stats = $this->cache->remember($key, Carbon::now()->addSeconds(20), function () use ($server) {
|
||||
return $this->repository->setServer($server)->getDetails();
|
||||
});
|
||||
|
||||
return $this->fractal->item($stats)
|
||||
->transformWith($this->getTransformer(StatsTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Pterodactyl\Models\Task;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\GetServerRequest;
|
||||
|
||||
class ServerController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* ServerController constructor.
|
||||
*/
|
||||
public function __construct(private GetUserPermissionsService $permissionsService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an individual server into a response that can be consumed by a
|
||||
* client using the API.
|
||||
*/
|
||||
public function index(GetServerRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->item($server)
|
||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
||||
->addMeta([
|
||||
'is_server_owner' => $request->user()->id === $server->owner_id,
|
||||
'user_permissions' => $this->permissionsService->handle($server, $request->user()),
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -94,7 +94,7 @@ class SettingsController extends ClientApiController
|
||||
|
||||
$original = $server->image;
|
||||
$defaultImage = $server->getDefaultDockerImage();
|
||||
|
||||
|
||||
if (empty($defaultImage)) {
|
||||
throw new BadRequestHttpException('No default docker image available for this server\'s egg.');
|
||||
}
|
||||
@@ -140,98 +140,95 @@ class SettingsController extends ClientApiController
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
public function previewEggChange(PreviewEggRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
|
||||
$previewData = $this->eggChangeService->previewEggChange($server, $eggId, $nestId);
|
||||
|
||||
// Log the preview action
|
||||
Activity::event('server:settings.egg-preview')
|
||||
->property([
|
||||
'current_egg_id' => $server->egg_id,
|
||||
'preview_egg_id' => $eggId,
|
||||
'preview_nest_id' => $nestId,
|
||||
])
|
||||
->log();
|
||||
|
||||
return new JsonResponse($previewData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to preview egg change', [
|
||||
'server_id' => $server->id,
|
||||
'egg_id' => $request->input('egg_id'),
|
||||
'nest_id' => $request->input('nest_id'),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
public function previewEggChange(PreviewEggRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
|
||||
/**
|
||||
* Apply egg configuration changes asynchronously.
|
||||
* This dispatches a background job to handle the complete egg change process.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function applyEggChange(ApplyEggChangeRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$dockerImage = $request->input('docker_image');
|
||||
$startupCommand = $request->input('startup_command');
|
||||
$environment = $request->input('environment', []);
|
||||
$shouldBackup = $request->input('should_backup', false);
|
||||
$shouldWipe = $request->input('should_wipe', false);
|
||||
|
||||
$result = $this->eggChangeService->applyEggChangeAsync(
|
||||
$server,
|
||||
$request->user(),
|
||||
$eggId,
|
||||
$nestId,
|
||||
$dockerImage,
|
||||
$startupCommand,
|
||||
$environment,
|
||||
$shouldBackup,
|
||||
$shouldWipe
|
||||
);
|
||||
|
||||
Activity::event('server:software.change-queued')
|
||||
->property([
|
||||
'operation_id' => $result['operation_id'],
|
||||
'from_egg' => $server->egg_id,
|
||||
'to_egg' => $eggId,
|
||||
'should_backup' => $shouldBackup,
|
||||
'should_wipe' => $shouldWipe,
|
||||
])
|
||||
->log();
|
||||
|
||||
return new JsonResponse($result, Response::HTTP_ACCEPTED);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to apply egg change', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$previewData = $this->eggChangeService->previewEggChange($server, $eggId, $nestId);
|
||||
|
||||
public function getOperationStatus(Server $server, string $operationId): JsonResponse
|
||||
{
|
||||
$operation = $this->operationService->getOperation($server, $operationId);
|
||||
return new JsonResponse($this->operationService->formatOperationResponse($operation));
|
||||
}
|
||||
// Log the preview action
|
||||
Activity::event('server:settings.egg-preview')
|
||||
->property([
|
||||
'current_egg_id' => $server->egg_id,
|
||||
'preview_egg_id' => $eggId,
|
||||
'preview_nest_id' => $nestId,
|
||||
])
|
||||
->log();
|
||||
|
||||
public function getServerOperations(Server $server): JsonResponse
|
||||
{
|
||||
$operations = $this->operationService->getServerOperations($server);
|
||||
return new JsonResponse(['operations' => $operations]);
|
||||
}
|
||||
return new JsonResponse($previewData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to preview egg change', [
|
||||
'server_id' => $server->id,
|
||||
'egg_id' => $request->input('egg_id'),
|
||||
'nest_id' => $request->input('nest_id'),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply egg configuration changes asynchronously.
|
||||
* This dispatches a background job to handle the complete egg change process.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function applyEggChange(ApplyEggChangeRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$dockerImage = $request->input('docker_image');
|
||||
$startupCommand = $request->input('startup_command');
|
||||
$environment = $request->input('environment', []);
|
||||
$shouldBackup = $request->input('should_backup', false);
|
||||
$shouldWipe = $request->input('should_wipe', false);
|
||||
|
||||
$result = $this->eggChangeService->applyEggChangeAsync(
|
||||
$server,
|
||||
$request->user(),
|
||||
$eggId,
|
||||
$nestId,
|
||||
$dockerImage,
|
||||
$startupCommand,
|
||||
$environment,
|
||||
$shouldBackup,
|
||||
$shouldWipe
|
||||
);
|
||||
|
||||
Activity::event('server:software.change-queued')
|
||||
->property([
|
||||
'operation_id' => $result['operation_id'],
|
||||
'from_egg' => $server->egg_id,
|
||||
'to_egg' => $eggId,
|
||||
'should_backup' => $shouldBackup,
|
||||
'should_wipe' => $shouldWipe,
|
||||
])
|
||||
->log();
|
||||
|
||||
return new JsonResponse($result, Response::HTTP_ACCEPTED);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to apply egg change', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function getOperationStatus(Server $server, string $operationId): JsonResponse
|
||||
{
|
||||
$operation = $this->operationService->getOperation($server, $operationId);
|
||||
return new JsonResponse($this->operationService->formatOperationResponse($operation));
|
||||
}
|
||||
|
||||
public function getServerOperations(Server $server): JsonResponse
|
||||
{
|
||||
$operations = $this->operationService->getServerOperations($server);
|
||||
return new JsonResponse(['operations' => $operations]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
@@ -140,16 +140,16 @@ class StartupController extends ClientApiController
|
||||
public function processCommand(GetStartupRequest $request, Server $server): array
|
||||
{
|
||||
$command = $request->input('command', $server->startup);
|
||||
|
||||
|
||||
// Temporarily update the server's startup command for processing
|
||||
$originalStartup = $server->startup;
|
||||
$server->startup = $command;
|
||||
|
||||
|
||||
$processedCommand = $this->startupCommandService->handle($server, false);
|
||||
|
||||
|
||||
// Restore original startup command
|
||||
$server->startup = $originalStartup;
|
||||
|
||||
|
||||
return [
|
||||
'processed_command' => $processedCommand,
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -28,7 +28,7 @@ class SubdomainController extends ClientApiController
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
|
||||
|
||||
$this->authorize(Permission::ACTION_ALLOCATION_READ, $server);
|
||||
|
||||
try {
|
||||
@@ -73,15 +73,15 @@ class SubdomainController extends ClientApiController
|
||||
public function store(CreateSubdomainRequest $request): JsonResponse
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
|
||||
|
||||
$this->authorize(Permission::ACTION_ALLOCATION_CREATE, $server);
|
||||
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
try {
|
||||
// Get ALL active subdomains for this server (more than one should be impossible, but PHP makes me angry)
|
||||
$existingSubdomains = $server->subdomains()->where('is_active', true)->get();
|
||||
|
||||
|
||||
$domain = Domain::where('id', $data['domain_id'])
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
@@ -148,7 +148,7 @@ class SubdomainController extends ClientApiController
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
|
||||
|
||||
$this->authorize(Permission::ACTION_ALLOCATION_DELETE, $server);
|
||||
|
||||
try {
|
||||
@@ -179,7 +179,6 @@ class SubdomainController extends ClientApiController
|
||||
return response()->json([
|
||||
'error' => 'Failed to delete subdomain(s).'
|
||||
], 422);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +188,9 @@ class SubdomainController extends ClientApiController
|
||||
public function checkAvailability(Request $request): JsonResponse
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
|
||||
|
||||
$this->authorize(Permission::ACTION_ALLOCATION_READ, $server);
|
||||
|
||||
|
||||
$request->validate([
|
||||
'subdomain' => 'required|string|min:1|max:63|regex:/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/',
|
||||
'domain_id' => 'required|integer|exists:domains,id',
|
||||
@@ -221,4 +220,5 @@ class SubdomainController extends ClientApiController
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
|
||||
class WebsocketController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* WebsocketController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private NodeJWTService $jwtService,
|
||||
private GetUserPermissionsService $permissionsService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a one-time token that is sent along in every websocket call to the Daemon.
|
||||
* This is a signed JWT that the Daemon then uses to verify the user's identity, and
|
||||
* allows us to continually renew this token and avoid users maintaining sessions wrongly,
|
||||
* as well as ensure that user's only perform actions they're allowed to.
|
||||
*/
|
||||
public function __invoke(ClientApiRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if ($user->cannot(Permission::ACTION_WEBSOCKET_CONNECT, $server)) {
|
||||
throw new HttpForbiddenException('You do not have permission to connect to this server\'s websocket.');
|
||||
}
|
||||
|
||||
$permissions = $this->permissionsService->handle($server, $user);
|
||||
|
||||
$node = $server->node;
|
||||
if (!is_null($server->transfer)) {
|
||||
// Check if the user has permissions to receive transfer logs.
|
||||
if (!in_array('admin.websocket.transfer', $permissions)) {
|
||||
throw new HttpForbiddenException('You do not have permission to view server transfer logs.');
|
||||
}
|
||||
|
||||
// Redirect the websocket request to the new node if the server has been archived.
|
||||
if ($server->transfer->archived) {
|
||||
$node = $server->transfer->newNode;
|
||||
}
|
||||
}
|
||||
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(10))
|
||||
->setUser($request->user())
|
||||
->setClaims([
|
||||
'server_uuid' => $server->uuid,
|
||||
'permissions' => $permissions,
|
||||
])
|
||||
->handle($node, $user->id . $server->uuid);
|
||||
|
||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $node->getBrowserConnectionAddress());
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'token' => $token->toString(),
|
||||
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Pterodactyl\Services\Backups\Wings\DeleteBackupService;
|
||||
use Pterodactyl\Services\Backups\Wings\DownloadLinkService;
|
||||
use Pterodactyl\Repositories\Eloquent\BackupRepository;
|
||||
use Pterodactyl\Services\Backups\Wings\InitiateBackupService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\BackupTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
|
||||
|
||||
class BackupController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* BackupController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private DaemonBackupRepository $daemonRepository,
|
||||
private DeleteBackupService $deleteBackupService,
|
||||
private InitiateBackupService $initiateBackupService,
|
||||
private DownloadLinkService $downloadLinkService,
|
||||
private BackupRepository $repository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the backups for a given server instance in a paginated
|
||||
* result set.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(Request $request, Server $server): array
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_READ, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$limit = min($request->query('per_page') ?? 20, 50);
|
||||
|
||||
return $this->fractal->collection($server->backups()->paginate($limit))
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->addMeta([
|
||||
'backup_count' => $this->repository->getNonFailedBackups($server)->count(),
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the backup process for a server.
|
||||
*
|
||||
* @throws \Spatie\Fractalistic\Exceptions\InvalidTransformation
|
||||
* @throws \Spatie\Fractalistic\Exceptions\NoTransformerSpecified
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(StoreBackupRequest $request, Server $server): array
|
||||
{
|
||||
$action = $this->initiateBackupService
|
||||
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
|
||||
|
||||
// Only set the lock status if the user even has permission to delete backups,
|
||||
// otherwise ignore this status. This gets a little funky since it isn't clear
|
||||
// how best to allow a user to create a backup that is locked without also preventing
|
||||
// them from just filling up a server with backups that can never be deleted?
|
||||
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked($request->boolean('is_locked'));
|
||||
}
|
||||
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
Activity::event('server:backup.start')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the lock status of a given backup for a server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function toggleLock(Request $request, Server $server, Backup $backup): array
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$action = $backup->is_locked ? 'server:backup.unlock' : 'server:backup.lock';
|
||||
|
||||
$backup->update(['is_locked' => !$backup->is_locked]);
|
||||
|
||||
Activity::event($action)->subject($backup)->property('name', $backup->name)->log();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about a single backup.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function view(Request $request, Server $server, Backup $backup): array
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_READ, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup from the panel as well as the remote source where it is currently
|
||||
* being stored.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function delete(Request $request, Server $server, Backup $backup): JsonResponse
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$this->deleteBackupService->handle($backup);
|
||||
|
||||
Activity::event('server:backup.delete')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the backup for a given server instance. For daemon local files, the file
|
||||
* will be streamed back through the Panel. For AWS S3 files, a signed URL will be generated
|
||||
* which the user is redirected to.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function download(Request $request, Server $server, Backup $backup): JsonResponse
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
if ($backup->disk !== Backup::ADAPTER_AWS_S3 && $backup->disk !== Backup::ADAPTER_WINGS) {
|
||||
throw new BadRequestHttpException('The backup requested references an unknown disk driver type and cannot be downloaded.');
|
||||
}
|
||||
|
||||
$url = $this->downloadLinkService->handle($backup, $request->user());
|
||||
|
||||
Activity::event('server:backup.download')->subject($backup)->property('name', $backup->name)->log();
|
||||
|
||||
return new JsonResponse([
|
||||
'object' => 'signed_url',
|
||||
'attributes' => ['url' => $url],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles restoring a backup by making a request to the Wings instance telling it
|
||||
* to begin the process of finding (or downloading) the backup and unpacking it
|
||||
* over the server files.
|
||||
*
|
||||
* If the "truncate" flag is passed through in this request then all the
|
||||
* files that currently exist on the server will be deleted before restoring.
|
||||
* Otherwise, the archive will simply be unpacked over the existing files.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function restore(RestoreBackupRequest $request, Server $server, Backup $backup): JsonResponse
|
||||
{
|
||||
// Cannot restore a backup unless a server is fully installed and not currently
|
||||
// processing a different backup restoration request.
|
||||
if (!is_null($server->status)) {
|
||||
throw new BadRequestHttpException('This server is not currently in a state that allows for a backup to be restored.');
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) {
|
||||
throw new BadRequestHttpException('This backup cannot be restored at this time: not completed or failed.');
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $request->input('truncate')]);
|
||||
|
||||
$log->transaction(function () use ($backup, $server, $request) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow Wings to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $this->downloadLinkService->handle($backup, $request->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => Server::STATUS_RESTORING_BACKUP]);
|
||||
|
||||
$this->daemonRepository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate'));
|
||||
});
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\User;
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Services\Allocations\FindAssignableAllocationService;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||
|
||||
class NetworkAllocationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* NetworkAllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private FindAssignableAllocationService $assignableAllocationService,
|
||||
private ServerRepository $serverRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the allocations available to a server and whether
|
||||
* they are currently assigned as the primary for this server.
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->collection($server->allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$original = $allocation->notes;
|
||||
|
||||
$allocation->forceFill(['notes' => $request->input('notes')])->save();
|
||||
|
||||
if ($original !== $allocation->notes) {
|
||||
Activity::event('server:allocation.notes')
|
||||
->subject($allocation)
|
||||
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$this->serverRepository->update($server->id, ['allocation_id' => $allocation->id]);
|
||||
|
||||
Activity::event('server:allocation.primary')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notes for the allocation for a server.
|
||||
*s.
|
||||
*
|
||||
* @throws DisplayException
|
||||
*/
|
||||
public function store(NewAllocationRequest $request, Server $server): array
|
||||
{
|
||||
if ($server->allocations()->count() >= $server->allocation_limit) {
|
||||
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
|
||||
}
|
||||
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an allocation from a server.
|
||||
*
|
||||
* @throws DisplayException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse
|
||||
{
|
||||
// Don't allow the deletion of allocations if the server does not have an
|
||||
// allocation limit set.
|
||||
if (empty($server->allocation_limit)) {
|
||||
throw new DisplayException('You cannot delete allocations for this server: no allocation limit is set.');
|
||||
}
|
||||
|
||||
if ($allocation->id === $server->allocation_id) {
|
||||
throw new DisplayException('You cannot delete the primary allocation for this server.');
|
||||
}
|
||||
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Helpers\Utilities;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
|
||||
use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
||||
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest;
|
||||
|
||||
class ScheduleController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* ScheduleController constructor.
|
||||
*/
|
||||
public function __construct(private ScheduleRepository $repository, private ProcessScheduleService $service)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the schedules belonging to a given server.
|
||||
*/
|
||||
public function index(ViewScheduleRequest $request, Server $server): array
|
||||
{
|
||||
$schedules = $server->schedules->loadMissing('tasks');
|
||||
|
||||
return $this->fractal->collection($schedules)
|
||||
->transformWith($this->getTransformer(ScheduleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new schedule for a server.
|
||||
*
|
||||
* @throws DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(StoreScheduleRequest $request, Server $server): array
|
||||
{
|
||||
/** @var Schedule $model */
|
||||
$model = $this->repository->create([
|
||||
'server_id' => $server->id,
|
||||
'name' => $request->input('name'),
|
||||
'cron_day_of_week' => $request->input('day_of_week'),
|
||||
'cron_month' => $request->input('month'),
|
||||
'cron_day_of_month' => $request->input('day_of_month'),
|
||||
'cron_hour' => $request->input('hour'),
|
||||
'cron_minute' => $request->input('minute'),
|
||||
'is_active' => (bool) $request->input('is_active'),
|
||||
'only_when_online' => (bool) $request->input('only_when_online'),
|
||||
'next_run_at' => $this->getNextRunAt($request),
|
||||
]);
|
||||
|
||||
Activity::event('server:schedule.create')
|
||||
->subject($model)
|
||||
->property('name', $model->name)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($model)
|
||||
->transformWith($this->getTransformer(ScheduleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific schedule for the server.
|
||||
*/
|
||||
public function view(ViewScheduleRequest $request, Server $server, Schedule $schedule): array
|
||||
{
|
||||
if ($schedule->server_id !== $server->id) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$schedule->loadMissing('tasks');
|
||||
|
||||
return $this->fractal->item($schedule)
|
||||
->transformWith($this->getTransformer(ScheduleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given schedule with the new data provided.
|
||||
*
|
||||
* @throws DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(UpdateScheduleRequest $request, Server $server, Schedule $schedule): array
|
||||
{
|
||||
$active = (bool) $request->input('is_active');
|
||||
|
||||
$data = [
|
||||
'name' => $request->input('name'),
|
||||
'cron_day_of_week' => $request->input('day_of_week'),
|
||||
'cron_month' => $request->input('month'),
|
||||
'cron_day_of_month' => $request->input('day_of_month'),
|
||||
'cron_hour' => $request->input('hour'),
|
||||
'cron_minute' => $request->input('minute'),
|
||||
'is_active' => $active,
|
||||
'only_when_online' => (bool) $request->input('only_when_online'),
|
||||
'next_run_at' => $this->getNextRunAt($request),
|
||||
];
|
||||
|
||||
// Toggle the processing state of the scheduled task when it is enabled or disabled so that an
|
||||
// invalid state can be reset without manual database intervention.
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2425
|
||||
if ($schedule->is_active !== $active) {
|
||||
$data['is_processing'] = false;
|
||||
}
|
||||
|
||||
$this->repository->update($schedule->id, $data);
|
||||
|
||||
Activity::event('server:schedule.update')
|
||||
->subject($schedule)
|
||||
->property(['name' => $schedule->name, 'active' => $active])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($schedule->refresh())
|
||||
->transformWith($this->getTransformer(ScheduleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given schedule immediately rather than waiting on it's normally scheduled time
|
||||
* to pass. This does not care about the schedule state.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule): JsonResponse
|
||||
{
|
||||
$this->service->handle($schedule, true);
|
||||
|
||||
Activity::event('server:schedule.execute')->subject($schedule)->property('name', $schedule->name)->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a schedule and it's associated tasks.
|
||||
*/
|
||||
public function delete(DeleteScheduleRequest $request, Server $server, Schedule $schedule): JsonResponse
|
||||
{
|
||||
$this->repository->delete($schedule->id);
|
||||
|
||||
Activity::event('server:schedule.delete')->subject($schedule)->property('name', $schedule->name)->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next run timestamp based on the cron data provided.
|
||||
*
|
||||
* @throws DisplayException
|
||||
*/
|
||||
protected function getNextRunAt(Request $request): Carbon
|
||||
{
|
||||
try {
|
||||
return Utilities::getScheduleNextRunDate(
|
||||
$request->input('minute'),
|
||||
$request->input('hour'),
|
||||
$request->input('day_of_month'),
|
||||
$request->input('month'),
|
||||
$request->input('day_of_week')
|
||||
);
|
||||
} catch (\Exception $exception) {
|
||||
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Pterodactyl\Models\Task;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Exceptions\Service\ServiceLimitExceededException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
|
||||
|
||||
class ScheduleTaskController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* ScheduleTaskController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
private TaskRepository $repository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task for a given schedule and store it in the database.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws ServiceLimitExceededException
|
||||
*/
|
||||
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule): array
|
||||
{
|
||||
$limit = config('pterodactyl.client_features.schedules.per_schedule_task_limit', 10);
|
||||
if ($schedule->tasks()->count() >= $limit) {
|
||||
throw new ServiceLimitExceededException("Schedules may not have more than $limit tasks associated with them. Creating this task would put this schedule over the limit.");
|
||||
}
|
||||
|
||||
if ($server->backup_limit === 0 && $request->action === 'backup') {
|
||||
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
|
||||
}
|
||||
|
||||
/** @var Task|null $lastTask */
|
||||
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
|
||||
|
||||
/** @var Task $task */
|
||||
$task = $this->connection->transaction(function () use ($request, $schedule, $lastTask) {
|
||||
$sequenceId = ($lastTask->sequence_id ?? 0) + 1;
|
||||
$requestSequenceId = $request->integer('sequence_id', $sequenceId);
|
||||
|
||||
// Ensure that the sequence id is at least 1.
|
||||
if ($requestSequenceId < 1) {
|
||||
$requestSequenceId = 1;
|
||||
}
|
||||
|
||||
// If the sequence id from the request is greater than or equal to the next available
|
||||
// sequence id, we don't need to do anything special. Otherwise, we need to update
|
||||
// the sequence id of all tasks that are greater than or equal to the request sequence
|
||||
// id to be one greater than the current value.
|
||||
if ($requestSequenceId < $sequenceId) {
|
||||
$schedule->tasks()
|
||||
->where('sequence_id', '>=', $requestSequenceId)
|
||||
->increment('sequence_id');
|
||||
$sequenceId = $requestSequenceId;
|
||||
}
|
||||
|
||||
return $this->repository->create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => $sequenceId,
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => $request->boolean('continue_on_failure'),
|
||||
]);
|
||||
});
|
||||
|
||||
Activity::event('server:task.create')
|
||||
->subject($schedule, $task)
|
||||
->property(['name' => $schedule->name, 'action' => $task->action, 'payload' => $task->payload])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($task)
|
||||
->transformWith($this->getTransformer(TaskTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given task for a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(StoreTaskRequest $request, Server $server, Schedule $schedule, Task $task): array
|
||||
{
|
||||
if ($schedule->id !== $task->schedule_id || $server->id !== $schedule->server_id) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
if ($server->backup_limit === 0 && $request->action === 'backup') {
|
||||
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($request, $schedule, $task) {
|
||||
$sequenceId = $request->integer('sequence_id', $task->sequence_id);
|
||||
// Ensure that the sequence id is at least 1.
|
||||
if ($sequenceId < 1) {
|
||||
$sequenceId = 1;
|
||||
}
|
||||
|
||||
// Shift all other tasks in the schedule up or down to make room for the new task.
|
||||
if ($sequenceId < $task->sequence_id) {
|
||||
$schedule->tasks()
|
||||
->where('sequence_id', '>=', $sequenceId)
|
||||
->where('sequence_id', '<', $task->sequence_id)
|
||||
->increment('sequence_id');
|
||||
} elseif ($sequenceId > $task->sequence_id) {
|
||||
$schedule->tasks()
|
||||
->where('sequence_id', '>', $task->sequence_id)
|
||||
->where('sequence_id', '<=', $sequenceId)
|
||||
->decrement('sequence_id');
|
||||
}
|
||||
|
||||
$this->repository->update($task->id, [
|
||||
'sequence_id' => $sequenceId,
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => $request->boolean('continue_on_failure'),
|
||||
]);
|
||||
});
|
||||
|
||||
Activity::event('server:task.update')
|
||||
->subject($schedule, $task)
|
||||
->property(['name' => $schedule->name, 'action' => $task->action, 'payload' => $task->payload])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($task->refresh())
|
||||
->transformWith($this->getTransformer(TaskTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given task for a schedule. If there are subsequent tasks stored in the database
|
||||
* for this schedule their sequence IDs are decremented properly.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task): JsonResponse
|
||||
{
|
||||
if ($task->schedule_id !== $schedule->id || $schedule->server_id !== $server->id) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
if (!$request->user()->can(Permission::ACTION_SCHEDULE_UPDATE, $server)) {
|
||||
throw new HttpForbiddenException('You do not have permission to perform this action.');
|
||||
}
|
||||
|
||||
$schedule->tasks()
|
||||
->where('sequence_id', '>', $task->sequence_id)
|
||||
->decrement('sequence_id');
|
||||
$task->delete();
|
||||
|
||||
Activity::event('server:task.delete')->subject($schedule, $task)->property('name', $schedule->name)->log();
|
||||
|
||||
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Services\Servers\ReinstallServerService;
|
||||
use Pterodactyl\Services\ServerOperations\EggChangeService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\SetEggRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\PreviewEggRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ApplyEggChangeRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
|
||||
class SettingsController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* SettingsController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ServerRepository $repository,
|
||||
private ReinstallServerService $reinstallServerService,
|
||||
private EggChangeService $eggChangeService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
$this->repository->update($server->id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
if ($server->name !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
if ($server->description !== $description) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstalls the server on the daemon.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall(ReinstallServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->reinstallServerService->handle($server);
|
||||
|
||||
Activity::event('server:reinstall')->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the Docker image in use by the server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!in_array($request->input('docker_image'), array_values($server->egg->docker_images))) {
|
||||
throw new BadRequestHttpException('The requested Docker image is not allowed for this server.');
|
||||
}
|
||||
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $request->input('docker_image')])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $request->input('docker_image')])
|
||||
->log();
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Startup Command
|
||||
*/
|
||||
private function resetStartupCommand(Server $server): JsonResponse
|
||||
{
|
||||
$server->startup = $server->egg->startup;
|
||||
$server->save();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the egg for a server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function changeEgg(SetEggRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$originalEggId = $server->egg_id;
|
||||
$originalNestId = $server->nest_id;
|
||||
|
||||
// Check if the new Egg and Nest IDs are different from the current ones
|
||||
if ($originalEggId !== $eggId || $originalNestId !== $nestId) {
|
||||
// Update the server's Egg and Nest IDs
|
||||
$server->egg_id = $eggId;
|
||||
$server->nest_id = $nestId;
|
||||
$server->save();
|
||||
|
||||
// Log an activity event for the Egg change
|
||||
Activity::event('server:settings.egg')
|
||||
->property(['original_egg_id' => $originalEggId, 'new_egg_id' => $eggId, 'original_nest_id' => $originalNestId, 'new_nest_id' => $nestId])
|
||||
->log();
|
||||
|
||||
// Reset the server's startup command
|
||||
$this->resetStartupCommand($server);
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
public function previewEggChange(PreviewEggRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
|
||||
$previewData = $this->eggChangeService->previewEggChange($server, $eggId, $nestId);
|
||||
|
||||
// Log the preview action
|
||||
Activity::event('server:settings.egg-preview')
|
||||
->property([
|
||||
'current_egg_id' => $server->egg_id,
|
||||
'preview_egg_id' => $eggId,
|
||||
'preview_nest_id' => $nestId,
|
||||
])
|
||||
->log();
|
||||
|
||||
return new JsonResponse($previewData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to preview egg change', [
|
||||
'server_id' => $server->id,
|
||||
'egg_id' => $request->input('egg_id'),
|
||||
'nest_id' => $request->input('nest_id'),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply egg configuration changes asynchronously.
|
||||
* This dispatches a background job to handle the complete egg change process.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function applyEggChange(ApplyEggChangeRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$dockerImage = $request->input('docker_image');
|
||||
$startupCommand = $request->input('startup_command');
|
||||
$environment = $request->input('environment', []);
|
||||
$shouldBackup = $request->input('should_backup', false);
|
||||
$shouldWipe = $request->input('should_wipe', false);
|
||||
|
||||
$result = $this->eggChangeService->applyEggChangeAsync(
|
||||
$server,
|
||||
$request->user(),
|
||||
$eggId,
|
||||
$nestId,
|
||||
$dockerImage,
|
||||
$startupCommand,
|
||||
$environment,
|
||||
$shouldBackup,
|
||||
$shouldWipe
|
||||
);
|
||||
|
||||
Activity::event('server:software.change-queued')
|
||||
->property([
|
||||
'operation_id' => $result['operation_id'],
|
||||
'from_egg' => $server->egg_id,
|
||||
'to_egg' => $eggId,
|
||||
'should_backup' => $shouldBackup,
|
||||
'should_wipe' => $shouldWipe,
|
||||
])
|
||||
->log();
|
||||
|
||||
return new JsonResponse($result, Response::HTTP_ACCEPTED);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to apply egg change', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Services\Servers\StartupCommandService;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\EggVariableTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\GetStartupRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest;
|
||||
|
||||
class StartupController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* StartupController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private StartupCommandService $startupCommandService,
|
||||
private ServerVariableRepository $repository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the startup information for the server including all the variables.
|
||||
*/
|
||||
public function index(GetStartupRequest $request, Server $server): array
|
||||
{
|
||||
$startup = $this->startupCommandService->handle($server);
|
||||
|
||||
return $this->fractal->collection(
|
||||
$server->variables()->where('user_viewable', true)->get()
|
||||
)
|
||||
->transformWith($this->getTransformer(EggVariableTransformer::class))
|
||||
->addMeta([
|
||||
'startup_command' => $startup,
|
||||
'docker_images' => $server->egg->docker_images,
|
||||
'raw_startup_command' => $server->startup,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single variable for a server.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(UpdateStartupVariableRequest $request, Server $server): array
|
||||
{
|
||||
/** @var \Pterodactyl\Models\EggVariable $variable */
|
||||
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
|
||||
$original = $variable->server_value;
|
||||
|
||||
if (is_null($variable) || !$variable->user_viewable) {
|
||||
throw new BadRequestHttpException('The environment variable you are trying to edit does not exist.');
|
||||
} elseif (!$variable->user_editable) {
|
||||
throw new BadRequestHttpException('The environment variable you are trying to edit is read-only.');
|
||||
}
|
||||
|
||||
// Revalidate the variable value using the egg variable specific validation rules for it.
|
||||
$this->validate($request, ['value' => $variable->rules]);
|
||||
|
||||
$this->repository->updateOrCreate([
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $variable->id,
|
||||
], [
|
||||
'variable_value' => $request->input('value') ?? '',
|
||||
]);
|
||||
|
||||
$variable = $variable->refresh();
|
||||
$variable->server_value = $request->input('value');
|
||||
|
||||
$startup = $this->startupCommandService->handle($server);
|
||||
|
||||
if ($variable->env_variable !== $request->input('value')) {
|
||||
Activity::event('server:startup.edit')
|
||||
->subject($variable)
|
||||
->property([
|
||||
'variable' => $variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $request->input('value'),
|
||||
])
|
||||
->log();
|
||||
}
|
||||
|
||||
return $this->fractal->item($variable)
|
||||
->transformWith($this->getTransformer(EggVariableTransformer::class))
|
||||
->addMeta([
|
||||
'startup_command' => $startup,
|
||||
'raw_startup_command' => $server->startup,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Server;
|
||||
@@ -35,66 +35,74 @@ use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*/
|
||||
protected $middleware = [
|
||||
TrustProxies::class,
|
||||
HandleCors::class,
|
||||
PreventRequestsDuringMaintenance::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*/
|
||||
protected $middleware = [
|
||||
TrustProxies::class,
|
||||
HandleCors::class,
|
||||
PreventRequestsDuringMaintenance::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
LanguageMiddleware::class,
|
||||
],
|
||||
'api' => [
|
||||
EnsureStatefulRequests::class,
|
||||
'auth:sanctum',
|
||||
IsValidJson::class,
|
||||
TrackAPIKey::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
AuthenticateIPAccess::class,
|
||||
],
|
||||
'application-api' => [
|
||||
SubstituteBindings::class,
|
||||
AuthenticateApplicationUser::class,
|
||||
],
|
||||
'client-api' => [
|
||||
SubstituteClientBindings::class,
|
||||
RequireClientApiKey::class,
|
||||
],
|
||||
'daemon' => [
|
||||
SubstituteBindings::class,
|
||||
DaemonAuthenticate::class,
|
||||
],
|
||||
];
|
||||
/* protected $middlewarePriority = [ */
|
||||
/* SubstituteClientBindings::class, */
|
||||
/* ]; */
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => AuthenticateSession::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'can' => Authorize::class,
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'node.maintenance' => MaintenanceMiddleware::class,
|
||||
'captcha' => \Pterodactyl\Http\Middleware\VerifyCaptcha::class,
|
||||
];
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
LanguageMiddleware::class,
|
||||
],
|
||||
'api' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
/* StartSession::class, */
|
||||
/* EnsureStatefulRequests::class, */
|
||||
'auth:sanctum',
|
||||
IsValidJson::class,
|
||||
TrackAPIKey::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
AuthenticateIPAccess::class,
|
||||
|
||||
],
|
||||
'application-api' => [
|
||||
SubstituteBindings::class,
|
||||
AuthenticateApplicationUser::class,
|
||||
],
|
||||
'client-api' => [
|
||||
SubstituteClientBindings::class,
|
||||
RequireClientApiKey::class,
|
||||
],
|
||||
'daemon' => [
|
||||
SubstituteBindings::class,
|
||||
DaemonAuthenticate::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => AuthenticateSession::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'can' => Authorize::class,
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'node.maintenance' => MaintenanceMiddleware::class,
|
||||
'captcha' => \Pterodactyl\Http\Middleware\VerifyCaptcha::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ class AuthenticateServerAccess
|
||||
/**
|
||||
* AuthenticateServerAccess constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Authenticate that this server exists and is not suspended or marked as installing.
|
||||
|
||||
26
app/Http/Middleware/Api/Client/Server/CheckDaemonType.php
Normal file
26
app/Http/Middleware/Api/Client/Server/CheckDaemonType.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Client\Server;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckDaemonType
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $daemon)
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
$daemonType = $server->node->daemonType;
|
||||
|
||||
if (! $daemonType) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($daemonType !== $daemon) {
|
||||
abort(400, "This endpoint requires daemon type '{$daemon}', but server is using '{$daemonType}'.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Requests\Admin\Node;
|
||||
|
||||
use Pterodactyl\Rules\Fqdn;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class NodeFormRequest extends AdminFormRequest
|
||||
@@ -16,12 +17,14 @@ class NodeFormRequest extends AdminFormRequest
|
||||
if ($this->method() === 'PATCH') {
|
||||
$rules = Node::getRulesForUpdate($this->route()->parameter('node'));
|
||||
$rules['internal_fqdn'] = ['nullable', 'string', Fqdn::make('scheme')];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
$data = Node::getRules();
|
||||
$data['fqdn'][] = Fqdn::make('scheme');
|
||||
$data['internal_fqdn'] = ['nullable', 'string', Fqdn::make('scheme')];
|
||||
log::info("rules", [$data]);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
use Pterodactyl\Extensions\Filesystem\S3Filesystem;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class BackupRemoteUploadController extends Controller
|
||||
{
|
||||
public const DEFAULT_MAX_PART_SIZE = 5 * 1024 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* BackupRemoteUploadController constructor.
|
||||
*/
|
||||
public function __construct(private BackupManager $backupManager) {}
|
||||
|
||||
/**
|
||||
* Returns the required presigned urls to upload a backup to S3 cloud storage.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function __invoke(Request $request, string $backup): JsonResponse
|
||||
{
|
||||
// Get the node associated with the request.
|
||||
/** @var \Pterodactyl\Models\Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
// Get the size query parameter.
|
||||
$size = (int) $request->query('size');
|
||||
if (empty($size)) {
|
||||
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
|
||||
}
|
||||
|
||||
/** @var Backup $model */
|
||||
$model = Backup::query()
|
||||
->where('uuid', $backup)
|
||||
->firstOrFail();
|
||||
|
||||
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||
// from messing with backups that they don't own.
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
}
|
||||
|
||||
// Prevent backups that have already been completed from trying to
|
||||
// be uploaded again.
|
||||
if (!is_null($model->completed_at)) {
|
||||
throw new ConflictHttpException('This backup is already in a completed state.');
|
||||
}
|
||||
|
||||
// Ensure we are using the S3 adapter.
|
||||
$adapter = $this->backupManager->adapter();
|
||||
if (!$adapter instanceof S3Filesystem) {
|
||||
throw new BadRequestHttpException('The configured backup adapter is not an S3 compatible adapter.');
|
||||
}
|
||||
|
||||
// The path where backup will be uploaded to
|
||||
$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);
|
||||
|
||||
// Get the S3 client
|
||||
$client = $adapter->getClient();
|
||||
$expires = CarbonImmutable::now()->addMinutes((int) config('backups.presigned_url_lifespan', 60));
|
||||
|
||||
// Params for generating the presigned urls
|
||||
$params = [
|
||||
'Bucket' => $adapter->getBucket(),
|
||||
'Key' => $path,
|
||||
'ContentType' => 'application/x-gzip',
|
||||
];
|
||||
|
||||
$storageClass = config('backups.disks.s3.storage_class');
|
||||
if (!is_null($storageClass)) {
|
||||
$params['StorageClass'] = $storageClass;
|
||||
}
|
||||
|
||||
// Execute the CreateMultipartUpload request
|
||||
$result = $client->execute($client->getCommand('CreateMultipartUpload', $params));
|
||||
|
||||
// Get the UploadId from the CreateMultipartUpload request, this is needed to create
|
||||
// the other presigned urls.
|
||||
$params['UploadId'] = $result->get('UploadId');
|
||||
|
||||
// Retrieve configured part size
|
||||
$maxPartSize = $this->getConfiguredMaxPartSize();
|
||||
|
||||
// Create as many UploadPart presigned urls as needed
|
||||
$parts = [];
|
||||
for ($i = 0; $i < ($size / $maxPartSize); ++$i) {
|
||||
$parts[] = $client->createPresignedRequest(
|
||||
$client->getCommand('UploadPart', array_merge($params, ['PartNumber' => $i + 1])),
|
||||
$expires
|
||||
)->getUri()->__toString();
|
||||
}
|
||||
|
||||
// Set the upload_id on the backup in the database.
|
||||
$model->update(['upload_id' => $params['UploadId']]);
|
||||
|
||||
return new JsonResponse([
|
||||
'parts' => $parts,
|
||||
'part_size' => $maxPartSize,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured maximum size of a single part in the multipart upload.
|
||||
*
|
||||
* The function tries to retrieve a configured value from the configuration.
|
||||
* If no value is specified, a fallback value will be used.
|
||||
*
|
||||
* Note if the received config cannot be converted to int (0), is zero or is negative,
|
||||
* the fallback value will be used too.
|
||||
*
|
||||
* The fallback value is {@see BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE}.
|
||||
*/
|
||||
private function getConfiguredMaxPartSize(): int
|
||||
{
|
||||
$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
|
||||
if ($maxPartSize <= 0) {
|
||||
$maxPartSize = self::DEFAULT_MAX_PART_SIZE;
|
||||
}
|
||||
|
||||
return $maxPartSize;
|
||||
}
|
||||
}
|
||||
159
app/Http/Requests/Api/Remote/Backups/BackupStatusController.php
Normal file
159
app/Http/Requests/Api/Remote/Backups/BackupStatusController.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
use Pterodactyl\Extensions\Filesystem\S3Filesystem;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
|
||||
|
||||
class BackupStatusController extends Controller
|
||||
{
|
||||
/**
|
||||
* BackupStatusController constructor.
|
||||
*/
|
||||
public function __construct(private BackupManager $backupManager) {}
|
||||
|
||||
/**
|
||||
* Handles updating the state of a backup.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
|
||||
{
|
||||
// Get the node associated with the request.
|
||||
/** @var \Pterodactyl\Models\Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
/** @var Backup $model */
|
||||
$model = Backup::query()
|
||||
->where('uuid', $backup)
|
||||
->firstOrFail();
|
||||
|
||||
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||
// from messing with backups that they don't own.
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
}
|
||||
|
||||
if ($model->is_successful) {
|
||||
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
|
||||
}
|
||||
|
||||
$action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.fail';
|
||||
$log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);
|
||||
|
||||
$log->transaction(function () use ($model, $request) {
|
||||
$successful = $request->boolean('successful');
|
||||
|
||||
$model->fill([
|
||||
'is_successful' => $successful,
|
||||
// Change the lock state to unlocked if this was a failed backup so that it can be
|
||||
// deleted easily. Also does not make sense to have a locked backup on the system
|
||||
// that is failed.
|
||||
'is_locked' => $successful ? $model->is_locked : false,
|
||||
'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null,
|
||||
'bytes' => $successful ? $request->input('size') : 0,
|
||||
'completed_at' => CarbonImmutable::now(),
|
||||
])->save();
|
||||
|
||||
// Check if we are using the s3 backup adapter. If so, make sure we mark the backup as
|
||||
// being completed in S3 correctly.
|
||||
$adapter = $this->backupManager->adapter();
|
||||
if ($adapter instanceof S3Filesystem) {
|
||||
$this->completeMultipartUpload($model, $adapter, $successful, $request->input('parts'));
|
||||
}
|
||||
});
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles toggling the restoration status of a server. The server status field should be
|
||||
* set back to null, even if the restoration failed. This is not an unsolvable state for
|
||||
* the server, and the user can keep trying to restore, or just use the reinstall button.
|
||||
*
|
||||
* The only thing the successful field does is update the entry value for the audit logs
|
||||
* table tracking for this restoration.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function restore(Request $request, string $backup): JsonResponse
|
||||
{
|
||||
/** @var Backup $model */
|
||||
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
|
||||
|
||||
$model->server->update(['status' => null]);
|
||||
|
||||
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
|
||||
->subject($model, $model->server)
|
||||
->property('name', $model->name)
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a multipart upload in a given S3-compatible instance as failed or successful for
|
||||
* the given backup.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws DisplayException
|
||||
*/
|
||||
protected function completeMultipartUpload(Backup $backup, S3Filesystem $adapter, bool $successful, ?array $parts): void
|
||||
{
|
||||
// This should never really happen, but if it does don't let us fall victim to Amazon's
|
||||
// wildly fun error messaging. Just stop the process right here.
|
||||
if (empty($backup->upload_id)) {
|
||||
// A failed backup doesn't need to error here, this can happen if the backup encounters
|
||||
// an error before we even start the upload. AWS gives you tooling to clear these failed
|
||||
// multipart uploads as needed too.
|
||||
if (!$successful) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DisplayException('Cannot complete backup request: no upload_id present on model.');
|
||||
}
|
||||
|
||||
$params = [
|
||||
'Bucket' => $adapter->getBucket(),
|
||||
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
|
||||
'UploadId' => $backup->upload_id,
|
||||
];
|
||||
|
||||
$client = $adapter->getClient();
|
||||
if (!$successful) {
|
||||
$client->execute($client->getCommand('AbortMultipartUpload', $params));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise send a CompleteMultipartUpload request.
|
||||
$params['MultipartUpload'] = [
|
||||
'Parts' => [],
|
||||
];
|
||||
|
||||
if (is_null($parts)) {
|
||||
$params['MultipartUpload']['Parts'] = $client->execute($client->getCommand('ListParts', $params))['Parts'];
|
||||
} else {
|
||||
foreach ($parts as $part) {
|
||||
$params['MultipartUpload']['Parts'][] = [
|
||||
'ETag' => $part['etag'],
|
||||
'PartNumber' => $part['part_number'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$client->execute($client->getCommand('CompleteMultipartUpload', $params));
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,11 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
SubdomainManagementService $subdomainService
|
||||
): void {
|
||||
$operation = null;
|
||||
|
||||
|
||||
try {
|
||||
$operation = ServerOperation::where('operation_id', $this->operationId)->firstOrFail();
|
||||
$operation->markAsStarted();
|
||||
|
||||
|
||||
Activity::actor($this->user)->event('server:software.change-started')
|
||||
->property([
|
||||
'operation_id' => $this->operationId,
|
||||
@@ -105,7 +105,6 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
$this->logSuccessfulChange();
|
||||
|
||||
$operation->markAsCompleted('Software configuration applied successfully. Server installation completed.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->handleJobFailure($e, $operation);
|
||||
throw $e;
|
||||
@@ -121,18 +120,18 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
|
||||
$currentEgg = $this->server->egg;
|
||||
$targetEgg = Egg::find($this->eggId);
|
||||
|
||||
|
||||
$backupName = sprintf(
|
||||
'Software Change: %s → %s (%s)',
|
||||
$currentEgg->name ?? 'Unknown',
|
||||
$targetEgg->name ?? 'Unknown',
|
||||
now()->format('M j, g:i A')
|
||||
);
|
||||
|
||||
|
||||
if (strlen($backupName) > 190) {
|
||||
$backupName = substr($backupName, 0, 187) . '...';
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$result = $elytraJobService->submitJob(
|
||||
$this->server,
|
||||
@@ -159,7 +158,6 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
$operation->updateProgress('Backup job submitted successfully');
|
||||
|
||||
return $result['job_id'];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw new BackupFailedException('Failed to create backup before egg change: ' . $e->getMessage());
|
||||
}
|
||||
@@ -211,12 +209,12 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
private function wipeServerFiles(DaemonFileRepository $fileRepository, ServerOperation $operation): void
|
||||
{
|
||||
$operation->updateProgress('Wiping server files...');
|
||||
|
||||
|
||||
try {
|
||||
$contents = $fileRepository->setServer($this->server)->getDirectory('/');
|
||||
|
||||
if (!empty($contents)) {
|
||||
$filesToDelete = array_map(function($item) {
|
||||
$filesToDelete = array_map(function ($item) {
|
||||
return $item['name'];
|
||||
}, $contents);
|
||||
|
||||
@@ -273,7 +271,7 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
SubdomainManagementService $subdomainService
|
||||
): void {
|
||||
$operation->updateProgress('Applying software configuration...');
|
||||
|
||||
|
||||
DB::transaction(function () use ($egg, $startupModificationService, $reinstallServerService, $operation, $subdomainService) {
|
||||
// Check if we need to remove subdomain before changing egg
|
||||
$activeSubdomain = $this->server->activeSubdomain;
|
||||
@@ -282,14 +280,14 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
$tempServer = clone $this->server;
|
||||
$tempServer->egg = $egg;
|
||||
$tempServer->egg_id = $egg->id;
|
||||
|
||||
|
||||
// If new egg doesn't support subdomains, delete the existing subdomain
|
||||
if (!$tempServer->supportsSubdomains()) {
|
||||
$operation->updateProgress('Removing incompatible subdomain...');
|
||||
|
||||
|
||||
try {
|
||||
$subdomainService->deleteSubdomain($activeSubdomain);
|
||||
|
||||
|
||||
Activity::actor($this->user)->event('server:subdomain.deleted-egg-change')
|
||||
->property([
|
||||
'operation_id' => $this->operationId,
|
||||
@@ -305,13 +303,13 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
'subdomain' => $activeSubdomain->full_domain,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
|
||||
// Continue with egg change even if subdomain deletion fails
|
||||
$operation->updateProgress('Warning: Could not fully remove subdomain, continuing with egg change...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($this->server->egg_id !== $this->eggId || $this->server->nest_id !== $this->nestId) {
|
||||
$this->server->update([
|
||||
'egg_id' => $this->eggId,
|
||||
@@ -331,7 +329,7 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
|
||||
$operation->updateProgress('Reinstalling server...');
|
||||
$reinstallServerService->handle($updatedServer);
|
||||
|
||||
|
||||
$operation->updateProgress('Finalizing installation...');
|
||||
});
|
||||
}
|
||||
@@ -398,4 +396,4 @@ class ApplyEggChangeJob extends Job implements ShouldQueue
|
||||
])
|
||||
->log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Pterodactyl\Enums\Daemon\Adapters;
|
||||
|
||||
/**
|
||||
* Backup model
|
||||
@@ -41,9 +42,13 @@ class Backup extends Model
|
||||
public const RESOURCE_NAME = 'backup';
|
||||
|
||||
// Backup adapters
|
||||
public const ADAPTER_WINGS = 'wings';
|
||||
public const ADAPTER_ELYTRA = 'elytra'; // Preferred name for local backups
|
||||
public const ADAPTER_AWS_S3 = 's3';
|
||||
|
||||
// Wings Adapters
|
||||
public const ADAPTER_WINGS = 'wings';
|
||||
|
||||
// Elytra Backups
|
||||
public const ADAPTER_ELYTRA = 'elytra';
|
||||
public const ADAPTER_RUSTIC_LOCAL = 'rustic_local';
|
||||
public const ADAPTER_RUSTIC_S3 = 'rustic_s3';
|
||||
|
||||
@@ -82,6 +87,7 @@ class Backup extends Model
|
||||
return in_array($this->disk, [self::ADAPTER_RUSTIC_LOCAL, self::ADAPTER_RUSTIC_S3]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if this backup is stored locally (not in cloud storage).
|
||||
*/
|
||||
@@ -95,7 +101,7 @@ class Backup extends Model
|
||||
*/
|
||||
public function getRepositoryType(): ?string
|
||||
{
|
||||
return match($this->disk) {
|
||||
return match ($this->disk) {
|
||||
self::ADAPTER_RUSTIC_LOCAL => 'local',
|
||||
self::ADAPTER_RUSTIC_S3 => 's3',
|
||||
default => null,
|
||||
@@ -161,9 +167,8 @@ class Backup extends Model
|
||||
*/
|
||||
public function getElytraAdapterType(): string
|
||||
{
|
||||
return match($this->disk) {
|
||||
self::ADAPTER_WINGS => 'elytra', // Legacy support: wings -> elytra
|
||||
self::ADAPTER_ELYTRA => 'elytra', // Direct mapping for new backups
|
||||
return match ($this->disk) {
|
||||
self::ADAPTER_ELYTRA => 'elytra',
|
||||
self::ADAPTER_AWS_S3 => 's3',
|
||||
self::ADAPTER_RUSTIC_LOCAL => 'rustic_local',
|
||||
self::ADAPTER_RUSTIC_S3 => 'rustic_s3',
|
||||
@@ -219,4 +224,4 @@ class Backup extends Model
|
||||
{
|
||||
return $this->query()->where($field ?? $this->getRouteKeyName(), $value)->firstOrFail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
app/Models/Daemons/.info
Normal file
1
app/Models/Daemons/.info
Normal file
@@ -0,0 +1 @@
|
||||
These files are used in app/Models/Node.php for it's configuration getting and such
|
||||
93
app/Models/Daemons/Elytra.php
Normal file
93
app/Models/Daemons/Elytra.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Models\Daemons;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
|
||||
use Pterodactyl\Contracts\Daemon\Daemon;
|
||||
|
||||
class Elytra implements Daemon
|
||||
{
|
||||
public function getConfiguration(Node $node): array
|
||||
{
|
||||
return [
|
||||
'debug' => false,
|
||||
'uuid' => $node->uuid,
|
||||
'token_id' => $node->daemon_token_id,
|
||||
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($node->daemon_token),
|
||||
'api' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => $node->daemonListen,
|
||||
'ssl' => [
|
||||
'enabled' => (!$node->behind_proxy && $node->scheme === 'https'),
|
||||
'cert' => '/etc/letsencrypt/live/' . Str::lower($node->getInternalFqdn()) . '/fullchain.pem',
|
||||
'key' => '/etc/letsencrypt/live/' . Str::lower($node->getInternalFqdn()) . '/privkey.pem',
|
||||
],
|
||||
'upload_limit' => $node->upload_size,
|
||||
],
|
||||
'system' => [
|
||||
'data' => $node->daemonBase,
|
||||
'sftp' => [
|
||||
'bind_port' => $node->daemonSFTP,
|
||||
],
|
||||
'backups' => [
|
||||
'rustic' => $this->getBackupConfiguration(),
|
||||
],
|
||||
],
|
||||
'allowed_mounts' => $node->mounts->pluck('source')->toArray(),
|
||||
'remote' => route('index'),
|
||||
'allowed_origins' => [
|
||||
config('app.url'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getBackupConfiguration()
|
||||
{
|
||||
$localConfig = config('backups.disks.rustic_local', []);
|
||||
$s3Config = config('backups.disks.rustic_s3', []);
|
||||
return [
|
||||
// Path to rustic binary
|
||||
'binary_path' => $localConfig['binary_path'] ?? 'rustic',
|
||||
|
||||
// Repository version (optional, default handled by rustic)
|
||||
'repository_version' => $localConfig['repository_version'] ?? 2,
|
||||
|
||||
// Pack size configuration for performance tuning
|
||||
'tree_pack_size_mb' => $localConfig['tree_pack_size_mb'] ?? 4,
|
||||
'data_pack_size_mb' => $localConfig['data_pack_size_mb'] ?? 32,
|
||||
|
||||
// Local repository configuration
|
||||
'local' => [
|
||||
'enabled' => !empty($localConfig),
|
||||
'repository_path' => $localConfig['repository_path'] ?? '/var/lib/pterodactyl/rustic-repos',
|
||||
'use_cold_storage' => $localConfig['use_cold_storage'] ?? false,
|
||||
'hot_repository_path' => $localConfig['hot_repository_path'] ?? '',
|
||||
],
|
||||
|
||||
// S3 repository configuration
|
||||
's3' => [
|
||||
'enabled' => !empty($s3Config['bucket']),
|
||||
'endpoint' => $s3Config['endpoint'] ?? '',
|
||||
'region' => $s3Config['region'] ?? 'us-east-1',
|
||||
'bucket' => $s3Config['bucket'] ?? '',
|
||||
'use_cold_storage' => $s3Config['use_cold_storage'] ?? false,
|
||||
'hot_bucket' => $s3Config['hot_bucket'] ?? '',
|
||||
'cold_storage_class' => $s3Config['cold_storage_class'] ?? 'GLACIER',
|
||||
'force_path_style' => $s3Config['force_path_style'] ?? false,
|
||||
'disable_ssl' => $s3Config['disable_ssl'] ?? false,
|
||||
'ca_cert_path' => $s3Config['ca_cert_path'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAutoDeploy(Node $node, string $token): string
|
||||
{
|
||||
$debugFlag = config('app.debug') ? ' --allow-insecure' : '';
|
||||
|
||||
return "cd /etc/elytra && sudo elytra configure --panel-url " . escapeshellarg(config('app.url')) . " --token " . escapeshellarg($token) . " --node " . escapeshellarg((string) $node->id) . $debugFlag . "";
|
||||
}
|
||||
}
|
||||
50
app/Models/Daemons/Wings.php
Normal file
50
app/Models/Daemons/Wings.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Models\Daemons;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Pterodactyl\Contracts\Daemon\Daemon;
|
||||
|
||||
class Wings implements Daemon
|
||||
{
|
||||
public function getConfiguration(Node $node): array
|
||||
{
|
||||
return [
|
||||
'debug' => false,
|
||||
'uuid' => $node->uuid,
|
||||
'token_id' => $node->daemon_token_id,
|
||||
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($node->daemon_token),
|
||||
'api' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => $node->daemonListen,
|
||||
'ssl' => [
|
||||
'enabled' => (!$node->behind_proxy && $node->scheme === 'https'),
|
||||
'cert' => '/etc/letsencrypt/live/' . Str::lower($node->getInternalFqdn()) . '/fullchain.pem',
|
||||
'key' => '/etc/letsencrypt/live/' . Str::lower($node->getInternalFqdn()) . '/privkey.pem',
|
||||
],
|
||||
'upload_limit' => $node->upload_size,
|
||||
],
|
||||
'system' => [
|
||||
'data' => $node->daemonBase,
|
||||
'sftp' => [
|
||||
'bind_port' => $node->daemonSFTP,
|
||||
],
|
||||
],
|
||||
'allowed_mounts' => $node->mounts->pluck('source')->toArray(),
|
||||
'remote' => route('index'),
|
||||
'allowed_origins' => [
|
||||
config('app.url'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAutoDeploy(Node $node, string $token): string
|
||||
{
|
||||
$debugFlag = config('app.debug') ? ' --allow-insecure' : '';
|
||||
|
||||
return "cd /etc/pterodactyl && sudo wings configure --panel-url " . escapeshellarg(config('app.url')) . " --token " . escapeshellarg($token) . " --node " . escapeshellarg((string) $node->id) . $debugFlag . "";
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Enums\Daemon\DaemonType;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Pterodactyl\Contracts\Daemon\Daemon as DaemonInterface;
|
||||
use Pterodactyl\Http\Controllers\Admin\NodeAutoDeployController;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -36,6 +39,8 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
* @property int $daemonListen
|
||||
* @property int $daemonSFTP
|
||||
* @property string $daemonBase
|
||||
* @property string $daemonType
|
||||
* @property string $backupDisk
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Location $location
|
||||
@@ -43,6 +48,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
|
||||
*/
|
||||
|
||||
class Node extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\NodeFactory> */
|
||||
@@ -110,6 +116,8 @@ class Node extends Model
|
||||
'daemon_token',
|
||||
'description',
|
||||
'maintenance_mode',
|
||||
'daemonType',
|
||||
'backupDisk'
|
||||
];
|
||||
|
||||
public static array $validationRules = [
|
||||
@@ -132,6 +140,8 @@ class Node extends Model
|
||||
'daemonListen' => 'required|numeric|between:1,65535',
|
||||
'maintenance_mode' => 'boolean',
|
||||
'upload_size' => 'int|between:1,1024',
|
||||
'daemonType' => 'required|string',
|
||||
'backupDisk' => 'required|string'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -150,6 +160,23 @@ class Node extends Model
|
||||
'use_separate_fqdns' => false,
|
||||
];
|
||||
|
||||
|
||||
private function getDaemonImplementation(): DaemonInterface
|
||||
{
|
||||
$implementations = DaemonType::allClass();
|
||||
|
||||
$daemonType = strtolower($this->daemonType);
|
||||
|
||||
if (!isset($implementations[$daemonType])) {
|
||||
|
||||
return new \Pterodactyl\Models\Daemons\Elytra();
|
||||
}
|
||||
|
||||
$implementationClass = $implementations[$daemonType];
|
||||
return new $implementationClass();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the connection address to use when making calls to this node.
|
||||
* This will use the internal FQDN if separate FQDNs are enabled and internal_fqdn is set,
|
||||
@@ -188,80 +215,17 @@ class Node extends Model
|
||||
*/
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'debug' => false,
|
||||
'uuid' => $this->uuid,
|
||||
'token_id' => $this->daemon_token_id,
|
||||
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($this->daemon_token),
|
||||
'api' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => $this->daemonListen,
|
||||
'ssl' => [
|
||||
'enabled' => (!$this->behind_proxy && $this->scheme === 'https'),
|
||||
'cert' => '/etc/letsencrypt/live/' . Str::lower($this->getInternalFqdn()) . '/fullchain.pem',
|
||||
'key' => '/etc/letsencrypt/live/' . Str::lower($this->getInternalFqdn()) . '/privkey.pem',
|
||||
],
|
||||
'upload_limit' => $this->upload_size,
|
||||
],
|
||||
'system' => [
|
||||
'data' => $this->daemonBase,
|
||||
'sftp' => [
|
||||
'bind_port' => $this->daemonSFTP,
|
||||
],
|
||||
'backups' => [
|
||||
'rustic' => $this->getRusticBackupConfiguration(),
|
||||
],
|
||||
],
|
||||
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
|
||||
'remote' => route('index'),
|
||||
'allowed_origins' => [
|
||||
config('app.url'), // note: I have no idea why this wasn't included by Pterodactyl upstream, this might need to be configurable later - ellie
|
||||
],
|
||||
];
|
||||
$daemon = $this->getDaemonImplementation();
|
||||
return $daemon->getConfiguration($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rustic backup configuration for Wings.
|
||||
* Matches the exact structure expected by elytra rustic implementation.
|
||||
* Returns the auto deploy command as a string.
|
||||
*/
|
||||
private function getRusticBackupConfiguration(): array
|
||||
public function getAutoDeploy(string $token): string
|
||||
{
|
||||
$localConfig = config('backups.disks.rustic_local', []);
|
||||
$s3Config = config('backups.disks.rustic_s3', []);
|
||||
|
||||
return [
|
||||
// Path to rustic binary
|
||||
'binary_path' => $localConfig['binary_path'] ?? 'rustic',
|
||||
|
||||
// Repository version (optional, default handled by rustic)
|
||||
'repository_version' => $localConfig['repository_version'] ?? 2,
|
||||
|
||||
// Pack size configuration for performance tuning
|
||||
'tree_pack_size_mb' => $localConfig['tree_pack_size_mb'] ?? 4,
|
||||
'data_pack_size_mb' => $localConfig['data_pack_size_mb'] ?? 32,
|
||||
|
||||
// Local repository configuration
|
||||
'local' => [
|
||||
'enabled' => !empty($localConfig),
|
||||
'repository_path' => $localConfig['repository_path'] ?? '/var/lib/pterodactyl/rustic-repos',
|
||||
'use_cold_storage' => $localConfig['use_cold_storage'] ?? false,
|
||||
'hot_repository_path' => $localConfig['hot_repository_path'] ?? '',
|
||||
],
|
||||
|
||||
// S3 repository configuration
|
||||
's3' => [
|
||||
'enabled' => !empty($s3Config['bucket']),
|
||||
'endpoint' => $s3Config['endpoint'] ?? '',
|
||||
'region' => $s3Config['region'] ?? 'us-east-1',
|
||||
'bucket' => $s3Config['bucket'] ?? '',
|
||||
'use_cold_storage' => $s3Config['use_cold_storage'] ?? false,
|
||||
'hot_bucket' => $s3Config['hot_bucket'] ?? '',
|
||||
'cold_storage_class' => $s3Config['cold_storage_class'] ?? 'GLACIER',
|
||||
'force_path_style' => $s3Config['force_path_style'] ?? false,
|
||||
'disable_ssl' => $s3Config['disable_ssl'] ?? false,
|
||||
'ca_cert_path' => $s3Config['ca_cert_path'] ?? '',
|
||||
],
|
||||
];
|
||||
$daemon = $this->getDaemonImplementation();
|
||||
return $daemon->getAutoDeploy($this, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,234 +6,234 @@ use Illuminate\Support\Collection;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal.
|
||||
*/
|
||||
public const RESOURCE_NAME = 'subuser_permission';
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal.
|
||||
*/
|
||||
public const RESOURCE_NAME = 'subuser_permission';
|
||||
|
||||
/**
|
||||
* Constants defining different permissions available.
|
||||
*/
|
||||
public const ACTION_WEBSOCKET_CONNECT = 'websocket.connect';
|
||||
public const ACTION_CONTROL_CONSOLE = 'control.console';
|
||||
public const ACTION_CONTROL_START = 'control.start';
|
||||
public const ACTION_CONTROL_STOP = 'control.stop';
|
||||
public const ACTION_CONTROL_RESTART = 'control.restart';
|
||||
/**
|
||||
* Constants defining different permissions available.
|
||||
*/
|
||||
public const ACTION_WEBSOCKET_CONNECT = 'websocket.connect';
|
||||
public const ACTION_CONTROL_CONSOLE = 'control.console';
|
||||
public const ACTION_CONTROL_START = 'control.start';
|
||||
public const ACTION_CONTROL_STOP = 'control.stop';
|
||||
public const ACTION_CONTROL_RESTART = 'control.restart';
|
||||
|
||||
public const ACTION_DATABASE_READ = 'database.read';
|
||||
public const ACTION_DATABASE_CREATE = 'database.create';
|
||||
public const ACTION_DATABASE_UPDATE = 'database.update';
|
||||
public const ACTION_DATABASE_DELETE = 'database.delete';
|
||||
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view_password';
|
||||
public const ACTION_DATABASE_READ = 'database.read';
|
||||
public const ACTION_DATABASE_CREATE = 'database.create';
|
||||
public const ACTION_DATABASE_UPDATE = 'database.update';
|
||||
public const ACTION_DATABASE_DELETE = 'database.delete';
|
||||
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view_password';
|
||||
|
||||
public const ACTION_SCHEDULE_READ = 'schedule.read';
|
||||
public const ACTION_SCHEDULE_CREATE = 'schedule.create';
|
||||
public const ACTION_SCHEDULE_UPDATE = 'schedule.update';
|
||||
public const ACTION_SCHEDULE_DELETE = 'schedule.delete';
|
||||
public const ACTION_SCHEDULE_READ = 'schedule.read';
|
||||
public const ACTION_SCHEDULE_CREATE = 'schedule.create';
|
||||
public const ACTION_SCHEDULE_UPDATE = 'schedule.update';
|
||||
public const ACTION_SCHEDULE_DELETE = 'schedule.delete';
|
||||
|
||||
public const ACTION_USER_READ = 'user.read';
|
||||
public const ACTION_USER_CREATE = 'user.create';
|
||||
public const ACTION_USER_UPDATE = 'user.update';
|
||||
public const ACTION_USER_DELETE = 'user.delete';
|
||||
public const ACTION_USER_READ = 'user.read';
|
||||
public const ACTION_USER_CREATE = 'user.create';
|
||||
public const ACTION_USER_UPDATE = 'user.update';
|
||||
public const ACTION_USER_DELETE = 'user.delete';
|
||||
|
||||
public const ACTION_BACKUP_READ = 'backup.read';
|
||||
public const ACTION_BACKUP_CREATE = 'backup.create';
|
||||
public const ACTION_BACKUP_DELETE = 'backup.delete';
|
||||
public const ACTION_BACKUP_DOWNLOAD = 'backup.download';
|
||||
public const ACTION_BACKUP_RESTORE = 'backup.restore';
|
||||
public const ACTION_BACKUP_READ = 'backup.read';
|
||||
public const ACTION_BACKUP_CREATE = 'backup.create';
|
||||
public const ACTION_BACKUP_DELETE = 'backup.delete';
|
||||
public const ACTION_BACKUP_DOWNLOAD = 'backup.download';
|
||||
public const ACTION_BACKUP_RESTORE = 'backup.restore';
|
||||
|
||||
public const ACTION_ALLOCATION_READ = 'allocation.read';
|
||||
public const ACTION_ALLOCATION_CREATE = 'allocation.create';
|
||||
public const ACTION_ALLOCATION_UPDATE = 'allocation.update';
|
||||
public const ACTION_ALLOCATION_DELETE = 'allocation.delete';
|
||||
public const ACTION_ALLOCATION_READ = 'allocation.read';
|
||||
public const ACTION_ALLOCATION_CREATE = 'allocation.create';
|
||||
public const ACTION_ALLOCATION_UPDATE = 'allocation.update';
|
||||
public const ACTION_ALLOCATION_DELETE = 'allocation.delete';
|
||||
|
||||
public const ACTION_FILE_READ = 'file.read';
|
||||
public const ACTION_FILE_READ_CONTENT = 'file.read-content';
|
||||
public const ACTION_FILE_CREATE = 'file.create';
|
||||
public const ACTION_FILE_UPDATE = 'file.update';
|
||||
public const ACTION_FILE_DELETE = 'file.delete';
|
||||
public const ACTION_FILE_ARCHIVE = 'file.archive';
|
||||
public const ACTION_FILE_SFTP = 'file.sftp';
|
||||
public const ACTION_FILE_READ = 'file.read';
|
||||
public const ACTION_FILE_READ_CONTENT = 'file.read-content';
|
||||
public const ACTION_FILE_CREATE = 'file.create';
|
||||
public const ACTION_FILE_UPDATE = 'file.update';
|
||||
public const ACTION_FILE_DELETE = 'file.delete';
|
||||
public const ACTION_FILE_ARCHIVE = 'file.archive';
|
||||
public const ACTION_FILE_SFTP = 'file.sftp';
|
||||
|
||||
public const ACTION_STARTUP_READ = 'startup.read';
|
||||
public const ACTION_STARTUP_UPDATE = 'startup.update';
|
||||
public const ACTION_STARTUP_COMMAND = 'startup.command';
|
||||
public const ACTION_STARTUP_DOCKER_IMAGE = 'startup.docker-image';
|
||||
public const ACTION_STARTUP_READ = 'startup.read';
|
||||
public const ACTION_STARTUP_UPDATE = 'startup.update';
|
||||
public const ACTION_STARTUP_COMMAND = 'startup.command';
|
||||
public const ACTION_STARTUP_DOCKER_IMAGE = 'startup.docker-image';
|
||||
|
||||
public const ACTION_STARTUP_SOFTWARE = 'startup.software';
|
||||
public const ACTION_STARTUP_SOFTWARE = 'startup.software';
|
||||
|
||||
public const ACTION_SETTINGS_RENAME = 'settings.rename';
|
||||
public const ACTION_SETTINGS_MODRINTH = 'settings.modrinth';
|
||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||
public const ACTION_SETTINGS_RENAME = 'settings.rename';
|
||||
public const ACTION_SETTINGS_MODR = 'settings.mod';
|
||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||
|
||||
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||
|
||||
public const ACTION_MODRINTH_DOWNLOAD = 'modrinth.download';
|
||||
public const ACTION_MOD_DOWNLOAD = 'mod.download';
|
||||
|
||||
/**
|
||||
* Should timestamps be used on this model.
|
||||
*/
|
||||
public $timestamps = false;
|
||||
/**
|
||||
* Should timestamps be used on this model.
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*/
|
||||
protected $table = 'permissions';
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*/
|
||||
protected $table = 'permissions';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* Cast values to correct type.
|
||||
*/
|
||||
protected $casts = [
|
||||
'subuser_id' => 'integer',
|
||||
];
|
||||
/**
|
||||
* Cast values to correct type.
|
||||
*/
|
||||
protected $casts = [
|
||||
'subuser_id' => 'integer',
|
||||
];
|
||||
|
||||
public static array $validationRules = [
|
||||
'subuser_id' => 'required|numeric|min:1',
|
||||
'permission' => 'required|string',
|
||||
];
|
||||
public static array $validationRules = [
|
||||
'subuser_id' => 'required|numeric|min:1',
|
||||
'permission' => 'required|string',
|
||||
];
|
||||
|
||||
/**
|
||||
* All the permissions available on the system. You should use self::permissions()
|
||||
* to retrieve them, and not directly access this array as it is subject to change.
|
||||
*
|
||||
* @see \Pterodactyl\Models\Permission::permissions()
|
||||
*/
|
||||
protected static array $permissions = [
|
||||
'websocket' => [
|
||||
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
|
||||
'keys' => [
|
||||
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
|
||||
],
|
||||
],
|
||||
/**
|
||||
* All the permissions available on the system. You should use self::permissions()
|
||||
* to retrieve them, and not directly access this array as it is subject to change.
|
||||
*
|
||||
* @see \Pterodactyl\Models\Permission::permissions()
|
||||
*/
|
||||
protected static array $permissions = [
|
||||
'websocket' => [
|
||||
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
|
||||
'keys' => [
|
||||
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
|
||||
],
|
||||
],
|
||||
|
||||
'control' => [
|
||||
'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||
'keys' => [
|
||||
'console' => 'Allows a user to send commands to the server instance via the console.',
|
||||
'start' => 'Allows a user to start the server if it is stopped.',
|
||||
'stop' => 'Allows a user to stop a server if it is running.',
|
||||
'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.',
|
||||
],
|
||||
],
|
||||
'control' => [
|
||||
'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||
'keys' => [
|
||||
'console' => 'Allows a user to send commands to the server instance via the console.',
|
||||
'start' => 'Allows a user to start the server if it is stopped.',
|
||||
'stop' => 'Allows a user to stop a server if it is running.',
|
||||
'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.',
|
||||
],
|
||||
],
|
||||
|
||||
'user' => [
|
||||
'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new subusers for the server.',
|
||||
'read' => 'Allows the user to view subusers and their permissions for the server.',
|
||||
'update' => 'Allows a user to modify other subusers.',
|
||||
'delete' => 'Allows a user to delete a subuser from the server.',
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new subusers for the server.',
|
||||
'read' => 'Allows the user to view subusers and their permissions for the server.',
|
||||
'update' => 'Allows a user to modify other subusers.',
|
||||
'delete' => 'Allows a user to delete a subuser from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
|
||||
'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
|
||||
'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
|
||||
'update' => 'Allows a user to update the contents of an existing file or directory.',
|
||||
'delete' => 'Allows a user to delete files or directories.',
|
||||
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',
|
||||
'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.',
|
||||
],
|
||||
],
|
||||
'file' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
|
||||
'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
|
||||
'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
|
||||
'update' => 'Allows a user to update the contents of an existing file or directory.',
|
||||
'delete' => 'Allows a user to delete files or directories.',
|
||||
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',
|
||||
'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.',
|
||||
],
|
||||
],
|
||||
|
||||
'backup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to generate and manage server backups.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new backups for this server.',
|
||||
'read' => 'Allows a user to view all backups that exist for this server.',
|
||||
'delete' => 'Allows a user to remove backups from the system.',
|
||||
'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.',
|
||||
'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all of the server files in the process.',
|
||||
],
|
||||
],
|
||||
'backup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to generate and manage server backups.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new backups for this server.',
|
||||
'read' => 'Allows a user to view all backups that exist for this server.',
|
||||
'delete' => 'Allows a user to remove backups from the system.',
|
||||
'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.',
|
||||
'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all of the server files in the process.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's allocations.
|
||||
'allocation' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
|
||||
'create' => 'Allows a user to assign additional allocations to the server.',
|
||||
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
|
||||
'delete' => 'Allows a user to delete an allocation from the server.',
|
||||
],
|
||||
],
|
||||
// Controls permissions for editing or viewing a server's allocations.
|
||||
'allocation' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
|
||||
'create' => 'Allows a user to assign additional allocations to the server.',
|
||||
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
|
||||
'delete' => 'Allows a user to delete an allocation from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's startup parameters.
|
||||
'startup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the startup variables for a server.',
|
||||
'update' => 'Allows a user to modify the startup variables for the server.',
|
||||
'command' => 'Allows a user to modify the startup command for the server.',
|
||||
'docker-image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||
'software' => 'Allows a user to modify the game / software used for the server.',
|
||||
],
|
||||
],
|
||||
// Controls permissions for editing or viewing a server's startup parameters.
|
||||
'startup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the startup variables for a server.',
|
||||
'update' => 'Allows a user to modify the startup variables for the server.',
|
||||
'command' => 'Allows a user to modify the startup command for the server.',
|
||||
'docker-image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||
'software' => 'Allows a user to modify the game / software used for the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'description' => 'Permissions that control a user\'s access to the database management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create a new database for this server.',
|
||||
'read' => 'Allows a user to view the database associated with this server.',
|
||||
'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.',
|
||||
'delete' => 'Allows a user to remove a database instance from this server.',
|
||||
'view_password' => 'Allows a user to view the password associated with a database instance for this server.',
|
||||
],
|
||||
],
|
||||
'database' => [
|
||||
'description' => 'Permissions that control a user\'s access to the database management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create a new database for this server.',
|
||||
'read' => 'Allows a user to view the database associated with this server.',
|
||||
'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.',
|
||||
'delete' => 'Allows a user to remove a database instance from this server.',
|
||||
'view_password' => 'Allows a user to view the password associated with a database instance for this server.',
|
||||
],
|
||||
],
|
||||
|
||||
'schedule' => [
|
||||
'description' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new schedules for this server.', // task.create-schedule
|
||||
'read' => 'Allows a user to view schedules and the tasks associated with them for this server.', // task.view-schedule, task.list-schedules
|
||||
'update' => 'Allows a user to update schedules and schedule tasks for this server.', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
|
||||
'delete' => 'Allows a user to delete schedules for this server.', // task.delete-schedule
|
||||
],
|
||||
],
|
||||
'schedule' => [
|
||||
'description' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new schedules for this server.', // task.create-schedule
|
||||
'read' => 'Allows a user to view schedules and the tasks associated with them for this server.', // task.view-schedule, task.list-schedules
|
||||
'update' => 'Allows a user to update schedules and schedule tasks for this server.', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
|
||||
'delete' => 'Allows a user to delete schedules for this server.', // task.delete-schedule
|
||||
],
|
||||
],
|
||||
|
||||
'settings' => [
|
||||
'description' => 'Permissions that control a user\'s access to the settings for this server.',
|
||||
'keys' => [
|
||||
'rename' => 'Allows a user to rename this server and change the description of it.',
|
||||
'reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||
'settings' => [
|
||||
'description' => 'Permissions that control a user\'s access to the settings for this server.',
|
||||
'keys' => [
|
||||
'rename' => 'Allows a user to rename this server and change the description of it.',
|
||||
'reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'activity' => [
|
||||
'description' => 'Permissions that control a user\'s access to the server activity logs.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the activity logs for the server.',
|
||||
],
|
||||
],
|
||||
'activity' => [
|
||||
'description' => 'Permissions that control a user\'s access to the server activity logs.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the activity logs for the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'modrinth' => [
|
||||
'description' => 'Permissions that control a user\'s access to downloading and updating mods.',
|
||||
'keys' => [
|
||||
'version' => 'Allows a user to change what version to download for',
|
||||
'loader' => 'Allows a user to change what loader to download for',
|
||||
'download' => 'Allows a user to download mods to the server using modrinth',
|
||||
'resolver' => 'Allows a user to access the Dependency Resolver',
|
||||
'update' => 'Allows a user to update Currently installed mods',
|
||||
],
|
||||
],
|
||||
];
|
||||
'mod' => [
|
||||
'description' => 'Permissions that control a user\'s access to downloading and updating mods.',
|
||||
'keys' => [
|
||||
'version' => 'Allows a user to change what version to download for',
|
||||
'loader' => 'Allows a user to change what loader to download for',
|
||||
'download' => 'Allows a user to download mods to the server',
|
||||
'resolver' => 'Allows a user to access the Dependency Resolver',
|
||||
'update' => 'Allows a user to update Currently installed mods',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns all the permissions available on the system for a user to
|
||||
* have when controlling a server.
|
||||
*/
|
||||
public static function permissions(): Collection
|
||||
{
|
||||
return Collection::make(self::$permissions);
|
||||
}
|
||||
/**
|
||||
* Returns all the permissions available on the system for a user to
|
||||
* have when controlling a server.
|
||||
*/
|
||||
public static function permissions(): Collection
|
||||
{
|
||||
return Collection::make(self::$permissions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ class Server extends Model
|
||||
if (!$this->egg || !is_array($this->egg->docker_images) || empty($this->egg->docker_images)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return !in_array($this->image, array_values($this->egg->docker_images));
|
||||
}
|
||||
|
||||
@@ -252,14 +252,14 @@ class Server extends Model
|
||||
if (!$this->egg || !is_array($this->egg->docker_images) || empty($this->egg->docker_images)) {
|
||||
throw new \RuntimeException('Server egg has no docker images configured.');
|
||||
}
|
||||
|
||||
|
||||
$eggDockerImages = $this->egg->docker_images;
|
||||
$defaultImage = reset($eggDockerImages);
|
||||
|
||||
|
||||
if (empty($defaultImage)) {
|
||||
throw new \RuntimeException('Server egg has no valid default docker image.');
|
||||
}
|
||||
|
||||
|
||||
return $defaultImage;
|
||||
}
|
||||
|
||||
@@ -530,7 +530,27 @@ class Server extends Model
|
||||
if (
|
||||
$this->isSuspended()
|
||||
|| $this->node->isUnderMaintenance()
|
||||
|| !$this->isInstalled()
|
||||
/* || !$this->isInstalled() */
|
||||
|| $this->status === self::STATUS_RESTORING_BACKUP
|
||||
|| !is_null($this->transfer)
|
||||
) {
|
||||
throw new ServerStateConflictException($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is currently in a user-accessible state. If not, an
|
||||
* exception is raised. This should be called whenever something needs to make
|
||||
* sure the server is not in a weird state that should block user access.
|
||||
*
|
||||
* @throws ServerStateConflictException
|
||||
*/
|
||||
public function validateCurrentStateClient()
|
||||
{
|
||||
if (
|
||||
$this->isSuspended()
|
||||
|| $this->node->isUnderMaintenance()
|
||||
/* || !$this->isInstalled() */ // NOTE: this causes issues with how users view servers with the new system
|
||||
|| $this->status === self::STATUS_RESTORING_BACKUP
|
||||
|| !is_null($this->transfer)
|
||||
) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Pterodactyl\Providers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Enums\Limits\ResourceLimit;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
@@ -106,5 +107,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
config('http.rate_limit.application')
|
||||
)->by($key);
|
||||
});
|
||||
ResourceLimit::boot();
|
||||
}
|
||||
}
|
||||
|
||||
45
app/Repositories/Eloquent/BackupRepository.php
Normal file
45
app/Repositories/Eloquent/BackupRepository.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Eloquent;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BackupRepository extends EloquentRepository
|
||||
{
|
||||
public function model(): string
|
||||
{
|
||||
return Backup::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if too many backups have been generated by the server.
|
||||
*/
|
||||
public function getBackupsGeneratedDuringTimespan(int $server, int $seconds = 600): array|Collection
|
||||
{
|
||||
return $this->getBuilder()
|
||||
->withTrashed()
|
||||
->where('server_id', $server)
|
||||
->where(function ($query) {
|
||||
$query->whereNull('completed_at')
|
||||
->orWhere('is_successful', '=', true);
|
||||
})
|
||||
->where('created_at', '>=', Carbon::now()->subSeconds($seconds)->toDateTimeString())
|
||||
->get()
|
||||
->toBase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query filtering only non-failed backups for a specific server.
|
||||
*/
|
||||
public function getNonFailedBackups(Server $server): HasMany
|
||||
{
|
||||
return $server->backups()->where(function ($query) {
|
||||
$query->whereNull('completed_at')
|
||||
->orWhere('is_successful', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -176,4 +176,16 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
|
||||
|
||||
return $instance->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a node with the given id with the Node's resource usage.
|
||||
*/
|
||||
public function getDaemonType(int $node_id): Node
|
||||
{
|
||||
$instance = $this->getBuilder()
|
||||
->select(['nodes.daemonType'])
|
||||
->where('nodes.id', $node_id);
|
||||
|
||||
return $instance->first();
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Repositories/Elytra/DaemonCommandRepository.php
Normal file
37
app/Repositories/Elytra/DaemonCommandRepository.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonCommandRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonCommandRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonCommandRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Sends a command or multiple commands to a running server instance.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function send(array|string $command): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/commands', $this->server->uuid),
|
||||
[
|
||||
'json' => ['commands' => is_array($command) ? $command : [$command]],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/Repositories/Elytra/DaemonConfigurationRepository.php
Normal file
50
app/Repositories/Elytra/DaemonConfigurationRepository.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonConfigurationRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Returns system information from the wings instance.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function getSystemInformation(?int $version = null): array
|
||||
{
|
||||
try {
|
||||
$response = $this->getHttpClient()->get('/api/system' . (!is_null($version) ? '?v=' . $version : ''));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
return json_decode($response->getBody()->__toString(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the configuration information for a daemon. Updates the information for
|
||||
* this instance using a passed-in model. This allows us to change plenty of information
|
||||
* in the model, and still use the old, pre-update model to actually make the HTTP request.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function update(Node $node): ResponseInterface
|
||||
{
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
'/api/update',
|
||||
['json' => $node->getConfiguration()]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
301
app/Repositories/Elytra/DaemonFileRepository.php
Normal file
301
app/Repositories/Elytra/DaemonFileRepository.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonFileRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonFileRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonFileRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Return the contents of a given file.
|
||||
*
|
||||
* @param int|null $notLargerThan the maximum content length in bytes
|
||||
*
|
||||
* @throws TransferException
|
||||
* @throws FileSizeTooLargeException
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function getContent(string $path, ?int $notLargerThan = null): string
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$response = $this->getHttpClient()->get(
|
||||
sprintf('/api/servers/%s/files/contents', $this->server->uuid),
|
||||
[
|
||||
'query' => ['file' => $path],
|
||||
]
|
||||
);
|
||||
} catch (ClientException|TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
$length = (int) Arr::get($response->getHeader('Content-Length'), 0, 0);
|
||||
if ($notLargerThan && $length > $notLargerThan) {
|
||||
throw new FileSizeTooLargeException();
|
||||
}
|
||||
|
||||
return $response->getBody()->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save new contents to a given file. This works for both creating and updating
|
||||
* a file.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function putContent(string $path, string $content): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/write', $this->server->uuid),
|
||||
[
|
||||
'query' => ['file' => $path],
|
||||
'body' => $content,
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a directory listing for a given path.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function getDirectory(string $path): array
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$response = $this->getHttpClient()->get(
|
||||
sprintf('/api/servers/%s/files/list-directory', $this->server->uuid),
|
||||
[
|
||||
'query' => ['directory' => $path],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory for the server in the given $path.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function createDirectory(string $name, string $path): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames or moves a file on the remote machine.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function renameFiles(?string $root, array $files): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->put(
|
||||
sprintf('/api/servers/%s/files/rename', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'root' => $root ?? '/',
|
||||
'files' => $files,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a given file and give it a unique name.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function copyFile(string $location): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'location' => $location,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file or folder for the server.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function deleteFiles(?string $root, array $files): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'root' => $root ?? '/',
|
||||
'files' => $files,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress the given files or folders in the given root.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function compressFiles(?string $root, array $files): array
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$response = $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'root' => $root ?? '/',
|
||||
'files' => $files,
|
||||
],
|
||||
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
|
||||
// since it will likely take quite awhile for large directories.
|
||||
'timeout' => 60 * 15,
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompresses a given archive file.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function decompressFile(?string $root, string $file): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/decompress', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'root' => $root ?? '/',
|
||||
'file' => $file,
|
||||
],
|
||||
// Wait for up to 15 minutes for the decompress to be completed when calling this endpoint
|
||||
// since it will likely take quite awhile for large directories.
|
||||
'timeout' => 60 * 15,
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chmods the given files.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function chmodFiles(?string $root, array $files): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/chmod', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'root' => $root ?? '/',
|
||||
'files' => $files,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a file from the given URL and saves it to the disk.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function pull(string $url, ?string $directory, array $params = []): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
$attributes = [
|
||||
'url' => $url,
|
||||
'root' => $directory ?? '/',
|
||||
'file_name' => $params['filename'] ?? null,
|
||||
'use_header' => $params['use_header'] ?? null,
|
||||
'foreground' => $params['foreground'] ?? null,
|
||||
];
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/pull', $this->server->uuid),
|
||||
[
|
||||
'json' => array_filter($attributes, fn ($value) => !is_null($value)),
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/Repositories/Elytra/DaemonPowerRepository.php
Normal file
35
app/Repositories/Elytra/DaemonPowerRepository.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonPowerRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonPowerRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonPowerRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Sends a power action to the server instance.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function send(string $action): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/power', $this->server->uuid),
|
||||
['json' => ['action' => $action]]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
app/Repositories/Elytra/DaemonRepository.php
Normal file
69
app/Repositories/Elytra/DaemonRepository.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
abstract class DaemonRepository
|
||||
{
|
||||
protected ?Server $server;
|
||||
|
||||
protected ?Node $node;
|
||||
|
||||
/**
|
||||
* DaemonRepository constructor.
|
||||
*/
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the server model this request is stemming from.
|
||||
*/
|
||||
public function setServer(Server $server): self
|
||||
{
|
||||
$this->server = $server;
|
||||
|
||||
$this->setNode($this->server->node);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the node model this request is stemming from.
|
||||
*/
|
||||
public function setNode(Node $node): self
|
||||
{
|
||||
$this->node = $node;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the Guzzle HTTP Client to be used for requests.
|
||||
*/
|
||||
public function getHttpClient(array $headers = []): Client
|
||||
{
|
||||
Assert::isInstanceOf($this->node, Node::class);
|
||||
|
||||
return new Client([
|
||||
'verify' => $this->app->environment('production'),
|
||||
'base_uri' => $this->node->getConnectionAddress(),
|
||||
'timeout' => config('pterodactyl.guzzle.timeout'),
|
||||
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),
|
||||
'headers' => array_merge($headers, [
|
||||
'Authorization' => 'Bearer ' . $this->node->getDecryptedKey(),
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
162
app/Repositories/Elytra/DaemonServerRepository.php
Normal file
162
app/Repositories/Elytra/DaemonServerRepository.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonServerRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonServerRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonServerRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Returns details about a server from the Daemon instance.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function getDetails(): array
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$response = $this->getHttpClient()->get(
|
||||
sprintf('/api/servers/%s', $this->server->uuid)
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception, false);
|
||||
}
|
||||
|
||||
return json_decode($response->getBody()->__toString(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new server on the Wings daemon.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function create(bool $startOnCompletion = true): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post('/api/servers', [
|
||||
'json' => [
|
||||
'uuid' => $this->server->uuid,
|
||||
'start_on_completion' => $startOnCompletion,
|
||||
],
|
||||
]);
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a server sync on Wings.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function sync(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a server from the daemon, forcibly if passed.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->delete('/api/servers/' . $this->server->uuid);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstall a server on the daemon.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function reinstall(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post(sprintf(
|
||||
'/api/servers/%s/reinstall',
|
||||
$this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the daemon to create a full archive of the server. Once the daemon is finished
|
||||
* they will send a POST request to "/api/remote/servers/{uuid}/archive" with a boolean.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function requestArchive(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post(sprintf(
|
||||
'/api/servers/%s/archive',
|
||||
$this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a single user's JTI by using their ID. This is simply a helper function to
|
||||
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
|
||||
* correctly and avoids any costly mistakes in the codebase.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function revokeUserJTI(int $id): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
$this->revokeJTIs([md5($id . $this->server->uuid)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes an array of JWT JTI's by marking any token generated before the current time on
|
||||
* the Wings instance as being invalid.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
protected function revokeJTIs(array $jtis): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()
|
||||
->post(sprintf('/api/servers/%s/ws/deny', $this->server->uuid), [
|
||||
'json' => ['jtis' => $jtis],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Repositories/Elytra/DaemonTransferRepository.php
Normal file
37
app/Repositories/Elytra/DaemonTransferRepository.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonTransferRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonTransferRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonTransferRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function notify(Node $targetNode, Plain $token): void
|
||||
{
|
||||
try {
|
||||
$this->getHttpClient()->post(sprintf('/api/servers/%s/transfer', $this->server->uuid), [
|
||||
'json' => [
|
||||
'server_id' => $this->server->uuid,
|
||||
'url' => $targetNode->getConnectionAddress() . '/api/transfers',
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'server' => [
|
||||
'uuid' => $this->server->uuid,
|
||||
'start_on_completion' => false,
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/Repositories/Wings/DaemonBackupRepository.php
Normal file
97
app/Repositories/Wings/DaemonBackupRepository.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
/**
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonBackupRepository setNode(\Pterodactyl\Models\Node $node)
|
||||
* @method \Pterodactyl\Repositories\Wings\DaemonBackupRepository setServer(\Pterodactyl\Models\Server $server)
|
||||
*/
|
||||
class DaemonBackupRepository extends DaemonRepository
|
||||
{
|
||||
protected ?string $adapter;
|
||||
|
||||
/**
|
||||
* Sets the backup adapter for this execution instance.
|
||||
*/
|
||||
public function setBackupAdapter(string $adapter): self
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the remote Daemon to begin generating a backup for the server.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function backup(Backup $backup): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/backup', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'adapter' => $this->adapter ?? config('backups.default'),
|
||||
'uuid' => $backup->uuid,
|
||||
'ignore' => implode("\n", $backup->ignored_files),
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Wings to begin restoring a backup for a server.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function restore(Backup $backup, ?string $url = null, bool $truncate = false): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/backup/%s/restore', $this->server->uuid, $backup->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'adapter' => $backup->disk,
|
||||
'truncate_directory' => $truncate,
|
||||
'download_url' => $url ?? '',
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup from the daemon.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function delete(Backup $backup): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->delete(
|
||||
sprintf('/api/servers/%s/backup/%s', $this->server->uuid, $backup->uuid)
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class DaemonFileRepository extends DaemonRepository
|
||||
'query' => ['file' => $path],
|
||||
]
|
||||
);
|
||||
} catch (ClientException|TransferException $exception) {
|
||||
} catch (ClientException | TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ class DaemonFileRepository extends DaemonRepository
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/files/pull', $this->server->uuid),
|
||||
[
|
||||
'json' => array_filter($attributes, fn ($value) => !is_null($value)),
|
||||
'json' => array_filter($attributes, fn($value) => !is_null($value)),
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
|
||||
@@ -21,9 +21,7 @@ abstract class DaemonRepository
|
||||
/**
|
||||
* DaemonRepository constructor.
|
||||
*/
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
}
|
||||
public function __construct(protected Application $app) {}
|
||||
|
||||
/**
|
||||
* Set the server model this request is stemming from.
|
||||
|
||||
28
app/Repositories/Wings/DaemonRevocationRepository.php
Normal file
28
app/Repositories/Wings/DaemonRevocationRepository.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DaemonRevocationRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for
|
||||
* the provided servers. If no servers are provided, the user is deauthorized on all
|
||||
* servers on the instance.
|
||||
*
|
||||
* @param string[] $servers
|
||||
*/
|
||||
|
||||
public function deauthorize(string $user, array $servers = []): void
|
||||
{
|
||||
try {
|
||||
$this->getHttpClient()->post('/api/deauthorize-user', [
|
||||
'json' => ['user' => $user, 'servers' => $servers],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
app/Services/Backups/Wings/DeleteBackupService.php
Normal file
81
app/Services/Backups/Wings/DeleteBackupService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Backups\Wings;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
use Pterodactyl\Exceptions\Service\Backup\BackupLockedException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DeleteBackupService
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
private BackupManager $manager,
|
||||
private DaemonBackupRepository $daemonBackupRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes a backup from the system. If the backup is stored in S3 a request
|
||||
* will be made to delete that backup from the disk as well.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Backup $backup): void
|
||||
{
|
||||
// If the backup is marked as failed it can still be deleted, even if locked
|
||||
// since the UI doesn't allow you to unlock a failed backup in the first place.
|
||||
//
|
||||
// I also don't really see any reason you'd have a locked, failed backup to keep
|
||||
// around. The logic that updates the backup to the failed state will also remove
|
||||
// the lock, so this condition should really never happen.
|
||||
if ($backup->is_locked && ($backup->is_successful && !is_null($backup->completed_at))) {
|
||||
throw new BackupLockedException();
|
||||
}
|
||||
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$this->deleteFromS3($backup);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($backup) {
|
||||
try {
|
||||
$this->daemonBackupRepository->setServer($backup->server)->delete($backup);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$previous = $exception->getPrevious();
|
||||
// Don't fail the request if the Daemon responds with a 404, just assume the backup
|
||||
// doesn't actually exist and remove its reference from the Panel as well.
|
||||
if (!$previous instanceof ClientException || $previous->getResponse()->getStatusCode() !== Response::HTTP_NOT_FOUND) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$backup->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup from an S3 disk.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function deleteFromS3(Backup $backup): void
|
||||
{
|
||||
$this->connection->transaction(function () use ($backup) {
|
||||
$backup->delete();
|
||||
|
||||
/** @var \Pterodactyl\Extensions\Filesystem\S3Filesystem $adapter */
|
||||
$adapter = $this->manager->adapter(Backup::ADAPTER_AWS_S3);
|
||||
|
||||
$adapter->getClient()->deleteObject([
|
||||
'Bucket' => $adapter->getBucket(),
|
||||
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
60
app/Services/Backups/Wings/DownloadLinkService.php
Normal file
60
app/Services/Backups/Wings/DownloadLinkService.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Backups\Wings;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
|
||||
class DownloadLinkService
|
||||
{
|
||||
/**
|
||||
* DownloadLinkService constructor.
|
||||
*/
|
||||
public function __construct(private BackupManager $backupManager, private NodeJWTService $jwtService) {}
|
||||
|
||||
/**
|
||||
* Returns the URL that allows for a backup to be downloaded by an individual
|
||||
* user, or by the Wings control software.
|
||||
*/
|
||||
public function handle(Backup $backup, User $user): string
|
||||
{
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
return $this->getS3BackupUrl($backup);
|
||||
}
|
||||
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser($user)
|
||||
->setClaims([
|
||||
'backup_uuid' => $backup->uuid,
|
||||
'server_uuid' => $backup->server->uuid,
|
||||
])
|
||||
->handle($backup->server->node, $user->id . $backup->server->uuid);
|
||||
|
||||
return sprintf('%s/download/backup?token=%s', $backup->server->node->getConnectionAddress(), $token->toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a signed URL that allows us to download a file directly out of a non-public
|
||||
* S3 bucket by using a signed URL.
|
||||
*/
|
||||
protected function getS3BackupUrl(Backup $backup): string
|
||||
{
|
||||
/** @var \Pterodactyl\Extensions\Filesystem\S3Filesystem $adapter */
|
||||
$adapter = $this->backupManager->adapter(Backup::ADAPTER_AWS_S3);
|
||||
|
||||
$request = $adapter->getClient()->createPresignedRequest(
|
||||
$adapter->getClient()->getCommand('GetObject', [
|
||||
'Bucket' => $adapter->getBucket(),
|
||||
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
|
||||
'ContentType' => 'application/x-gzip',
|
||||
]),
|
||||
CarbonImmutable::now()->addMinutes(5)
|
||||
);
|
||||
|
||||
return $request->getUri()->__toString();
|
||||
}
|
||||
}
|
||||
134
app/Services/Backups/Wings/InitiateBackupService.php
Normal file
134
app/Services/Backups/Wings/InitiateBackupService.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Backups\Wings;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
use Pterodactyl\Repositories\Eloquent\BackupRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
use Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
|
||||
class InitiateBackupService
|
||||
{
|
||||
private ?array $ignoredFiles;
|
||||
|
||||
private bool $isLocked = false;
|
||||
|
||||
/**
|
||||
* InitiateBackupService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private BackupRepository $repository,
|
||||
private ConnectionInterface $connection,
|
||||
private DaemonBackupRepository $daemonBackupRepository,
|
||||
private DeleteBackupService $deleteBackupService,
|
||||
private BackupManager $backupManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set if the backup should be locked once it is created which will prevent
|
||||
* its deletion by users or automated system processes.
|
||||
*/
|
||||
public function setIsLocked(bool $isLocked): self
|
||||
{
|
||||
$this->isLocked = $isLocked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files to be ignored by this backup.
|
||||
*
|
||||
* @param string[]|null $ignored
|
||||
*/
|
||||
public function setIgnoredFiles(?array $ignored): self
|
||||
{
|
||||
if (is_array($ignored)) {
|
||||
foreach ($ignored as $value) {
|
||||
Assert::string($value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the ignored files to be any values that are not empty in the array. Don't use
|
||||
// the PHP empty function here incase anything that is "empty" by default (0, false, etc.)
|
||||
// were passed as a file or folder name.
|
||||
$this->ignoredFiles = is_null($ignored) ? [] : array_filter($ignored, function ($value) {
|
||||
return strlen($value) > 0;
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the backup process for a server on Wings.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws TooManyBackupsException
|
||||
* @throws TooManyRequestsHttpException
|
||||
*/
|
||||
public function handle(Server $server, ?string $name = null, bool $override = false): Backup
|
||||
{
|
||||
$limit = config('backups.throttles.limit');
|
||||
$period = config('backups.throttles.period');
|
||||
if ($period > 0) {
|
||||
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, $period);
|
||||
if ($previous->count() >= $limit) {
|
||||
$message = sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period);
|
||||
|
||||
throw new TooManyRequestsHttpException((int) CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), $message);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the server has reached or exceeded its backup limit.
|
||||
// completed_at == null will cover any ongoing backups, while is_successful == true will cover any completed backups.
|
||||
$successful = $this->repository->getNonFailedBackups($server);
|
||||
if ($server->backup_limit == null) {
|
||||
$server->backup_limit = $successful->count() + 1;
|
||||
};
|
||||
if (!$server->backup_limit || $successful->count() >= $server->backup_limit) {
|
||||
// Do not allow the user to continue if this server is already at its limit and can't override.
|
||||
if ($server->backup_limit == null) {
|
||||
$server->backup_limit = 12;
|
||||
};
|
||||
|
||||
if (!$override || $server->backup_limit <= 0) {
|
||||
throw new TooManyBackupsException($server->backup_limit);
|
||||
}
|
||||
|
||||
// Get the oldest backup the server has that is not "locked" (indicating a backup that should
|
||||
// never be automatically purged). If we find a backup we will delete it and then continue with
|
||||
// this process. If no backup is found that can be used an exception is thrown.
|
||||
/** @var Backup $oldest */
|
||||
$oldest = $successful->where('is_locked', false)->orderBy('created_at')->first();
|
||||
if (!$oldest) {
|
||||
throw new TooManyBackupsException($server->backup_limit);
|
||||
}
|
||||
|
||||
$this->deleteBackupService->handle($oldest);
|
||||
}
|
||||
|
||||
return $this->connection->transaction(function () use ($server, $name) {
|
||||
/** @var Backup $backup */
|
||||
$backup = $this->repository->create([
|
||||
'server_id' => $server->id,
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'name' => trim($name) ?: sprintf('Backup at %s', CarbonImmutable::now()->toDateTimeString()),
|
||||
'ignored_files' => array_values($this->ignoredFiles ?? []),
|
||||
'disk' => $server->node->backupDisk,
|
||||
'is_locked' => $this->isLocked,
|
||||
], true, true);
|
||||
|
||||
$this->daemonBackupRepository->setServer($server)
|
||||
->setBackupAdapter($server->node->backupDisk)
|
||||
->backup($backup);
|
||||
|
||||
return $backup;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Pterodactyl\Services\Captcha;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Manager;
|
||||
use Pterodactyl\Services\Captcha\Providers\TurnstileProvider;
|
||||
use Pterodactyl\Services\Captcha\Providers\HCaptchaProvider;
|
||||
@@ -73,6 +74,8 @@ class CaptchaManager extends Manager
|
||||
*/
|
||||
public function getWidget(): string
|
||||
{
|
||||
|
||||
|
||||
if ($this->getDefaultDriver() === 'none') {
|
||||
return '';
|
||||
}
|
||||
@@ -103,4 +106,4 @@ class CaptchaManager extends Manager
|
||||
|
||||
return $this->driver()->getScriptIncludes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RecaptchaProvider implements CaptchaProviderInterface
|
||||
{
|
||||
private const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
/* private const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; */
|
||||
|
||||
protected string $siteKey;
|
||||
protected string $secretKey;
|
||||
protected string $verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
@@ -29,7 +30,7 @@ class RecaptchaProvider implements CaptchaProviderInterface
|
||||
try {
|
||||
$httpResponse = Http::timeout(10)
|
||||
->asForm()
|
||||
->post(self::VERIFY_URL, [
|
||||
->post($this->verifyUrl, [
|
||||
'secret' => $this->secretKey,
|
||||
'response' => $response,
|
||||
'remoteip' => $remoteIp,
|
||||
@@ -106,4 +107,5 @@ class RecaptchaProvider implements CaptchaProviderInterface
|
||||
{
|
||||
return !empty($this->siteKey) && !empty($this->secretKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,13 +372,9 @@ class BackupJob implements Job
|
||||
}
|
||||
}
|
||||
|
||||
private function handleRestoreCompletion(ElytraJob $job, array $statusData): void
|
||||
{
|
||||
}
|
||||
private function handleRestoreCompletion(ElytraJob $job, array $statusData): void {}
|
||||
|
||||
private function handleDownloadCompletion(ElytraJob $job, array $statusData): void
|
||||
{
|
||||
}
|
||||
private function handleDownloadCompletion(ElytraJob $job, array $statusData): void {}
|
||||
|
||||
private function submitDeleteAllJob(Server $server, ElytraJob $job, ElytraRepository $elytraRepository): string
|
||||
{
|
||||
@@ -576,4 +572,5 @@ class BackupJob implements Job
|
||||
{
|
||||
return 'Backup operation failed. Please contact an administrator for details.'; // todo: better sanitization - elllie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,10 @@ use Pterodactyl\Models\ServerSubdomain;
|
||||
use Pterodactyl\Contracts\Dns\DnsProviderInterface;
|
||||
use Pterodactyl\Contracts\Subdomain\SubdomainFeatureInterface;
|
||||
use Pterodactyl\Exceptions\Dns\DnsProviderException;
|
||||
use Pterodactyl\Services\Subdomain\Features\FactorioSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\MinecraftSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\RustSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\ScpSlSubdomainFeature;
|
||||
use Pterodactyl\Services\Subdomain\Features\TeamSpeakSubdomainFeature;
|
||||
use Pterodactyl\Services\Dns\Providers\CloudflareProvider;
|
||||
use Pterodactyl\Services\Dns\Providers\HetznerProvider;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Services\Dns\Providers\Route53Provider;
|
||||
use Pterodactyl\Enums\Subdomain\Providers;
|
||||
use Pterodactyl\Enums\Subdomain\Features;
|
||||
|
||||
class SubdomainManagementService
|
||||
{
|
||||
@@ -27,20 +21,10 @@ class SubdomainManagementService
|
||||
public function __construct()
|
||||
{
|
||||
// Register DNS providers
|
||||
$this->dnsProviders = [
|
||||
'cloudflare' => CloudflareProvider::class,
|
||||
'hetzner' => HetznerProvider::class,
|
||||
'route53' => Route53Provider::class
|
||||
];
|
||||
$this->dnsProviders = Providers::all();
|
||||
|
||||
// Register subdomain features
|
||||
$this->subdomainFeatures = [
|
||||
'subdomain_factorio' => FactorioSubdomainFeature::class,
|
||||
'subdomain_minecraft' => MinecraftSubdomainFeature::class,
|
||||
'subdomain_rust' => RustSubdomainFeature::class,
|
||||
'subdomain_scpsl' => ScpSlSubdomainFeature::class,
|
||||
'subdomain_teamspeak' => TeamSpeakSubdomainFeature::class,
|
||||
];
|
||||
$this->subdomainFeatures = Features::all();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,136 +16,138 @@ use Pterodactyl\Services\Servers\StartupCommandService;
|
||||
|
||||
class ServerTransformer extends BaseClientTransformer
|
||||
{
|
||||
protected array $defaultIncludes = ['allocations', 'variables'];
|
||||
protected array $defaultIncludes = ['allocations', 'variables'];
|
||||
|
||||
protected array $availableIncludes = ['egg', 'subusers'];
|
||||
protected array $availableIncludes = ['egg', 'subusers'];
|
||||
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Server::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a server model into a representation that can be returned
|
||||
* to a client.
|
||||
*/
|
||||
public function transform(Server $server): array
|
||||
{
|
||||
/** @var StartupCommandService $service */
|
||||
$service = Container::getInstance()->make(StartupCommandService::class);
|
||||
|
||||
$user = $this->request->user();
|
||||
|
||||
return [
|
||||
'server_owner' => $user->id === $server->owner_id,
|
||||
'identifier' => $server->uuidShort,
|
||||
'internal_id' => $server->id,
|
||||
'uuid' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'node' => $server->node->name,
|
||||
'is_node_under_maintenance' => $server->node->isUnderMaintenance(),
|
||||
'sftp_details' => [
|
||||
'ip' => $server->node->fqdn,
|
||||
'port' => $server->node->daemonSFTP,
|
||||
],
|
||||
'sftp_alias' => [
|
||||
'ip' => $server->node->SFTPAliasAddress,
|
||||
'port' => $server->node->SFTPAliasPort
|
||||
],
|
||||
'description' => $server->description,
|
||||
'limits' => [
|
||||
'memory' => $server->memory,
|
||||
'overhead_memory' => $server->overhead_memory,
|
||||
'swap' => $server->swap,
|
||||
'disk' => $server->disk,
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
],
|
||||
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||
'docker_image' => $server->image,
|
||||
'egg_features' => $server->egg->inherit_features,
|
||||
'egg' => $server->egg->uuid,
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
'allocations' => $server->allocation_limit,
|
||||
'backups' => $server->backup_limit,
|
||||
'backupStorageMb' => $server->backup_storage_limit,
|
||||
],
|
||||
'status' => $server->status,
|
||||
// This field is deprecated, please use "status".
|
||||
'is_suspended' => $server->isSuspended(),
|
||||
// This field is deprecated, please use "status".
|
||||
'is_installing' => !$server->isInstalled(),
|
||||
'is_transferring' => !is_null($server->transfer),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allocations associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Server $server): Collection
|
||||
{
|
||||
$transformer = $this->makeTransformer(AllocationTransformer::class);
|
||||
|
||||
$user = $this->request->user();
|
||||
// While we include this permission, we do need to actually handle it slightly different here
|
||||
// for the purpose of keeping things functionally working. If the user doesn't have read permissions
|
||||
// for the allocations we'll only return the primary server allocation, and any notes associated
|
||||
// with it will be hidden.
|
||||
//
|
||||
// This allows us to avoid too much permission regression, without also hiding information that
|
||||
// is generally needed for the frontend to make sense when browsing or searching results.
|
||||
if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) {
|
||||
$primary = clone $server->allocation;
|
||||
$primary->notes = null;
|
||||
|
||||
return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME);
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Server::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME);
|
||||
}
|
||||
/**
|
||||
* Transform a server model into a representation that can be returned
|
||||
* to a client.
|
||||
*/
|
||||
public function transform(Server $server): array
|
||||
{
|
||||
/** @var StartupCommandService $service */
|
||||
$service = Container::getInstance()->make(StartupCommandService::class);
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeVariables(Server $server): Collection|NullResource
|
||||
{
|
||||
if (!$this->request->user()->can(Permission::ACTION_STARTUP_READ, $server)) {
|
||||
return $this->null();
|
||||
$user = $this->request->user();
|
||||
|
||||
return [
|
||||
'server_owner' => $user->id === $server->owner_id,
|
||||
'identifier' => $server->uuidShort,
|
||||
'internal_id' => $server->id,
|
||||
'uuid' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'node' => $server->node->name,
|
||||
'is_node_under_maintenance' => $server->node->isUnderMaintenance(),
|
||||
'sftp_details' => [
|
||||
'ip' => $server->node->fqdn,
|
||||
'port' => $server->node->daemonSFTP,
|
||||
],
|
||||
'sftp_alias' => [
|
||||
'ip' => $server->node->SFTPAliasAddress,
|
||||
'port' => $server->node->SFTPAliasPort
|
||||
],
|
||||
'description' => $server->description,
|
||||
'limits' => [
|
||||
'memory' => $server->memory,
|
||||
'overhead_memory' => $server->overhead_memory,
|
||||
'swap' => $server->swap,
|
||||
'disk' => $server->disk,
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
],
|
||||
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||
'docker_image' => $server->image,
|
||||
'egg_features' => $server->egg->inherit_features,
|
||||
'egg' => $server->egg->uuid,
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
'allocations' => $server->allocation_limit,
|
||||
'backups' => $server->backup_limit,
|
||||
'backupStorageMb' => $server->backup_storage_limit,
|
||||
],
|
||||
'status' => $server->status,
|
||||
// This field is deprecated, please use "status".
|
||||
'is_suspended' => $server->isSuspended(),
|
||||
// This field is deprecated, please use "status".
|
||||
'is_installing' => !$server->isInstalled(),
|
||||
'is_transferring' => !is_null($server->transfer),
|
||||
'daemon_type' => $server->node->daemonType,
|
||||
'backup_disk' => $server->node->backupDisk,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->collection(
|
||||
$server->variables->where('user_viewable', true),
|
||||
$this->makeTransformer(EggVariableTransformer::class),
|
||||
EggVariable::RESOURCE_NAME
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns the allocations associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Server $server): Collection
|
||||
{
|
||||
$transformer = $this->makeTransformer(AllocationTransformer::class);
|
||||
|
||||
/**
|
||||
* Returns the egg associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeEgg(Server $server): Item
|
||||
{
|
||||
return $this->item($server->egg, $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME);
|
||||
}
|
||||
$user = $this->request->user();
|
||||
// While we include this permission, we do need to actually handle it slightly different here
|
||||
// for the purpose of keeping things functionally working. If the user doesn't have read permissions
|
||||
// for the allocations we'll only return the primary server allocation, and any notes associated
|
||||
// with it will be hidden.
|
||||
//
|
||||
// This allows us to avoid too much permission regression, without also hiding information that
|
||||
// is generally needed for the frontend to make sense when browsing or searching results.
|
||||
if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) {
|
||||
$primary = clone $server->allocation;
|
||||
$primary->notes = null;
|
||||
|
||||
/**
|
||||
* Returns the subusers associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeSubusers(Server $server): Collection|NullResource
|
||||
{
|
||||
if (!$this->request->user()->can(Permission::ACTION_USER_READ, $server)) {
|
||||
return $this->null();
|
||||
return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME);
|
||||
}
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeVariables(Server $server): Collection|NullResource
|
||||
{
|
||||
if (!$this->request->user()->can(Permission::ACTION_STARTUP_READ, $server)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
return $this->collection(
|
||||
$server->variables->where('user_viewable', true),
|
||||
$this->makeTransformer(EggVariableTransformer::class),
|
||||
EggVariable::RESOURCE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the egg associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeEgg(Server $server): Item
|
||||
{
|
||||
return $this->item($server->egg, $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subusers associated with this server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeSubusers(Server $server): Collection|NullResource
|
||||
{
|
||||
if (!$this->request->user()->can(Permission::ACTION_USER_READ, $server)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pyrodactyl-oss/pyrodactyl",
|
||||
"description": "Pyrodactyl is the Pterodactyl-based game server panel that's faster, smaller, safer, and more accessible.",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Pyrodactyl-Oss",
|
||||
@@ -10,7 +10,7 @@
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Pyro Host Inc.",
|
||||
"name": "Pyro, Inc.",
|
||||
"email": "team@pyro.host",
|
||||
"homepage": "https://pyro.host",
|
||||
"role": "Former Maintainer"
|
||||
@@ -72,13 +72,15 @@
|
||||
],
|
||||
"psr-4": {
|
||||
"Pterodactyl\\": "app/",
|
||||
"Pyrodactyl\\": "app/",
|
||||
"Database\\Factories\\": "database/Factories/",
|
||||
"Database\\Seeders\\": "database/Seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Pterodactyl\\Tests\\": "tests/"
|
||||
"Pterodactyl\\Tests\\": "tests/",
|
||||
"Pyrodactyl\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
// will be stored in this location by default. It is possible to change this once backups
|
||||
// have been made, without losing data.
|
||||
// Options: elytra, wings (legacy), s3, rustic_local, rustic_s3
|
||||
'default' => env('APP_BACKUP_DRIVER', Backup::ADAPTER_RUSTIC_LOCAL),
|
||||
'default' => env('APP_BACKUP_DRIVER', Backup::ADAPTER_WINGS),
|
||||
|
||||
// This value is used to determine the lifespan of UploadPart presigned urls that wings
|
||||
// uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => env('CAPTCHA_ENABLED', false),
|
||||
'enabled' => env('CAPTCHA_ENABLED', true),
|
||||
];
|
||||
|
||||
@@ -26,5 +26,5 @@ return [
|
||||
| That way, you can access vars, like "SomeNamespace.someVariable."
|
||||
|
|
||||
*/
|
||||
'js_namespace' => 'Pterodactyl',
|
||||
'js_namespace' => 'Pyrodactyl',
|
||||
];
|
||||
|
||||
@@ -10,35 +10,37 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class NodeFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Node::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'public' => true,
|
||||
'name' => 'FactoryNode_' . Str::random(10),
|
||||
'fqdn' => $this->faker->unique()->ipv4,
|
||||
'scheme' => 'http',
|
||||
'behind_proxy' => false,
|
||||
'memory' => 1024,
|
||||
'memory_overallocate' => 0,
|
||||
'disk' => 10240,
|
||||
'disk_overallocate' => 0,
|
||||
'upload_size' => 100,
|
||||
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
||||
'daemon_token' => Crypt::encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
|
||||
'daemonListen' => 8080,
|
||||
'daemonSFTP' => 2022,
|
||||
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
||||
];
|
||||
}
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Node::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'public' => true,
|
||||
'name' => 'FactoryNode_' . Str::random(10),
|
||||
'fqdn' => $this->faker->unique()->ipv4,
|
||||
'scheme' => 'http',
|
||||
'behind_proxy' => false,
|
||||
'memory' => 1024,
|
||||
'memory_overallocate' => 0,
|
||||
'disk' => 10240,
|
||||
'disk_overallocate' => 0,
|
||||
'upload_size' => 100,
|
||||
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
||||
'daemon_token' => Crypt::encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
|
||||
'daemonListen' => 8080,
|
||||
'daemonSFTP' => 2022,
|
||||
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
||||
'backupDisk' => 'local',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
94
database/Seeders/eggs/hytale/egg-hytale.json
Normal file
94
database/Seeders/eggs/hytale/egg-hytale.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2026-01-13T05:17:28+00:00",
|
||||
"name": "Hytale",
|
||||
"author": "hello@pterodactyl.io",
|
||||
"description": "Set out on an adventure built for both creation and play. Hytale blends the freedom of a sandbox with the momentum of an RPG: explore a procedurally generated world full of dungeons, secrets, and a variety of creatures, then shape it block by block.",
|
||||
"features": [
|
||||
"java_version"
|
||||
],
|
||||
"docker_images": {
|
||||
"ghcr.io/pterodactyl/games:hytale": "ghcr.io/pterodactyl/games:hytale"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": "java -XX:AOTCache=Server\/HytaleServer.aot -Xms128M $( ((SERVER_MEMORY)) && printf %s \"-Xmx${SERVER_MEMORY}M\" ) -jar Server\/HytaleServer.jar $( ((HYTALE_ALLOW_OP)) && printf %s \"--allow-op\" ) --assets Assets.zip --bind 0.0.0.0:${SERVER_PORT}",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\n \"done\": \"Hytale Server Booted\"\n}",
|
||||
"logs": "{}",
|
||||
"stop": "/stop"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/ash\nset -e\n\nmkdir -p \/mnt\/server\ncd \/mnt\/server\n\necho -e \"Downloading Hytale Downloader CLI...\"\n\nDOWNLOAD_URL=\"https:\/\/downloader.hytale.com\/hytale-downloader.zip\"\n\nrm -f hytale-downloader.zip\ncurl -o hytale-downloader.zip $DOWNLOAD_URL\nunzip -o hytale-downloader.zip -d hytale-downloader\nmv hytale-downloader\/hytale-downloader-linux-amd64 hytale-downloader\/hytale-downloader-linux\nchmod 555 hytale-downloader\/hytale-downloader-linux\n\necho -e \"Verifying Hytale Downloader installation...\"\n\necho -e \"Hytale Downloader version: `.\/hytale-downloader\/hytale-downloader-linux -version`\"\n\necho -e \"Hytale Downloader installed successfully.\"",
|
||||
"container": "ghcr.io/pterodactyl/installers:alpine",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Accept Early Plugins",
|
||||
"description": "Hytale warns that loading early plugins is unsupported and may cause stability issues.",
|
||||
"env_variable": "HYTALE_ACCEPT_EARLY_PLUGINS",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Auth Mode",
|
||||
"description": "Authentication mode.",
|
||||
"env_variable": "HYTALE_AUTH_MODE",
|
||||
"default_value": "authenticated",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|in:authenticated,offline",
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Patchline",
|
||||
"description": "The branch of Hytale to install.",
|
||||
"env_variable": "HYTALE_PATCHLINE",
|
||||
"default_value": "release",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|in:release,pre-release",
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Disable Sentry",
|
||||
"description": "Hytale uses Sentry to track crashes. Disable Sentry during active plugin development.",
|
||||
"env_variable": "DISABLE_SENTRY",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Use Ahead-of-Time Cache",
|
||||
"description": "Hytale provides a pre-trained AOT Java cache which can significantly improve boot times.",
|
||||
"env_variable": "USE_AOT_CACHE",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Allow operators",
|
||||
"description": "Allow operators",
|
||||
"env_variable": "HYTALE_ALLOW_OP",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
database/migrations/2025_12_07_022523_wings_or_elytra.php
Normal file
28
database/migrations/2025_12_07_022523_wings_or_elytra.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table("nodes", function (Blueprint $table) {
|
||||
$table->enum('daemonType', ['wings', 'elytra'])->default("elytra")->comment("What daemon Type this node uses");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table("nodes", function (Blueprint $table) {
|
||||
$table->dropColumn('daemonType');
|
||||
});
|
||||
}
|
||||
};
|
||||
28
database/migrations/2026_01_02_044710_backup_disk.php
Normal file
28
database/migrations/2026_01_02_044710_backup_disk.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// omg, first migration of 2026
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table("nodes", function (Blueprint $table) {
|
||||
$table->string('backupDisk')->default("rustic_local")->comment("What Backup type this Node uses");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table("nodes", function (Blueprint $table) {
|
||||
$table->dropColumn('backupDisk');
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user