Payload: system-wide virtual controllers through Sony's VDA API

A GoldHEN ELF payload that creates virtual DualShock 4 controllers with
scePadVirtualDevice*, the path Remote Play uses, so they work on the home
screen, at sign-in and in games. A phone or PC drives them from a page the
payload serves on port 4264.

- Stage 0 (default) is the browser controller: four slots, explicit
  creation, native PS4 user selection, input over a WebSocket.
- Stages 1-8 are the diagnostics that established the VDA call order.
- Loopback-only /api/status and /api/stop for the launcher app.
- The klog connection is reopened when a controller is created, so a
  start through GoldHEN AutoRun still works if it beats the klog server.

The VDA code is ported from seregonwar/SplashDown (GPL-3.0).
This commit is contained in:
MoHadiShibli committed 2026-10-04 19:53:43 +03:00
commit 6d931a05ce
21 files changed
+6840

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# The builds run in Linux containers on a Windows checkout: keep LF everywhere,
# whatever core.autocrlf says, or make and the shell scripts break.
* text=auto eol=lf
*.ttf binary
*.png binary
+5
View File
@@ -0,0 +1,5 @@
build/
*.elf
*.o
*.d
__pycache__/
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 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 General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is 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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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 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. Use with the GNU Affero General Public License.
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 Affero 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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 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 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 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 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 General Public License for more details.
You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+71
View File
@@ -0,0 +1,71 @@
# Control4Free -- system-wide virtual controllers for the PS4 (GoldHEN payload).
#
# Build:
# make # browser controller: control4free.elf
# make C4F_STAGE=1 # diagnostic probe
# make C4F_STAGE=4 # create a virtual pad and press PS
# make C4F_STAGE=4 C4F_VDA_USER=1 # same, created for user 1 (-> ...-stage4-u1.elf)
# make clean
#
# A build needs the ps4-payload-sdk; dev/scripts/build.ps1 runs this inside Docker.
ifdef PS4_PAYLOAD_SDK
include $(PS4_PAYLOAD_SDK)/toolchain/orbis.mk
else
$(error PS4_PAYLOAD_SDK is undefined -- use dev/scripts/build.ps1, or point it at an SDK)
endif
# Spike stage (see dev/notes/vda.md). Each stage does everything the lower ones do:
# 1 probe only (no scePad call) 2 pad service 3 add a virtual device
# 4 press PS on it 5 raise the selection screen and confirm with Cross
# 6 map the button bits by reading injected samples back
# 7 visual direction test on the selection screen (fallback for 6)
# 8 resident command server on port 4264 (dev/scripts/console.ps1)
C4F_STAGE ?= 0
# Optional: the userId the virtual device is created for. Unset = foreground
# user mapped to a local-user id (0x10000000). Each value gets its own ELF.
C4F_VDA_USER ?=
NAME := control4free
TAG := $(if $(filter 0,$(C4F_STAGE)),web,stage$(C4F_STAGE)$(if $(C4F_VDA_USER),-u$(C4F_VDA_USER)))
ELF := build/$(NAME)$(if $(filter 0,$(C4F_STAGE)),,-$(TAG)).elf
OBJDIR := build/$(TAG)
SOURCES := src/main.c src/log.c src/vda.c src/server.c
OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o)
ifeq ($(C4F_STAGE),0)
SOURCES += src/web.c src/net.c
OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o) $(OBJDIR)/client.o
endif
CFLAGS += -std=gnu11 -Wall -Wextra -Wpointer-arith -g -O2
CFLAGS += -MMD -MP
CFLAGS += -Iinclude -Ivendor/jsmn -DC4F_STAGE=$(C4F_STAGE)
CFLAGS += $(if $(C4F_VDA_USER),-DC4F_VDA_USER=$(C4F_VDA_USER))
LDFLAGS += -lScePad -lSceUserService -ldl -lpthread
.PHONY: all clean
.DEFAULT_GOAL := all
-include $(OBJECTS:.o=.d)
all: $(ELF)
$(OBJDIR) build:
mkdir -p $@
$(OBJDIR)/%.o: src/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
build/client.c: client/index.html tools/embed_client.py | build
python3 tools/embed_client.py $< $@
$(OBJDIR)/client.o: build/client.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
$(ELF): $(OBJECTS) | build
$(CC) -o $@ $(OBJECTS) $(LDFLAGS)
@echo "built $@"
clean:
rm -rf build
+2665
View File
File diff suppressed because it is too large. Load diff
+36
View File
@@ -0,0 +1,36 @@
# Build image for Control4Free payloads.
#
# Holds clang/lld plus a pinned ps4-payload-sdk release, so a build needs nothing
# installed on the host but Docker. Matches the SDK's own CI (clang-18 / lld-18).
FROM ubuntu:24.04
ARG SDK_VERSION=v0.9
# llvm-18 is here for llvm-config-18: the SDK's orbis-ld asks it where ld.lld
# lives, and without it the link fails on an empty path.
RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates clang-18 lld-18 llvm-18 curl unzip make python3 xxd \
&& rm -rf /var/lib/apt/lists/*
# The SDK's orbis-* wrappers look for llvm-config-<ver> and exec bin/clang.
RUN ln -sf /usr/bin/clang-18 /usr/bin/clang \
&& ln -sf /usr/bin/clang++-18 /usr/bin/clang++ \
&& ln -sf /usr/bin/ld.lld-18 /usr/bin/ld.lld \
&& ln -sf /usr/bin/llvm-config-18 /usr/bin/llvm-config \
&& test -x /usr/bin/llvm-config-18 \
&& test -x "$(llvm-config-18 --bindir)/ld.lld"
RUN curl -fsSL -o /tmp/sdk.zip \
"https://github.com/ps4-payload-dev/sdk/releases/download/${SDK_VERSION}/ps4-payload-sdk.zip" \
&& unzip -q /tmp/sdk.zip -d /opt \
&& rm /tmp/sdk.zip \
&& chmod -R +x /opt/ps4-payload-sdk/bin
# orbis-clang execs $SCRIPT_DIR/clang, and orbis-ld execs $SCRIPT_DIR/ld.lld.
RUN ln -sf /usr/bin/clang-18 /opt/ps4-payload-sdk/bin/clang \
&& ln -sf /usr/bin/clang++-18 /opt/ps4-payload-sdk/bin/clang++ \
&& ln -sf /usr/bin/ld.lld-18 /opt/ps4-payload-sdk/bin/ld.lld
ENV PS4_PAYLOAD_SDK=/opt/ps4-payload-sdk
ENV LLVM_CONFIG=/usr/bin/llvm-config-18
WORKDIR /src
+28
View File
@@ -0,0 +1,28 @@
/* Control4Free -- logging.
*
* Everything goes to klog (GoldHEN's klog server, port 3232) and is mirrored to a
* file on the console, so a run can be read back over FTP even if the klog
* connection dropped. Every line is prefixed [c4f].
*
* c4fLogSetKlog(0) turns the klog half off. The /dev/klog scanner needs that:
* while it is reading, its own writes would come back round and bury the kernel
* lines it is looking for.
*/
#ifndef C4F_LOG_H
#define C4F_LOG_H
#define C4F_LOG_DIR "/data/control4free"
#define C4F_LOG_PATH C4F_LOG_DIR "/spike.log"
void c4fLogOpen(void);
void c4fLogClose(void);
void c4fLogSetKlog(int enabled);
int c4fLogKlogEnabled(void);
void c4fLog(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
/* On-screen toast. Keep these few: each one interrupts the user. */
void c4fNotify(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
#endif /* C4F_LOG_H */
+40
View File
@@ -0,0 +1,40 @@
#ifndef C4F_NET_H
#define C4F_NET_H
#include <stddef.h>
#include <stdint.h>
#define C4F_NET_CLIENTS 8
#define C4F_NET_MESSAGE 4096
typedef struct C4fNetClient {
int fd, websocket, closing, fragmented, loopback;
uint64_t openedMs;
unsigned char rx[8192], tx[8192];
size_t rxUsed, txUsed, txSent, messageUsed;
char message[C4F_NET_MESSAGE + 1];
const unsigned char *body;
size_t bodySize, bodySent;
void *user;
} C4fNetClient;
enum { C4F_NET_OPEN, C4F_NET_MESSAGE_EVENT, C4F_NET_CLOSE,
C4F_NET_HTTP_STATUS, C4F_NET_HTTP_STOP };
typedef void (*C4fNetHandler)(C4fNetClient *, int, const char *, size_t, void *);
typedef struct {
int fd;
C4fNetClient clients[C4F_NET_CLIENTS];
C4fNetHandler handler;
void *context;
} C4fNet;
uint64_t c4fTimeMs(void);
int c4fNetOpen(C4fNet *, int port, C4fNetHandler, void *);
void c4fNetPoll(C4fNet *, int timeoutMs);
void c4fNetClose(C4fNet *);
int c4fNetText(C4fNetClient *, const char *);
void c4fNetHttpJson(C4fNetClient *, const char *);
void c4fWebSocketAccept(const char *key, char out[29]);
extern const unsigned char c4fPage[];
extern const size_t c4fPageSize;
#endif
+127
View File
@@ -0,0 +1,127 @@
/* Control4Free -- declarations for the system libraries we drive.
*
* The virtual-device API (VDA) is not in any public SDK header, so the pieces we
* need are declared here. Everything was taken from ps4libdoc, the OpenOrbis
* stubs and seregonwar/SplashDown's psbutton.c, which is the only known working
* caller on a PS4.
*/
#ifndef C4F_SCE_H
#define C4F_SCE_H
#include <stddef.h>
#include <stdint.h>
/* ---- libSceUserService ---- */
int32_t sceUserServiceInitialize(void *params);
int32_t sceUserServiceTerminate(void);
int32_t sceUserServiceGetInitialUser(int32_t *outUserId);
int32_t sceUserServiceGetForegroundUser(int32_t *outUserId) __attribute__((weak));
#define C4F_USER_ID_INVALID (-1)
/* ---- libScePad ---- */
int32_t scePadInit(void);
int32_t scePadGetHandle(int32_t userId, int32_t type, int32_t index);
int32_t scePadOpen(int32_t userId, int32_t type, int32_t index, void *param);
int32_t scePadClose(int32_t handle);
int32_t scePadReadState(int32_t handle, void *data);
/* Lets an unsigned process talk to the pad service at all. SplashDown calls this
* with 1 before scePadInit; without it the VDA calls are refused. */
int32_t scePadSetProcessPrivilege(int32_t privilege);
/* The virtual-device API. AddDevice returns a status, NOT a usable handle: the
* handle is the MBus DeviceId the kernel logs right after the call. */
int32_t scePadVirtualDeviceAddDevice(void *param, int32_t deviceType);
int32_t scePadVirtualDeviceDeleteDevice(int32_t handle);
int32_t scePadVirtualDeviceInsertData(int32_t handle, const void *padData);
int32_t scePadVirtualDeviceGetRemoteSetting(int32_t handle, void *setting) __attribute__((weak));
/* Device type 3 is what SplashDown uses on both PS4 and PS5. On a PS4 it shows up
* in klog as REMOTEPLAY, type 4, subType 2 -- the path Remote Play itself uses. */
#define C4F_VIRTUAL_DEVICE_TYPE 3
/* AddDevice's parameter block. size must be sizeof the struct (32). The six
* trailing words are unknown; we fill them with a marker so the log shows
* whether the call writes anything back into them. */
typedef struct {
int32_t size;
int32_t userId;
int32_t pad[6];
} C4fVdaParam;
/* userId values outside the real-user range are rejected, so AddDevice gets a
* value in 0x1000000x. 0x10000000 is the first local user on a PS4. */
#define C4F_VDA_USER_FALLBACK 0x10000000
/* ---- libSceMbus (no SDK stub exists: resolved with dlopen/dlsym) ---- */
typedef int32_t (*C4fMbusBindFn)(uint64_t deviceId, int32_t userId);
typedef int32_t (*C4fMbusDisconnectFn)(uint64_t deviceId);
#define C4F_LIBSCEMBUS_PATH "/system/common/lib/libSceMbus.sprx"
/* ---- libkernel ---- */
int sceKernelSendNotificationRequest(int unk0, void *req, size_t size, int unk1);
/* ---- Pad sample ---- */
typedef struct {
uint16_t x;
uint16_t y;
uint8_t finger;
uint8_t pad[3];
} ScePadTouch;
typedef struct {
uint8_t fingers;
uint8_t pad1[3];
uint32_t pad2;
ScePadTouch touch[2];
} ScePadTouchData;
/* 120 bytes, same layout the normal scePadRead path uses. */
typedef struct {
uint32_t buttons;
uint8_t lx, ly;
uint8_t rx, ry;
uint8_t l2, r2;
uint16_t padding;
float quat[4];
float vel[3];
float accel[3];
ScePadTouchData touchData;
uint8_t connected;
uint8_t align[3];
uint64_t timestamp;
uint8_t ext[16];
uint8_t count;
uint8_t unknown[15];
} ScePadData;
/* VDA bits verified on PS4 firmware 10.01. With AddDevice user=1, the native
* selection screen opens automatically: RIGHT=0x20 selects the next user and
* CROSS=0x4000 signs them in. Pressing PS during selection cancels that flow.
* The previously assumed Cross=0x20000 was not correct for this console. */
#define C4F_VDI_BUTTON_PS 0x00010000u /* confirmed on our console: moved the home screen */
#define C4F_VDI_BUTTON_CROSS 0x00004000u /* confirmed: native second-user sign-in */
/* Standard read-side bits. RIGHT and CROSS also verified on the VDA input
* path; the remaining inputs still need hardware checks. */
#define C4F_PAD_L3 0x00000002u
#define C4F_PAD_R3 0x00000004u
#define C4F_PAD_OPTIONS 0x00000008u
#define C4F_PAD_UP 0x00000010u
#define C4F_PAD_RIGHT 0x00000020u
#define C4F_PAD_DOWN 0x00000040u
#define C4F_PAD_LEFT 0x00000080u
#define C4F_PAD_L2 0x00000100u
#define C4F_PAD_R2 0x00000200u
#define C4F_PAD_L1 0x00000400u
#define C4F_PAD_R1 0x00000800u
#define C4F_PAD_TRIANGLE 0x00001000u
#define C4F_PAD_CIRCLE 0x00002000u
#define C4F_PAD_CROSS 0x00004000u
#define C4F_PAD_SQUARE 0x00008000u
#define C4F_PAD_TOUCHPAD 0x00100000u
#endif /* C4F_SCE_H */
+16
View File
@@ -0,0 +1,16 @@
/* Control4Free -- the resident command server (see src/server.c). */
#ifndef C4F_SERVER_H
#define C4F_SERVER_H
#include <stdint.h>
/* Its own port, so experiments no longer go through GoldHEN's PayLoader. */
#define C4F_SERVER_PORT 4264
/* Listens, runs commands, and returns when told to quit or when idle too long.
* `klogFd` is the kernel-log reader new pads are found through. Any pad still
* alive is removed before it returns. */
int c4fServerRun(int32_t realUserId, int klogFd);
#endif /* C4F_SERVER_H */
+110
View File
@@ -0,0 +1,110 @@
/* Control4Free -- the virtual device (VDA) lifecycle.
*
* One C4fVirtualPad is one virtual DualShock 4 as the system sees it. The order
* the calls have to go in, and why, is in dev/notes/vda.md.
*/
#ifndef C4F_VDA_H
#define C4F_VDA_H
#include <stdint.h>
#include "c4f_sce.h"
typedef struct {
int32_t handle; /* what InsertData takes: low 32 bits of deviceId */
uint64_t deviceId; /* the MBus DeviceId, read out of klog */
int32_t userId; /* the user it is bound to, or C4F_USER_ID_INVALID */
int32_t vdaUserId; /* the userId AddDevice was given */
int owned; /* we created it, so we delete it */
int bound; /* sceMbusBindDeviceWithUserId succeeded */
} C4fVirtualPad;
/* Resolves libSceMbus. Must run before any scePad* call: libScePad imports from
* libSceMbus and those imports are unresolved in a payload, so calling into
* libScePad first can kill the process with PRX_NOT_RESOLVED_FUNCTION.
* Returns 0 on success. */
int c4fMbusInit(void);
/* Starts the MBus client this process never started. which: 0 calls libScePad's
* scePadMbusInit(), 1 calls sceMbusInit(). Both take no arguments as far as we
* know (no published signature). Returns 0 if the function was found and
* called; its own return value goes in *outRet. */
int c4fMbusClientInit(int which, int32_t *outRet);
/* scePadInit(), then scePadSetProcessPrivilege(1) (it fails with
* NOT_INITIALIZED the other way round). Returns 0 if scePadInit succeeded. */
int c4fPadInit(void);
/* Picks the user to give the virtual pad: the foreground user, else the initial
* user, else C4F_VDA_USER_FALLBACK. */
int32_t c4fPickUserId(void);
/* AddDevice, then capture the DeviceId from /dev/klog. klogFd may be -1, in which
* case only the write-back words of the parameter block are inspected.
* Returns 0 and fills *out on success. */
int c4fVirtualPadAdd(C4fVirtualPad *out, int32_t userId, int klogFd);
/* Same, but AddDevice gets `vdaUser` exactly as given (no mapping). */
int c4fVirtualPadAddAs(C4fVirtualPad *out, int32_t userId, int32_t vdaUser, int klogFd);
/* Keep existing pads reporting during the bounded klog waits for a new pad.
* The callback must not read klog, create devices, or run the network loop. */
void c4fVdaSetWaitCallback(void (*callback)(void *), void *context);
/* sceMbusBindDeviceWithUserId. Until this runs the device exists but belongs to
* nobody, so input goes nowhere. Returns 0 on success. */
int c4fVirtualPadBind(C4fVirtualPad *pad, int32_t userId);
/* The bind with `authid` switched on for just the call (0 = leave it as is).
* Ghostcontrol uses SceShellCore's 0x4800000000000010 for this on PS4. */
int c4fVirtualPadBindAs(C4fVirtualPad *pad, int32_t userId, uint64_t authid);
#define C4F_AUTHID_SHELLCORE 0x4800000000000010ull
/* A neutral sample: sticks centred, identity quaternion, connected. */
void c4fPadDataNeutral(ScePadData *data);
/* One InsertData call. Returns the raw return value. */
int32_t c4fVirtualPadInsert(const C4fVirtualPad *pad, const ScePadData *data);
/* Holds `buttons` for durationMs, one sample every C4F_FRAME_MS. A real pad
* reports continuously, so a single sample is often missed. */
int32_t c4fVirtualPadHold(const C4fVirtualPad *pad, uint32_t buttons,
int durationMs, const char *label);
/* DeleteDevice, if we own the handle. */
void c4fVirtualPadRemove(C4fVirtualPad *pad);
/* Asks the pad service whether the device has been assigned to a user yet.
* An unassigned virtual pad answers nothing useful; once the system's user
* selection has run, scePadGetHandle starts returning a handle for the user it
* was given to. Logs every probe. Returns that user id, or
* C4F_USER_ID_INVALID while it is still unassigned.
* `realUserId` is the signed-in account to include among the candidates. */
int32_t c4fProbeAssignment(int32_t realUserId);
/* Opens a read handle on the pad for `userId`, so injected samples can be read
* back. Returns the handle, or negative. Close it with scePadClose. */
int32_t c4fOpenReadHandle(int32_t userId);
/* Injects one candidate bit at a time and reads back what the system reports,
* which gives the input-bit -> button map without anyone watching the screen.
* `readHandle` must come from c4fOpenReadHandle. Logs a line per bit. */
void c4fProbeButtonMap(const C4fVirtualPad *pad, int32_t readHandle);
#define C4F_FRAME_MS 33
/* ---- klog capture ---- */
/* GoldHEN's klog server port, read from inside the console when /dev/klog is
* already held by that server. */
#define C4F_KLOG_PORT 3232
/* /dev/klog if free, else a socket to the local klog server. -1 if neither. */
int c4fKlogOpen(void);
void c4fKlogDrain(int fd);
/* Writes a marker to klog and reads it back. 0 = the capture path works. */
int c4fKlogSelfTest(int fd);
/* Scans for the MBus "device added" line and returns its DeviceId. */
int c4fKlogFindDeviceId(int fd, uint64_t *outDeviceId, int timeoutMs);
#endif /* C4F_VDA_H */
+7
View File
@@ -0,0 +1,7 @@
#ifndef C4F_WEB_H
#define C4F_WEB_H
#define C4F_WEB_PORT 4264
#define C4F_MAX_PADS 4
/* Takes ownership of klogFd (may be -1): reconnects it as needed and closes it. */
int c4fWebRun(int klogFd);
#endif
+68
View File
@@ -0,0 +1,68 @@
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ps4/klog.h>
#include "c4f_log.h"
#include "c4f_sce.h"
static int g_logFd = -1;
static int g_klogEnabled = 1;
void c4fLogSetKlog(int enabled) { g_klogEnabled = enabled; }
int c4fLogKlogEnabled(void) { return g_klogEnabled; }
void c4fLogOpen(void)
{
(void)mkdir("/data", 0777);
(void)mkdir(C4F_LOG_DIR, 0777);
g_logFd = open(C4F_LOG_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0666);
c4fLog("---- Control4Free spike log (stage %d) ----\n", C4F_STAGE);
}
void c4fLogClose(void)
{
if (g_logFd < 0) return;
c4fLog("---- end ----\n");
(void)fsync(g_logFd);
(void)close(g_logFd);
g_logFd = -1;
}
void c4fLog(const char *fmt, ...)
{
char msg[1024];
va_list ap;
int n;
va_start(ap, fmt);
n = vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
if (n < 0) return;
if (n >= (int)sizeof(msg)) n = (int)sizeof(msg) - 1;
if (g_klogEnabled) klog_printf("[c4f] %s", msg);
if (g_logFd >= 0) (void)write(g_logFd, msg, (size_t)n);
}
/* The notification request is mostly opaque; only the message matters. */
typedef struct {
char unused[45];
char message[3075];
} C4fNotifyRequest;
void c4fNotify(const char *fmt, ...)
{
C4fNotifyRequest req;
va_list ap;
(void)memset(&req, 0, sizeof(req));
va_start(ap, fmt);
(void)vsnprintf(req.message, sizeof(req.message), fmt, ap);
va_end(ap);
(void)sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
}
+416
View File
@@ -0,0 +1,416 @@
/* Control4Free -- system-wide virtual controllers for the PS4.
*
* The browser payload builds on a successful hardware experiment: a GoldHEN
* payload can create a virtual controller that the PS4 menus accept.
* A game plugin cannot (GoldHEN only loads plugins into game processes, and the
* menus live in SceShellUI/SceShellCore), so Control4Free is a payload and uses
* Sony's own virtual-device API, the one Remote Play goes through.
*
* The work is split into stages so each call can be checked on klog before the
* next one is let near the console. Pick one at build time:
*
* C4F_STAGE=0 browser controller (default), with explicit creation/sign-in.
* C4F_STAGE=1 probe only: firmware, credentials, users. No scePad call.
* C4F_STAGE=2 plus libSceMbus, scePadSetProcessPrivilege and scePadInit.
* C4F_STAGE=3 plus AddDevice. Holds the device a few seconds so the
* "Who's using this controller?" screen can be watched for,
* then deletes it. No input is injected.
* C4F_STAGE=4 plus one short PS press, then 10 s of neutral input so a user
* selection prompt can show. No MBus bind (it crashed; C4F_BIND=1
* puts it back).
* C4F_STAGE=5 the sign-in attempt: raise the selection screen, press Cross to
* confirm the highlighted user, then check whether the pad was
* really assigned. Build it with C4F_VDA_USER=1.
* C4F_STAGE=6 button mapping: inject one bit at a time and read the pad back,
* so the map comes from the system, not from guessing. Build with
* the default user so the pad is assigned and readable.
* C4F_STAGE=7 visual direction test on the selection screen.
* C4F_STAGE=8 command server: stays resident on port 4264 and takes commands
* from dev\scripts\console.ps1, so the PayLoader is needed once.
*
* Diagnostic stages require an explicit build setting.
*/
#include <stdint.h>
#include <unistd.h>
#include <ps4/kernel.h>
#include "c4f_log.h"
#include "c4f_sce.h"
#include "c4f_vda.h"
#include "c4f_server.h"
#include "c4f_web.h"
#ifndef C4F_STAGE
#define C4F_STAGE 0
#endif
/* The authid SplashDown raises to before the VDA calls. */
#define C4F_AUTHID_VDA 0x3800000000010003L
/* Stage 3: how long the virtual device is left in place to be observed. */
#ifndef C4F_STAGE3_DWELL_MS
#define C4F_STAGE3_DWELL_MS 8000
#endif
/* Stage 4 press shape. PS is one of only two bits confirmed on this path. A
* short press: holding PS for over a second opens the quick menu instead. */
#ifndef C4F_PRESS_BUTTONS
#define C4F_PRESS_BUTTONS C4F_VDI_BUTTON_PS
#endif
#ifndef C4F_PRESS_LABEL
#define C4F_PRESS_LABEL "PS"
#endif
#ifndef C4F_PRE_NEUTRAL_MS
#define C4F_PRE_NEUTRAL_MS 300
#endif
#ifndef C4F_PRESS_MS
#define C4F_PRESS_MS 200
#endif
/* Neutral samples keep flowing after the press, like a real pad that is still
* switched on, so a "Who's using this controller?" prompt has time to appear
* before the device is removed (removing it cancels the prompt). */
#ifndef C4F_POST_NEUTRAL_MS
#define C4F_POST_NEUTRAL_MS 10000
#endif
/* Stage 5 timings. The screen takes a moment to come up after device creation, and
* the pad has to keep reporting the whole time or the system drops it. */
#ifndef C4F_S5_SCREEN_WAIT_MS
#define C4F_S5_SCREEN_WAIT_MS 4000
#endif
#ifndef C4F_S5_CROSS_MS
#define C4F_S5_CROSS_MS 120
#endif
#ifndef C4F_S5_SETTLE_MS
#define C4F_S5_SETTLE_MS 3000
#endif
/* How long the pad is kept alive after the attempt, so the result stays on
* screen and a second probe can run. */
#ifndef C4F_S5_ALIVE_MS
#define C4F_S5_ALIVE_MS 15000
#endif
/* sceMbusBindDeviceWithUserId crashed the host process on 2026-10-04 (stage 4,
* ScePartyDaemon, reason 0xb). It is also not what we want: the goal is the
* system's own user selection. Off unless asked for. */
#ifndef C4F_BIND
#define C4F_BIND 0
#endif
/* Reports what privileges this process actually has. On a GoldHEN console the
* payload may already be jailbroken, so the before/after pair tells us whether
* raising them by hand is needed at all. */
static void c4fLogCredentials(const char *when, pid_t pid)
{
uint8_t caps[16];
uint64_t authid = kernel_get_ucred_authid(pid);
int i;
char hex[33];
if (kernel_get_ucred_caps(pid, caps) == 0) {
for (i = 0; i < 16; i++) {
static const char digits[] = "0123456789abcdef";
hex[i * 2] = digits[(caps[i] >> 4) & 0xf];
hex[i * 2 + 1] = digits[caps[i] & 0xf];
}
hex[32] = '\0';
} else {
(void)__builtin_memcpy(hex, "<unreadable>", 13);
}
c4fLog("credentials %s: authid=0x%016llx caps=%s\n",
when, (unsigned long long)authid, hex);
}
/* GoldHEN's PayLoader does not give us a process of our own: it runs the ELF
* inside an existing system process (ScePartyDaemon on our console). So the
* credentials we raise belong to that daemon, and they have to be put back
* before we leave or the daemon carries on with our identity. */
static pid_t g_pid;
static uint64_t g_savedAuthid;
static uint8_t g_savedCaps[16];
static int g_credsSaved;
static int c4fRaiseCredentials(pid_t pid)
{
uint8_t caps[16];
int i;
g_pid = pid;
g_savedAuthid = kernel_get_ucred_authid(pid);
g_credsSaved = kernel_get_ucred_caps(pid, g_savedCaps) == 0;
if (!g_credsSaved || !g_savedAuthid) {
g_credsSaved = 0;
c4fLog("could not save host credentials; refusing to modify them\n");
return -1;
}
for (i = 0; i < 16; i++) caps[i] = 0xff;
if (kernel_set_ucred_authid(pid, C4F_AUTHID_VDA) != 0) {
c4fLog("kernel_set_ucred_authid failed\n");
return -1;
}
if (kernel_set_ucred_caps(pid, caps) != 0) {
c4fLog("kernel_set_ucred_caps failed\n");
return -1;
}
return 0;
}
static void c4fRestoreCredentials(void)
{
if (!g_credsSaved) {
c4fLog("credentials were not saved; leaving them as they are\n");
return;
}
if (kernel_set_ucred_authid(g_pid, g_savedAuthid) != 0)
c4fLog("restoring authid failed\n");
if (kernel_set_ucred_caps(g_pid, g_savedCaps) != 0)
c4fLog("restoring caps failed\n");
c4fLogCredentials("restored", g_pid);
}
static int c4fFinish(int klogFd, int status)
{
if (klogFd >= 0) close(klogFd);
c4fRestoreCredentials();
c4fLog("exiting with status %d\n", status);
c4fLogClose();
return status;
}
int main(void)
{
pid_t pid = getpid();
uint32_t fw;
int32_t userId;
int klogFd = -1;
int32_t ret;
#if C4F_STAGE >= 3 && C4F_STAGE <= 7
C4fVirtualPad pad;
#endif
c4fLogOpen();
#if C4F_STAGE != 0
c4fNotify("Control4Free: stage %d", C4F_STAGE);
#endif
fw = kernel_get_fw_version();
c4fLog("pid=%d firmware=0x%08x (10.01 reads as 0x1001xxxx)\n", pid, fw);
c4fLogCredentials("on entry", pid);
if (c4fRaiseCredentials(pid) != 0) return c4fFinish(klogFd, 1);
c4fLogCredentials("after raise", pid);
/* Opened before anything creates a device, so the add shows up in the scan. */
klogFd = c4fKlogOpen();
c4fKlogDrain(klogFd);
/* Stage 3 depends on reading kernel lines back, so prove that works first. */
c4fKlogSelfTest(klogFd);
ret = sceUserServiceInitialize(NULL);
c4fLog("sceUserServiceInitialize = 0x%08x\n", (uint32_t)ret);
userId = c4fPickUserId();
c4fLog("chosen userId=0x%08x\n", (uint32_t)userId);
#if C4F_STAGE == 1
c4fLog("stage 1 done: nothing in libScePad was called\n");
c4fNotify("Control4Free: probe done");
#else
/* libScePad has unresolved imports into libSceMbus in a payload, so MBus has
* to be loaded first or the first scePad call can kill the process. */
if (c4fMbusInit() != 0) {
c4fNotify("Control4Free: libSceMbus missing");
return c4fFinish(klogFd, 1);
}
if (c4fPadInit() != 0) {
c4fNotify("Control4Free: scePadInit failed");
return c4fFinish(klogFd, 1);
}
c4fLog("pad service ready\n");
#endif
#if C4F_STAGE == 8
/* Resident command server: sent once, then driven over its own port. */
if (c4fServerRun(userId, klogFd) != 0)
return c4fFinish(klogFd, 1);
#endif
#if C4F_STAGE == 0
/* c4fWebRun owns the klog descriptor from here on. */
ret = c4fWebRun(klogFd);
klogFd = -1;
if (ret != 0) {
c4fNotify("Control4Free: could not start; check payload log");
return c4fFinish(klogFd, 1);
}
#endif
#if C4F_STAGE >= 3 && C4F_STAGE <= 7
if (c4fVirtualPadAdd(&pad, userId, klogFd) != 0) {
c4fNotify("Control4Free: no virtual device");
return c4fFinish(klogFd, 1);
}
/* The toast names the user value, so whoever watches the TV can tell runs apart. */
c4fNotify("Control4Free: device 0x%x, user 0x%x",
(uint32_t)pad.handle, (uint32_t)pad.vdaUserId);
#endif
#if C4F_STAGE == 3
c4fLog("holding the device for %d ms -- watch the screen for a controller "
"assignment prompt\n", C4F_STAGE3_DWELL_MS);
usleep(C4F_STAGE3_DWELL_MS * 1000);
c4fVirtualPadRemove(&pad);
c4fNotify("Control4Free: device removed");
#endif
#if C4F_STAGE == 7
/* Visual fallback for the button map, for when reading the pad back does not
* work. It tests one hypothesis: that the VDA path uses the ordinary
* documented layout (PS = 0x10000 fits it, which is why it is likely).
* Build with C4F_VDA_USER=1 so the selection screen is what receives the
* presses -- a d-pad there only moves a highlight.
* Watch the screen and report what moved; nothing is confirmed here. */
{
ScePadData neutral;
int press;
c4fPadDataNeutral(&neutral);
ret = c4fVirtualPadInsert(&pad, &neutral);
c4fLog("first neutral InsertData = 0x%08x\n", (uint32_t)ret);
c4fVirtualPadHold(&pad, 0, 500, "pre-neutral");
/* AddDevice(user=1) opens selection itself. PS would cancel it. */
c4fNotify("C4F: waiting for user selection");
c4fVirtualPadHold(&pad, 0, 4000, "waiting for the screen");
c4fNotify("C4F: 3x DOWN now");
for (press = 0; press < 3; press++) {
c4fVirtualPadHold(&pad, C4F_PAD_DOWN, 120, "DOWN 0x40");
c4fVirtualPadHold(&pad, 0, 900, "gap");
}
c4fVirtualPadHold(&pad, 0, 1500, "pause");
c4fNotify("C4F: 3x UP now");
for (press = 0; press < 3; press++) {
c4fVirtualPadHold(&pad, C4F_PAD_UP, 120, "UP 0x10");
c4fVirtualPadHold(&pad, 0, 900, "gap");
}
c4fVirtualPadHold(&pad, 0, 4000, "staying alive");
c4fVirtualPadRemove(&pad);
c4fNotify("C4F: direction test finished");
}
#endif
#if C4F_STAGE == 6
/* Button mapping, read back from the system instead of watched on screen.
* Build this one with the default user (0x10000000) so the pad is already
* assigned and can be read; an unassigned pad answers nothing. */
{
int32_t readHandle;
ScePadData neutral;
c4fPadDataNeutral(&neutral);
ret = c4fVirtualPadInsert(&pad, &neutral);
c4fLog("first neutral InsertData = 0x%08x\n", (uint32_t)ret);
/* The user the device was created for -- not the signed-in account,
* whose handle would be the real DualShock. */
readHandle = c4fOpenReadHandle(pad.vdaUserId);
if (readHandle < 0) {
c4fLog("no read handle: cannot map buttons this way\n");
c4fNotify("Control4Free: no read handle");
} else {
c4fNotify("Control4Free: mapping buttons");
c4fProbeButtonMap(&pad, readHandle);
(void)scePadClose(readHandle);
}
c4fVirtualPadRemove(&pad);
c4fNotify("Control4Free: probe finished");
}
#endif
#if C4F_STAGE == 5
c4fLog("no MBus bind: the system's own user selection is the point here\n");
{
ScePadData neutral;
int32_t assigned;
c4fPadDataNeutral(&neutral);
ret = c4fVirtualPadInsert(&pad, &neutral);
c4fLog("first neutral InsertData = 0x%08x\n", (uint32_t)ret);
if (ret < 0) {
c4fNotify("Control4Free: InsertData failed 0x%x", (uint32_t)ret);
c4fVirtualPadRemove(&pad);
return c4fFinish(klogFd, 1);
}
c4fLog("before anything: ");
(void)c4fProbeAssignment(userId);
/* AddDevice(user=1) opens selection itself. A PS press here cancels
* selection and disconnects the pad from the sign-in flow. */
c4fVirtualPadHold(&pad, 0, 500, "pre-neutral");
c4fNotify("Control4Free: waiting for user selection");
c4fVirtualPadHold(&pad, 0, C4F_S5_SCREEN_WAIT_MS, "waiting for the screen");
/* Cross confirms whoever is highlighted. RIGHT=0x20 is also verified
* on this path; stage 8 can use it to choose another user first. */
c4fVirtualPadHold(&pad, C4F_VDI_BUTTON_CROSS, C4F_S5_CROSS_MS, "Cross press");
c4fVirtualPadHold(&pad, 0, C4F_S5_SETTLE_MS, "settling");
assigned = c4fProbeAssignment(userId);
if (assigned != C4F_USER_ID_INVALID) {
/* GetHandle may be the physical controller; it cannot prove that
* this virtual DeviceId belongs to the returned user. */
c4fLog("read handle found for user 0x%08x; verify this virtual "
"DeviceId's OWNER_CHANGED event in klog\n", (uint32_t)assigned);
}
c4fNotify("Control4Free: confirmation sent; check system log");
/* Keep reporting so the result stays visible, then look once more. */
c4fVirtualPadHold(&pad, 0, C4F_S5_ALIVE_MS, "staying alive");
c4fLog("final check: ");
(void)c4fProbeAssignment(userId);
c4fVirtualPadRemove(&pad);
c4fNotify("Control4Free: done");
}
#endif
#if C4F_STAGE == 4
#if C4F_BIND
if (c4fVirtualPadBind(&pad, userId) != 0)
c4fLog("bind failed; injecting anyway to see what happens\n");
#else
c4fLog("no MBus bind: leaving user selection to the system\n");
#endif
{
ScePadData neutral;
c4fPadDataNeutral(&neutral);
ret = c4fVirtualPadInsert(&pad, &neutral);
c4fLog("first neutral InsertData = 0x%08x\n", (uint32_t)ret);
if (ret < 0) {
c4fNotify("Control4Free: InsertData failed 0x%x", (uint32_t)ret);
c4fVirtualPadRemove(&pad);
return c4fFinish(klogFd, 1);
}
}
c4fNotify("Control4Free: pressing %s", C4F_PRESS_LABEL);
c4fVirtualPadHold(&pad, 0, C4F_PRE_NEUTRAL_MS, "pre-neutral");
c4fVirtualPadHold(&pad, C4F_PRESS_BUTTONS, C4F_PRESS_MS, C4F_PRESS_LABEL " press");
c4fVirtualPadHold(&pad, 0, C4F_POST_NEUTRAL_MS, "post-neutral");
c4fVirtualPadRemove(&pad);
c4fNotify("Control4Free: done");
#endif
return c4fFinish(klogFd, 0);
}
+343
View File
@@ -0,0 +1,343 @@
/* Bounded HTTP/WebSocket transport for the embedded controller page. */
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include "c4f_net.h"
uint64_t c4fTimeMs(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000;
}
static uint32_t c4fRol(uint32_t v, unsigned n) { return (v << n) | (v >> (32 - n)); }
/* RFC 6455's SHA-1 + base64 handshake. Keys are fixed at 24 ASCII bytes; this
* implementation hashes only the key and protocol GUID, never arbitrary data. */
void c4fWebSocketAccept(const char *key, char out[29])
{
unsigned char msg[128] = {0}, digest[20];
uint32_t h[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0};
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
unsigned block, i, j = 0;
memcpy(msg, key, 24);
memcpy(msg + 24, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 36);
msg[60] = 0x80;
msg[126] = 1; msg[127] = 0xe0; /* 60 * 8 bits */
for (block = 0; block < 2; block++) {
uint32_t w[80], a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
for (i = 0; i < 16; i++) {
const unsigned char *p = msg + block * 64 + i * 4;
w[i] = (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
for (i = 16; i < 80; i++) w[i] = c4fRol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
for (i = 0; i < 80; i++) {
uint32_t f, k, t;
if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; }
else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }
else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }
else { f = b ^ c ^ d; k = 0xca62c1d6; }
t = c4fRol(a, 5) + f + e + k + w[i];
e = d; d = c; c = c4fRol(b, 30); b = a; a = t;
}
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
}
for (i = 0; i < 20; i++) digest[i] = (unsigned char)(h[i/4] >> (24 - 8*(i%4)));
for (i = 0; i < 20; i += 3) {
uint32_t v = (uint32_t)digest[i] << 16;
if (i+1 < 20) v |= (uint32_t)digest[i+1] << 8;
if (i+2 < 20) v |= digest[i+2];
out[j++] = b64[v >> 18]; out[j++] = b64[(v >> 12) & 63];
out[j++] = i+1 < 20 ? b64[(v >> 6) & 63] : '=';
out[j++] = i+2 < 20 ? b64[v & 63] : '=';
}
out[j] = 0;
}
static int c4fQueue(C4fNetClient *c, const void *data, size_t n)
{
if (c->closing || c->body) return -1;
if (c->txSent) {
memmove(c->tx, c->tx + c->txSent, c->txUsed - c->txSent);
c->txUsed -= c->txSent; c->txSent = 0;
}
if (n > sizeof(c->tx) - c->txUsed) { c->closing = 1; return -1; }
if (n) memcpy(c->tx + c->txUsed, data, n);
c->txUsed += n;
return 0;
}
static int c4fFrame(C4fNetClient *c, unsigned op, const void *data, size_t n)
{
unsigned char head[4] = { (unsigned char)(0x80 | op), (unsigned char)n, 0, 0 };
size_t h = 2;
if (n >= 126) { head[1] = 126; head[2] = n >> 8; head[3] = n; h = 4; }
if (c4fQueue(c, head, h) != 0) return -1;
return c4fQueue(c, data, n);
}
int c4fNetText(C4fNetClient *c, const char *text) { return c4fFrame(c, 1, text, strlen(text)); }
void c4fNetHttpJson(C4fNetClient *c, const char *json)
{
char header[256];
int n = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %zu\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", strlen(json));
if (c4fQueue(c, header, (size_t)n) || c4fQueue(c, json, strlen(json))) return;
c->closing = 2;
}
static void c4fDrop(C4fNet *net, C4fNetClient *c)
{
if (c->websocket) net->handler(c, C4F_NET_CLOSE, NULL, 0, net->context);
close(c->fd);
memset(c, 0, sizeof(*c)); c->fd = -1;
}
/* Header values are copied and duplicates are rejected. */
static int c4fHeader(const char *request, const char *key, char *value, size_t size)
{
const char *p = strstr(request, "\r\n");
size_t k = strlen(key);
int found = 0;
value[0] = 0;
while (p && p[2] && p[2] != '\r') {
const char *end, *start;
p += 2; end = strstr(p, "\r\n");
if (!end) return -1;
if ((size_t)(end - p) > k && !strncasecmp(p, key, k) && p[k] == ':') {
if (found++) return -1;
start = p + k + 1;
while (start < end && (*start == ' ' || *start == '\t')) start++;
while (end > start && (end[-1] == ' ' || end[-1] == '\t')) end--;
if ((size_t)(end-start) >= size) return -1;
memcpy(value, start, end-start); value[end-start] = 0;
}
p = strstr(p, "\r\n");
}
return found;
}
static void c4fHttpError(C4fNetClient *c, int code, const char *message)
{
char buf[512];
int n = snprintf(buf, sizeof(buf), "HTTP/1.1 %d Error\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n\r\n%s", code, strlen(message), message);
c4fQueue(c, buf, (size_t)n); c->closing = 2;
}
static void c4fHttp(C4fNet *net, C4fNetClient *c)
{
char request[8193], host[128], origin[160], key[40], upgrade[32], version[12], connection[128];
char *end;
size_t used;
memcpy(request, c->rx, c->rxUsed); request[c->rxUsed] = 0;
end = strstr(request, "\r\n\r\n");
if (!end) { if (c->rxUsed == sizeof(c->rx)) c4fHttpError(c, 431, "Headers too large"); return; }
used = (size_t)(end-request) + 4;
request[used] = 0;
int statusRequest = !strncmp(request, "GET /api/status HTTP/1.1\r\n", 26);
int stopRequest = !strncmp(request, "POST /api/stop HTTP/1.1\r\n", 25);
if (!statusRequest && !stopRequest && strncmp(request, "GET / HTTP/1.1\r\n", 16) &&
strncmp(request, "GET /index.html HTTP/1.1\r\n", 26) &&
strncmp(request, "GET /ws HTTP/1.1\r\n", 18) &&
strncmp(request, "GET /?", 6)) {
c4fHttpError(c, 404, "Not found"); return;
}
if (c4fHeader(request, "Host", host, sizeof(host)) != 1 ||
c4fHeader(request, "Origin", origin, sizeof(origin)) < 0 ||
c4fHeader(request, "Upgrade", upgrade, sizeof(upgrade)) < 0) {
c4fHttpError(c, 400, "Invalid headers"); return;
}
/* Serve only literal IPv4 addresses or localhost: block DNS-rebinding hosts. */
{
char address[128], *colon; struct in_addr parsed;
snprintf(address, sizeof(address), "%s", host);
colon = strchr(address, ':'); if (colon) *colon = 0;
if (strcmp(address, "localhost") && inet_pton(AF_INET, address, &parsed) != 1) {
c4fHttpError(c, 403, "Open the PS4 IP address"); return;
}
}
if (statusRequest || stopRequest) {
char launcher[8], length[16], transfer[32];
/* Management is local to the console. Reject browser-origin requests
* and require a custom header, so a website cannot trigger shutdown. */
if (!c->loopback || origin[0] || upgrade[0] ||
c4fHeader(request, "X-Control4Free-Launcher", launcher, sizeof(launcher)) != 1 || strcmp(launcher, "1") ||
c4fHeader(request, "Content-Length", length, sizeof(length)) < 0 || (length[0] && strcmp(length, "0")) ||
c4fHeader(request, "Transfer-Encoding", transfer, sizeof(transfer)) != 0) {
c4fHttpError(c, 403, "Local launcher only"); return;
}
c->rxUsed = 0;
net->handler(c, stopRequest ? C4F_NET_HTTP_STOP : C4F_NET_HTTP_STATUS, NULL, 0, net->context);
} else if (upgrade[0]) {
char accept[29], reply[256], expected[160];
int n;
snprintf(expected, sizeof(expected), "http://%s", host);
if (origin[0] && strcmp(origin, "null") && strcmp(origin, expected)) {
c4fHttpError(c, 403, "Origin not allowed"); return;
}
if (strcasecmp(upgrade, "websocket") ||
c4fHeader(request, "Sec-WebSocket-Key", key, sizeof(key)) != 1 ||
c4fHeader(request, "Sec-WebSocket-Version", version, sizeof(version)) != 1 ||
c4fHeader(request, "Connection", connection, sizeof(connection)) != 1 ||
strcmp(version, "13") || strlen(key) != 24 || strcmp(key+22, "==") ||
strspn(key, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") != 22) {
c4fHttpError(c, 400, "Invalid WebSocket handshake"); return;
}
for (size_t i = 0; connection[i]; i++) connection[i] = (char)tolower((unsigned char)connection[i]);
if (!strstr(connection, "upgrade")) { c4fHttpError(c, 400, "Upgrade required"); return; }
c4fWebSocketAccept(key, accept);
n = snprintf(reply, sizeof(reply), "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", accept);
c4fQueue(c, reply, (size_t)n); c->websocket = 1;
memmove(c->rx, c->rx+used, c->rxUsed-used); c->rxUsed -= used;
net->handler(c, C4F_NET_OPEN, NULL, 0, net->context);
} else {
char reply[768];
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; media-src data:; connect-src ws:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
c4fQueue(c, reply, (size_t)n);
c->body = c4fPage; c->bodySize = c4fPageSize; c->rxUsed = 0;
}
}
static void c4fWs(C4fNet *net, C4fNetClient *c)
{
while (c->rxUsed >= 2 && !c->closing) {
unsigned char *p = c->rx;
unsigned op = p[0] & 15, fin = p[0] >> 7;
size_t n = p[1] & 127, head = 2;
if ((p[0] & 0x70) || !(p[1] & 128)) { c->closing = 1; break; }
if (n == 126) {
if (c->rxUsed < 4) break;
n = (size_t)p[2] << 8 | p[3]; head = 4;
if (n < 126) { c->closing = 1; break; }
} else if (n == 127) { c->closing = 1; break; } /* messages are capped at 4 KiB */
if (n > C4F_NET_MESSAGE || (op >= 8 && (!fin || n > 125))) { c->closing = 1; break; }
if (c->rxUsed < head + 4 + n) break;
unsigned char *mask = p + head, *data = mask + 4;
for (size_t i = 0; i < n; i++) data[i] ^= mask[i % 4];
if (op == 8) {
c4fFrame(c, 8, NULL, 0); c->closing = 2;
} else if (op == 9) c4fFrame(c, 10, data, n);
else if (op == 10) { /* pong */ }
else if ((op == 1 && !c->fragmented) || (op == 0 && c->fragmented)) {
if (n > C4F_NET_MESSAGE - c->messageUsed) { c->closing = 1; break; }
memcpy(c->message+c->messageUsed, data, n); c->messageUsed += n;
c->fragmented = !fin;
if (fin) {
c->message[c->messageUsed] = 0;
net->handler(c, C4F_NET_MESSAGE_EVENT, c->message, c->messageUsed, net->context);
c->messageUsed = 0;
}
} else { c->closing = 1; break; }
size_t taken = head+4+n;
memmove(c->rx, c->rx+taken, c->rxUsed-taken); c->rxUsed -= taken;
}
}
int c4fNetOpen(C4fNet *net, int port, C4fNetHandler handler, void *context)
{
struct sockaddr_in addr;
int one = 1;
memset(net, 0, sizeof(*net)); net->fd = -1;
for (int i = 0; i < C4F_NET_CLIENTS; i++) net->clients[i].fd = -1;
net->handler = handler; net->context = context;
net->fd = socket(AF_INET, SOCK_STREAM, 0);
if (net->fd < 0) return -1;
setsockopt(net->fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
memset(&addr, 0, sizeof(addr));
#ifdef __FreeBSD__
addr.sin_len = sizeof(addr);
#endif
addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(port);
if (net->fd >= (int)FD_SETSIZE || fcntl(net->fd, F_SETFL, O_NONBLOCK) ||
bind(net->fd, (struct sockaddr *)&addr, sizeof(addr)) || listen(net->fd, 8)) {
close(net->fd); net->fd = -1; return -1;
}
return 0;
}
void c4fNetPoll(C4fNet *net, int timeoutMs)
{
fd_set rd, wr;
struct timeval timeout = { .tv_sec = 0, .tv_usec = timeoutMs * 1000 };
int maxFd = net->fd, ready;
FD_ZERO(&rd); FD_ZERO(&wr); FD_SET(net->fd, &rd);
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
C4fNetClient *c = &net->clients[i];
if (c->fd < 0) continue;
if (c->closing == 1) { c4fDrop(net, c); continue; }
if (!c->closing && !c->body) FD_SET(c->fd, &rd);
if (c->txSent < c->txUsed || c->body) FD_SET(c->fd, &wr);
if (c->fd > maxFd) maxFd = c->fd;
}
ready = select(maxFd+1, &rd, &wr, NULL, &timeout);
if (ready < 0) return;
if (FD_ISSET(net->fd, &rd)) {
struct sockaddr_in peer;
socklen_t peerSize = sizeof(peer);
int fd = accept(net->fd, (struct sockaddr *)&peer, &peerSize), index;
for (index = 0; index < C4F_NET_CLIENTS && net->clients[index].fd >= 0; index++) {}
if (fd >= 0) {
if (index == C4F_NET_CLIENTS || fd >= (int)FD_SETSIZE || fcntl(fd, F_SETFL, O_NONBLOCK)) close(fd);
else {
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
#ifdef SO_NOSIGPIPE
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
net->clients[index].fd = fd; net->clients[index].openedMs = c4fTimeMs();
net->clients[index].loopback = (ntohl(peer.sin_addr.s_addr) >> 24) == 127;
}
}
}
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
C4fNetClient *c = &net->clients[i];
if (c->fd < 0) continue;
if (FD_ISSET(c->fd, &rd)) {
ssize_t n = recv(c->fd, c->rx+c->rxUsed, sizeof(c->rx)-c->rxUsed, 0);
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) c->closing = 1;
else if (n > 0) {
c->rxUsed += (size_t)n;
if (!c->websocket) c4fHttp(net, c);
if (c->websocket) c4fWs(net, c);
}
}
if (c->closing != 1 && FD_ISSET(c->fd, &wr)) {
const unsigned char *data = c->tx+c->txSent;
size_t left = c->txUsed-c->txSent;
int body = left == 0, flags = 0;
if (body) { data = c->body+c->bodySent; left = c->bodySize-c->bodySent; }
if (left > 16384) left = 16384;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#endif
ssize_t n = send(c->fd, data, left, flags);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) c->closing = 1;
else if (n > 0) {
if (body) {
c->bodySent += (size_t)n;
if (c->bodySent == c->bodySize) { c->body = NULL; c->closing = 1; }
} else { c->txSent += (size_t)n; if (c->txSent == c->txUsed) c->txSent = c->txUsed = 0; }
}
}
if (!c->websocket && c4fTimeMs()-c->openedMs > 10000) c->closing = 1;
if (c->closing == 1 || (c->closing == 2 && c->txSent == c->txUsed)) c4fDrop(net, c);
}
}
void c4fNetClose(C4fNet *net)
{
for (int i = 0; i < C4F_NET_CLIENTS; i++) if (net->clients[i].fd >= 0) c4fDrop(net, &net->clients[i]);
if (net->fd >= 0) close(net->fd);
net->fd = -1;
}
+594
View File
@@ -0,0 +1,594 @@
/* Control4Free -- a small command server, so experiments stop depending on
* GoldHEN's PayLoader.
*
* The PayLoader stops listening a few minutes after it is started, which meant
* restarting it by hand before every single test. This payload is sent once and
* then stays resident, listening on its own port, so the PC can create virtual
* pads and inject input as often as it likes over one connection. Replies come
* straight back down the socket, so there is no log file to fetch either.
*
* It is deliberately short-lived: an idle client or a long session makes it shut
* down on its own, because it is running inside somebody else's process
* (ScePartyDaemon) and must not sit there forever.
*
* Protocol: one ASCII command per line, one reply line per command.
* add [userhex] create a virtual pad (default 1 = selection screen)
* padmbus call scePadMbusInit() (start the MBus client)
* mbusinit call sceMbusInit()
* bind [userhex] [authid] give the pad to a user (default: signed-in, ShellCore authid)
* del delete it
* press <hexbits> [ms] hold those button bits
* stick <lx> <ly> <rx> <ry> [ms] hold stick positions (0-255, 128 centre)
* hold <ms> neutral for that long (keeps the pad alive)
* sweep <first> <last> [ms] press each single bit from first to last
* status what the server thinks is going on
* klog [lines] recent system log (LOG lines, then OK klog finished)
* quit clean up and exit
*/
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "c4f_log.h"
#include "c4f_sce.h"
#include "c4f_server.h"
#include "c4f_vda.h"
/* Shut down rather than linger inside the host process. */
#ifndef C4F_IDLE_TIMEOUT_S
#define C4F_IDLE_TIMEOUT_S 300
#endif
#ifndef C4F_SESSION_MAX_S
#define C4F_SESSION_MAX_S 1800
#endif
#define C4F_DEFAULT_HOLD_MS 150
#define C4F_MAX_HOLD_MS 30000
#define C4F_KLOG_LINES 128
#define C4F_KLOG_LINE_SIZE 512
static C4fVirtualPad g_pad;
static int g_havePad;
static int g_klogFd = -1; /* how a new pad's handle is found */
static uint64_t g_deadlineMs;
static uint64_t g_nextFrameMs;
static uint64_t g_frames;
static uint64_t g_insertErrors;
static int32_t g_lastInsert;
static int g_replyFailed;
/* A bounded history: never echo these lines through c4fLog, which would feed
* them straight back into the same klog stream. */
static char g_klogLines[C4F_KLOG_LINES][C4F_KLOG_LINE_SIZE];
static char g_klogPartial[C4F_KLOG_LINE_SIZE];
static size_t g_klogUsed;
static unsigned g_klogNext;
static unsigned g_klogCount;
static uint64_t g_klogTotal;
static uint64_t c4fNowMs(void)
{
struct timespec now;
(void)clock_gettime(CLOCK_MONOTONIC, &now);
return (uint64_t)now.tv_sec * 1000u + (uint64_t)now.tv_nsec / 1000000u;
}
static void c4fCaptureKlog(void)
{
char buf[2048];
int batch;
if (g_klogFd < 0) return;
/* Bound the work even when a noisy process keeps writing continuously. */
for (batch = 0; batch < 8; batch++) {
ssize_t n = read(g_klogFd, buf, sizeof(buf));
ssize_t i;
if (n < 0 && errno == EINTR) continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
if (n <= 0) {
g_klogFd = -1; /* main still owns and closes the original fd */
break;
}
for (i = 0; i < n; i++) {
unsigned char ch = (unsigned char)buf[i];
if (ch == '\n') {
g_klogPartial[g_klogUsed] = '\0';
if (g_klogUsed && !strstr(g_klogPartial, "[c4f]")) {
(void)memcpy(g_klogLines[g_klogNext], g_klogPartial,
g_klogUsed + 1);
g_klogNext = (g_klogNext + 1) % C4F_KLOG_LINES;
if (g_klogCount < C4F_KLOG_LINES) g_klogCount++;
g_klogTotal++;
}
g_klogUsed = 0;
} else if ((ch >= 32 || ch == '\t') &&
g_klogUsed + 1 < sizeof(g_klogPartial)) {
g_klogPartial[g_klogUsed++] = (char)ch;
}
}
}
}
static int32_t c4fReport(const ScePadData *data)
{
int32_t ret = c4fVirtualPadInsert(&g_pad, data);
g_frames++;
if (ret < 0) {
g_insertErrors++;
if (ret != g_lastInsert)
c4fLog("InsertData failed = 0x%08x\n", (uint32_t)ret);
}
g_lastInsert = ret;
g_nextFrameMs = c4fNowMs() + C4F_FRAME_MS;
return ret;
}
static void c4fPump(void)
{
c4fCaptureKlog();
if (g_havePad && c4fNowMs() >= g_nextFrameMs) {
ScePadData neutral;
c4fPadDataNeutral(&neutral);
(void)c4fReport(&neutral);
}
}
/* Wait in frame-sized slices, so silence or a disconnected client never stops
* the pad reporting. All pad calls stay on the same thread. */
static int c4fWaitReadable(int fd, uint64_t untilMs)
{
for (;;) {
struct timeval tv;
fd_set set;
uint64_t now, waitMs;
int ready;
c4fPump();
now = c4fNowMs();
if (now >= untilMs || now >= g_deadlineMs) return 0;
waitMs = untilMs - now;
if (waitMs > g_deadlineMs - now) waitMs = g_deadlineMs - now;
if (waitMs > C4F_FRAME_MS) waitMs = C4F_FRAME_MS;
if (g_havePad && g_nextFrameMs > now && waitMs > g_nextFrameMs - now)
waitMs = g_nextFrameMs - now;
tv.tv_sec = 0;
tv.tv_usec = (int)waitMs * 1000;
FD_ZERO(&set);
FD_SET(fd, &set);
ready = select(fd + 1, &set, NULL, NULL, &tv);
if (ready < 0 && errno == EINTR) continue;
if (ready != 0) return ready;
}
}
static void c4fReply(int fd, const char *text)
{
size_t left = strlen(text);
while (left && !g_replyFailed) {
int flags = 0;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#endif
ssize_t sent = send(fd, text, left, flags);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) { g_replyFailed = 1; break; }
text += sent;
left -= (size_t)sent;
}
}
static void c4fReplyFmt(int fd, const char *fmt, ...)
{
char line[512];
va_list ap;
int n;
va_start(ap, fmt);
n = vsnprintf(line, sizeof(line), fmt, ap);
va_end(ap);
if (n < 0) return;
c4fReply(fd, line);
c4fLog("> %s", line);
}
/* Holds `buttons` and the given stick positions for durationMs. */
static int32_t c4fHold(uint32_t buttons, uint8_t lx, uint8_t ly,
uint8_t rx, uint8_t ry, int durationMs)
{
ScePadData data;
uint64_t untilMs;
int32_t result = 0;
if (durationMs < C4F_FRAME_MS) durationMs = C4F_FRAME_MS;
if (durationMs > C4F_MAX_HOLD_MS) durationMs = C4F_MAX_HOLD_MS;
untilMs = c4fNowMs() + (unsigned)durationMs;
c4fPadDataNeutral(&data);
data.buttons = buttons;
data.lx = lx;
data.ly = ly;
data.rx = rx;
data.ry = ry;
while (c4fNowMs() < untilMs && c4fNowMs() < g_deadlineMs) {
int32_t ret = c4fReport(&data);
if (ret < 0 && result == 0) result = ret;
c4fCaptureKlog();
usleep(C4F_FRAME_MS * 1000);
}
return result;
}
static int32_t c4fNeutral(int durationMs)
{
return c4fHold(0, 128, 128, 128, 128, durationMs);
}
static int c4fDuration(int fd, const char *arg, int fallback, int *out)
{
char *end;
long value;
if (!arg) { *out = fallback; return 1; }
errno = 0;
value = strtol(arg, &end, 10);
if (errno || end == arg || *end || value < C4F_FRAME_MS ||
value > C4F_MAX_HOLD_MS) {
c4fReplyFmt(fd, "ERR duration must be %d..%d ms\n",
C4F_FRAME_MS, C4F_MAX_HOLD_MS);
return 0;
}
*out = (int)value;
return 1;
}
static int c4fInputResult(int fd, uint64_t errorsBefore)
{
if (c4fNowMs() >= g_deadlineMs) {
c4fReplyFmt(fd, "ERR session deadline reached; input stopped\n");
return 0;
}
if (g_insertErrors == errorsBefore) return 1;
c4fReplyFmt(fd, "ERR InsertData failed; errors=%llu last=0x%08x (see log)\n",
(unsigned long long)g_insertErrors, (uint32_t)g_lastInsert);
return 0;
}
static int c4fNeedPad(int fd)
{
if (g_havePad) return 1;
c4fReplyFmt(fd, "ERR no pad; run add first\n");
return 0;
}
/* Returns 0 to keep going, 1 to shut down. */
static int c4fCommand(int fd, char *line, int32_t realUserId)
{
char *cmd = strtok(line, " \t");
char *a1 = strtok(NULL, " \t");
char *a2 = strtok(NULL, " \t");
char *a3 = strtok(NULL, " \t");
char *a4 = strtok(NULL, " \t");
char *a5 = strtok(NULL, " \t");
uint64_t errorsBefore = g_insertErrors;
if (!cmd || !*cmd) return 0;
c4fLog("< %s %s %s\n", cmd, a1 ? a1 : "", a2 ? a2 : "");
if (strcmp(cmd, "quit") == 0) {
c4fReplyFmt(fd, "OK bye\n");
return 1;
}
if (strcmp(cmd, "status") == 0) {
c4fReplyFmt(fd, "OK pad=%d handle=0x%08x deviceId=0x%llx vdaUser=0x%08x realUser=0x%08x frames=%llu errors=%llu last=0x%08x klog=%d\n",
g_havePad, (uint32_t)g_pad.handle,
(unsigned long long)g_pad.deviceId,
(uint32_t)g_pad.vdaUserId, (uint32_t)realUserId,
(unsigned long long)g_frames, (unsigned long long)g_insertErrors,
(uint32_t)g_lastInsert, g_klogFd >= 0);
return 0;
}
if (strcmp(cmd, "klog") == 0) {
unsigned count = g_klogCount;
unsigned i;
if (a1) {
char *end;
long requested;
errno = 0;
requested = strtol(a1, &end, 10);
if (errno || end == a1 || *end || requested < 1 || requested > C4F_KLOG_LINES) {
c4fReplyFmt(fd, "ERR klog [1..%d]\n", C4F_KLOG_LINES);
return 0;
}
if ((unsigned)requested < count) count = (unsigned)requested;
}
for (i = 0; i < count; i++) {
unsigned index = (g_klogNext + C4F_KLOG_LINES - count + i) % C4F_KLOG_LINES;
c4fReply(fd, "LOG ");
c4fReply(fd, g_klogLines[index]);
c4fReply(fd, "\n");
}
c4fReplyFmt(fd, "OK klog finished lines=%u total=%llu available=%d\n",
count, (unsigned long long)g_klogTotal, g_klogFd >= 0);
return 0;
}
if (strcmp(cmd, "add") == 0) {
int32_t user = a1 ? (int32_t)strtoul(a1, NULL, 0) : 1;
if (g_havePad) {
c4fReplyFmt(fd, "ERR pad already exists; del first\n");
return 0;
}
/* Only lines written after this point may name the new device. */
c4fKlogDrain(g_klogFd);
g_klogUsed = 0; /* drain/scanner consume whole sections of the stream */
/* Exactly the value asked for: the whole point of the argument is to try
* values the default mapping would rewrite (1 became 0x10000000). */
if (c4fVirtualPadAddAs(&g_pad, user, user, g_klogFd) != 0) {
c4fReplyFmt(fd, "ERR AddDevice gave no handle\n");
return 0;
}
g_havePad = 1;
g_frames = g_insertErrors = 0;
g_lastInsert = 0;
errorsBefore = 0;
/* One neutral sample so the device starts reporting like a real pad. */
c4fNeutral(C4F_FRAME_MS * 3);
if (!c4fInputResult(fd, errorsBefore)) return 0;
c4fReplyFmt(fd, "OK handle=0x%08x deviceId=0x%llx vdaUser=0x%08x\n",
(uint32_t)g_pad.handle, (unsigned long long)g_pad.deviceId,
(uint32_t)g_pad.vdaUserId);
return 0;
}
/* padmbus / mbusinit: start the MBus client before a bind. */
if (strcmp(cmd, "padmbus") == 0 || strcmp(cmd, "mbusinit") == 0) {
int which = strcmp(cmd, "padmbus") == 0 ? 0 : 1;
int32_t ret = 0;
if (c4fMbusClientInit(which, &ret) != 0) {
c4fReplyFmt(fd, "ERR %s not found\n", which == 0 ? "scePadMbusInit" : "sceMbusInit");
return 0;
}
c4fReplyFmt(fd, "OK %s() = 0x%08x\n",
which == 0 ? "scePadMbusInit" : "sceMbusInit", (uint32_t)ret);
return 0;
}
/* bind [userhex] [authidhex]: give the pad to a user. Default user is the
* signed-in account; default authid is SceShellCore's, as Ghostcontrol uses
* on PS4. "bind <user> 0" makes the call with our own credentials -- that
* is the form that crashed the host on 2026-10-04. */
if (strcmp(cmd, "bind") == 0) {
int32_t user = a1 ? (int32_t)strtoul(a1, NULL, 0) : realUserId;
uint64_t authid = a2 ? strtoull(a2, NULL, 0) : C4F_AUTHID_SHELLCORE;
if (!c4fNeedPad(fd)) return 0;
if (c4fVirtualPadBindAs(&g_pad, user, authid) != 0) {
c4fReplyFmt(fd, "ERR bind to 0x%08x failed (see log)\n", (uint32_t)user);
return 0;
}
c4fReplyFmt(fd, "OK bound to 0x%08x (authid 0x%llx)\n",
(uint32_t)user, (unsigned long long)authid);
return 0;
}
if (strcmp(cmd, "del") == 0) {
if (!c4fNeedPad(fd)) return 0;
c4fVirtualPadRemove(&g_pad);
g_havePad = 0;
c4fReplyFmt(fd, "OK deleted\n");
return 0;
}
if (strcmp(cmd, "press") == 0) {
uint32_t bits;
int ms;
if (!a1) { c4fReplyFmt(fd, "ERR press <hexbits> [ms]\n"); return 0; }
if (!c4fNeedPad(fd)) return 0;
bits = (uint32_t)strtoul(a1, NULL, 0);
if (!c4fDuration(fd, a2, C4F_DEFAULT_HOLD_MS, &ms)) return 0;
c4fNeutral(C4F_FRAME_MS * 2);
c4fHold(bits, 128, 128, 128, 128, ms);
c4fNeutral(C4F_FRAME_MS * 3);
if (!c4fInputResult(fd, errorsBefore)) return 0;
c4fReplyFmt(fd, "OK pressed 0x%08x for %dms\n", bits, ms);
return 0;
}
if (strcmp(cmd, "stick") == 0) {
int ms;
if (!a4) { c4fReplyFmt(fd, "ERR stick <lx> <ly> <rx> <ry> [ms]\n"); return 0; }
if (!c4fNeedPad(fd)) return 0;
if (!c4fDuration(fd, a5, 1000, &ms)) return 0;
c4fHold(0, (uint8_t)atoi(a1), (uint8_t)atoi(a2),
(uint8_t)atoi(a3), (uint8_t)atoi(a4), ms);
c4fNeutral(C4F_FRAME_MS * 3);
if (!c4fInputResult(fd, errorsBefore)) return 0;
c4fReplyFmt(fd, "OK sticks %s,%s %s,%s for %dms\n", a1, a2, a3, a4, ms);
return 0;
}
if (strcmp(cmd, "hold") == 0) {
int ms;
if (!c4fNeedPad(fd)) return 0;
if (!c4fDuration(fd, a1, 1000, &ms)) return 0;
c4fNeutral(ms);
if (!c4fInputResult(fd, errorsBefore)) return 0;
c4fReplyFmt(fd, "OK held neutral %dms\n", ms);
return 0;
}
/* Presses each single bit in a range, so one command can walk a whole byte
* while somebody watches the screen. Each one is announced on screen. */
if (strcmp(cmd, "sweep") == 0) {
int first = a1 ? atoi(a1) : 0;
int last = a2 ? atoi(a2) : 15;
int ms;
int bit;
if (!c4fNeedPad(fd)) return 0;
if (!c4fDuration(fd, a3, 400, &ms)) return 0;
if (first < 0) first = 0;
if (last > 31) last = 31;
for (bit = first; bit <= last; bit++) {
uint32_t mask = 1u << bit;
if (g_replyFailed || c4fNowMs() >= g_deadlineMs) break;
c4fNotify("C4F: bit %d (0x%x)", bit, mask);
c4fNeutral(400);
c4fHold(mask, 128, 128, 128, 128, ms);
c4fNeutral(600);
if (!c4fInputResult(fd, errorsBefore)) return 0;
c4fReplyFmt(fd, "OK bit %d = 0x%08x\n", bit, mask);
}
c4fReplyFmt(fd, "OK sweep finished %d..%d\n", first, last);
return 0;
}
c4fReplyFmt(fd, "ERR unknown command %s\n", cmd);
return 0;
}
static int c4fAcceptOne(int listenFd, int timeoutS)
{
int ready;
ready = c4fWaitReadable(listenFd, c4fNowMs() + (unsigned)timeoutS * 1000u);
if (ready <= 0) return -1;
return accept(listenFd, NULL, NULL);
}
int c4fServerRun(int32_t realUserId, int klogFd)
{
struct sockaddr_in addr;
int listenFd;
int one = 1;
int sessions = 0;
g_klogFd = klogFd;
g_havePad = 0;
g_klogUsed = g_klogNext = g_klogCount = 0;
g_klogTotal = 0;
g_frames = g_insertErrors = 0;
g_deadlineMs = c4fNowMs() + (uint64_t)C4F_SESSION_MAX_S * 1000u;
/* Never let an unexpected blocking log source freeze controller reports. */
if (g_klogFd >= 0) {
int flags = fcntl(g_klogFd, F_GETFL, 0);
if (flags < 0 || fcntl(g_klogFd, F_SETFL, flags | O_NONBLOCK) < 0) {
c4fLog("klog nonblocking setup failed errno=%d\n", errno);
g_klogFd = -1;
}
}
listenFd = socket(AF_INET, SOCK_STREAM, 0);
if (listenFd < 0) {
c4fLog("socket() failed errno=%d\n", errno);
return -1;
}
(void)setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
(void)memset(&addr, 0, sizeof(addr));
#ifdef __FreeBSD__
addr.sin_len = sizeof(addr);
#endif
addr.sin_family = AF_INET;
addr.sin_port = htons(C4F_SERVER_PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listenFd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
c4fLog("bind(%d) failed errno=%d\n", C4F_SERVER_PORT, errno);
(void)close(listenFd);
return -1;
}
if (listen(listenFd, 1) < 0) {
c4fLog("listen() failed errno=%d\n", errno);
(void)close(listenFd);
return -1;
}
c4fLog("command server listening on port %d\n", C4F_SERVER_PORT);
c4fNotify("Control4Free: server on port %d", C4F_SERVER_PORT);
while (c4fNowMs() < g_deadlineMs) {
char buf[512];
size_t used = 0;
int discarding = 0;
int clientFd = c4fAcceptOne(listenFd, 30);
int done = 0;
uint64_t idleDeadline;
if (clientFd < 0) continue;
#ifdef SO_NOSIGPIPE
(void)setsockopt(clientFd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
{
struct timeval sendTimeout = { .tv_sec = 1, .tv_usec = 0 };
(void)setsockopt(clientFd, SOL_SOCKET, SO_SNDTIMEO,
&sendTimeout, sizeof(sendTimeout));
}
g_replyFailed = 0;
idleDeadline = c4fNowMs() + (uint64_t)C4F_IDLE_TIMEOUT_S * 1000u;
sessions++;
c4fLog("client %d connected\n", sessions);
c4fReplyFmt(clientFd, "OK Control4Free ready (port %d)\n", C4F_SERVER_PORT);
while (!done && !g_replyFailed) {
char incoming[512];
ssize_t n;
ssize_t i;
if (c4fWaitReadable(clientFd, idleDeadline) <= 0) {
c4fLog("client wait ended (timeout, session limit, or socket error)\n");
break;
}
n = recv(clientFd, incoming, sizeof(incoming), 0);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
idleDeadline = c4fNowMs() + (uint64_t)C4F_IDLE_TIMEOUT_S * 1000u;
for (i = 0; i < n && !done && !g_replyFailed; i++) {
char ch = incoming[i];
if (c4fNowMs() >= g_deadlineMs) break;
if (ch == '\n') {
if (!discarding) {
if (used && buf[used - 1] == '\r') used--;
buf[used] = '\0';
done = c4fCommand(clientFd, buf, realUserId);
}
used = 0;
discarding = 0;
c4fPump();
} else if (!discarding) {
if (used == sizeof(buf) - 1 || ch == '\0') {
c4fReplyFmt(clientFd, "ERR invalid or too long line\n");
discarding = 1; /* discard the entire command, including its tail */
used = 0;
} else {
buf[used++] = ch;
}
}
}
}
(void)close(clientFd);
c4fLog("client disconnected\n");
if (done) break;
}
if (g_havePad) {
c4fLog("cleaning up the pad before exit\n");
c4fVirtualPadRemove(&g_pad);
g_havePad = 0;
}
(void)close(listenFd);
c4fLog("command server stopped\n");
c4fNotify("Control4Free: server stopped");
return 0;
}
+761
View File
@@ -0,0 +1,761 @@
/* Control4Free -- virtual device creation and input injection.
*
* Ported from seregonwar/SplashDown's psbutton.c (GPL-3.0), the only known
* working caller of this API on a PS4. The call order and the klog trick for
* recovering the device handle both come from there; see dev/notes/vda.md.
*/
#include <arpa/inet.h>
#include <ctype.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ps4/kernel.h>
#include <ps4/klog.h>
#include "c4f_log.h"
#include "c4f_sce.h"
#include "c4f_vda.h"
#ifndef RTLD_NOW
#define RTLD_NOW 2
#endif
#ifndef RTLD_GLOBAL
#define RTLD_GLOBAL 0x100
#endif
static C4fMbusBindFn g_mbusBind;
static C4fMbusDisconnectFn g_mbusDisconnect;
static void *g_mbusHandle;
static void (*g_waitCallback)(void *);
static void *g_waitContext;
void c4fVdaSetWaitCallback(void (*callback)(void *), void *context)
{
g_waitCallback = callback;
g_waitContext = context;
}
static uint64_t c4fVdaNowMs(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000;
}
/* ---- libSceMbus ---- */
int c4fMbusInit(void)
{
static const char *paths[] = {
C4F_LIBSCEMBUS_PATH,
"/system/common/lib/libSceMbus",
"libSceMbus.sprx",
"libSceMbus",
};
void *handle = NULL;
size_t i;
for (i = 0; i < sizeof(paths) / sizeof(paths[0]) && !handle; i++) {
handle = dlopen(paths[i], RTLD_NOW | RTLD_GLOBAL);
c4fLog("dlopen(%s) = %p\n", paths[i], handle);
}
if (!handle) {
c4fLog("FATAL: libSceMbus did not load; not touching libScePad\n");
return -1;
}
g_mbusBind = (C4fMbusBindFn)dlsym(handle, "sceMbusBindDeviceWithUserId");
g_mbusDisconnect = (C4fMbusDisconnectFn)dlsym(handle, "sceMbusDisconnectDevice");
c4fLog("sceMbusBindDeviceWithUserId=%p sceMbusDisconnectDevice=%p\n",
(void *)g_mbusBind, (void *)g_mbusDisconnect);
if (!g_mbusBind) {
c4fLog("FATAL: sceMbusBindDeviceWithUserId unresolved\n");
return -1;
}
g_mbusHandle = handle;
return 0;
}
/* libSceMbus is a client of a system service. Our host process (ScePartyDaemon)
* never starts that client, and a bind without it faulted twice on 2026-10-04.
* libScePad has its own wrapper for starting it, scePadMbusInit, which games
* never call; there is also sceMbusInit itself. Neither has a published
* signature, so both are called with no arguments and the return is logged.
* which: 0 = scePadMbusInit, 1 = sceMbusInit. */
typedef int32_t (*C4fNoArgFn)(void);
int c4fMbusClientInit(int which, int32_t *outRet)
{
const char *name = which == 0 ? "scePadMbusInit" : "sceMbusInit";
void *lib;
C4fNoArgFn fn;
if (which == 0) {
lib = dlopen("/system/common/lib/libScePad.sprx", RTLD_NOW | RTLD_GLOBAL);
} else {
lib = g_mbusHandle;
}
if (!lib) {
c4fLog("%s: library handle missing\n", name);
return -1;
}
fn = (C4fNoArgFn)dlsym(lib, name);
c4fLog("%s resolved at %p\n", name, (void *)fn);
if (!fn) return -1;
*outRet = fn();
c4fLog("%s() = 0x%08x\n", name, (uint32_t)*outRet);
return 0;
}
/* ---- pad service ---- */
/* scePadInit has to come first. SplashDown sets the privilege before init and
* never checks the result; on our console that call returns 0x80920005
* (SCE_PAD_ERROR_NOT_INITIALIZED), so it did nothing. Ghostcontrol only sets it
* in processes whose libScePad is already initialized. */
int c4fPadInit(void)
{
int32_t ret;
ret = scePadInit();
c4fLog("scePadInit() = 0x%08x\n", (uint32_t)ret);
if (ret != 0) {
c4fLog("scePadInit failed\n");
return -1;
}
ret = scePadSetProcessPrivilege(1);
c4fLog("scePadSetProcessPrivilege(1) = 0x%08x\n", (uint32_t)ret);
return 0;
}
static int c4fIsRealUser(int32_t userId)
{
return userId != 0 && userId != C4F_USER_ID_INVALID;
}
int32_t c4fPickUserId(void)
{
int32_t userId = C4F_USER_ID_INVALID;
int32_t ret;
if (sceUserServiceGetForegroundUser) {
ret = sceUserServiceGetForegroundUser(&userId);
c4fLog("GetForegroundUser = 0x%08x id=0x%08x\n", (uint32_t)ret, (uint32_t)userId);
if (ret == 0 && c4fIsRealUser(userId)) return userId;
} else {
c4fLog("GetForegroundUser not present on this firmware\n");
}
userId = C4F_USER_ID_INVALID;
ret = sceUserServiceGetInitialUser(&userId);
c4fLog("GetInitialUser = 0x%08x id=0x%08x\n", (uint32_t)ret, (uint32_t)userId);
if (ret == 0 && c4fIsRealUser(userId)) return userId;
c4fLog("no real user found; using fallback 0x%08x\n", C4F_VDA_USER_FALLBACK);
return C4F_VDA_USER_FALLBACK;
}
/* Which user the device is created for. SplashDown maps any real account id
* (like 1A2B3C4D) onto 0x10000000. On our console a device created that way
* acts as the signed-in user: PS works, but there is no "Who's using this
* controller?" screen (2026-10-04). C4F_VDA_USER overrides it at build time to
* look for a value that makes the device arrive unassigned; Ghostcontrol uses
* 1 as an "anonymous" slot and sees -1 on devices waiting for assignment. */
static int32_t c4fVdaUserId(int32_t userId)
{
#ifdef C4F_VDA_USER
(void)userId;
return (int32_t)(C4F_VDA_USER);
#else
if (userId >= 0x10000000 && userId <= 0x1000000f) return userId;
return C4F_VDA_USER_FALLBACK;
#endif
}
/* ---- /dev/klog scanning ----
*
* AddDevice hands back a status, not a handle. The handle we need is the MBus
* DeviceId, and the only way for a payload to learn it is to read the kernel log
* while the device is being added. Ugly, but it is what works.
*/
/* /dev/klog has a single reader, and on GoldHEN 2.4b18 its klog server already
* holds it (open fails with EBUSY, confirmed 2026-10-04). That server has no off
* switch, so the fallback is to be one of its clients: it rebroadcasts the same
* kernel lines on port 3232. */
static int c4fKlogConnectLocal(void)
{
struct sockaddr_in addr;
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) {
c4fLog("klog socket() failed errno=%d\n", errno);
return -1;
}
(void)memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(C4F_KLOG_PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
c4fLog("connect(127.0.0.1:%d) failed errno=%d\n", C4F_KLOG_PORT, errno);
close(s);
return -1;
}
if (fcntl(s, F_SETFL, fcntl(s, F_GETFL, 0) | O_NONBLOCK) != 0)
c4fLog("klog socket O_NONBLOCK failed errno=%d\n", errno);
return s;
}
int c4fKlogOpen(void)
{
int fd = open("/dev/klog", O_RDONLY | O_NONBLOCK);
if (fd >= 0) {
c4fLog("opened /dev/klog for DeviceId capture\n");
return fd;
}
c4fLog("open(/dev/klog) failed errno=%d (16: GoldHEN's klog server holds it); "
"trying 127.0.0.1:%d\n", errno, C4F_KLOG_PORT);
fd = c4fKlogConnectLocal();
if (fd >= 0)
c4fLog("reading klog through 127.0.0.1:%d\n", C4F_KLOG_PORT);
else
c4fLog("no klog source; DeviceId capture off\n");
return fd;
}
/* Throws away whatever is already buffered so only new lines get scanned. A
* fresh connection to the klog server may replay a backlog, and an old "device
* added" line in it would hand us a stale DeviceId. Keep reading until the
* stream has been quiet for a while rather than stopping at the first empty
* read, because the backlog does not arrive all at once. */
void c4fKlogDrain(int fd)
{
char tmp[1024];
int quietMs = 0;
int totalMs = 0;
long bytes = 0;
uint64_t started = c4fVdaNowMs();
if (fd < 0) return;
while (quietMs < 300 && totalMs < 3000 && c4fVdaNowMs() - started < 3000) {
if (g_waitCallback) g_waitCallback(g_waitContext);
ssize_t n = read(fd, tmp, sizeof(tmp));
if (n > 0) {
bytes += n;
quietMs = 0;
continue;
}
usleep(20000);
quietMs += 20;
totalMs += 20;
}
c4fLog("klog drain: discarded %ld bytes of backlog\n", bytes);
}
/* Writes a marker into the kernel log and checks we can read it back. If this
* fails, stage 3 cannot learn the virtual device's handle. */
int c4fKlogSelfTest(int fd)
{
static const char marker[] = "c4f-klog-selftest";
char buf[512];
char window[sizeof(buf) + sizeof(marker)];
size_t keep = 0;
long bytes = 0;
int found = 0;
int waited;
int savedKlog;
if (fd < 0) {
c4fLog("klog self-test skipped: no klog source\n");
return -1;
}
/* Straight to klog, not through c4fLog, so the mirror file only gets the
* result line. */
klog_printf("[c4f] %s\n", marker);
savedKlog = c4fLogKlogEnabled();
c4fLogSetKlog(0);
for (waited = 0; waited < 2000 && !found; ) {
ssize_t n = read(fd, window + keep, sizeof(buf));
if (n <= 0) {
usleep(20000);
waited += 20;
continue;
}
bytes += n;
{
size_t total = keep + (size_t)n;
size_t tail = sizeof(marker) - 1;
window[total] = '\0';
if (strstr(window, marker)) found = 1;
/* Carry the tail over so a marker split across two reads still matches. */
if (total > tail) {
(void)memmove(window, window + total - tail, tail);
keep = tail;
} else {
keep = total;
}
}
}
c4fLogSetKlog(savedKlog);
c4fLog("klog self-test: %s (%ld bytes read in %d ms)\n",
found ? "PASS, our own line came back" : "FAIL, marker never seen",
bytes, waited);
return found ? 0 : -1;
}
static uint64_t c4fParseHexAfter(const char *line, const char *key)
{
const char *p = strstr(line, key);
uint64_t value = 0;
if (!p) return 0;
p += strlen(key);
while (*p == ' ' || *p == '\t' || *p == ':' || *p == '=') p++;
if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) p += 2;
if (!isxdigit((unsigned char)*p)) return 0;
while (isxdigit((unsigned char)*p)) {
char c = *p++;
value <<= 4;
if (c >= '0' && c <= '9') value |= (uint64_t)(c - '0');
else if (c >= 'a' && c <= 'f') value |= (uint64_t)(10 + c - 'a');
else value |= (uint64_t)(10 + c - 'A');
}
return value;
}
static uint64_t c4fParseDeviceId(const char *line)
{
static const char *keys[] = { "DeviceId", "DeviceID", "deviceId", "deviceID" };
size_t i;
for (i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
uint64_t id = c4fParseHexAfter(line, keys[i]);
if (id) return id;
}
return 0;
}
static int c4fIsAddLine(const char *line)
{
return strstr(line, "DEVICE_ADDED") != NULL || strstr(line, " ADD") != NULL;
}
/* A PS4 virtual pad is logged as the Remote Play device: type 4, subType 2. */
static int c4fIsVirtualAddLine(const char *line)
{
int looksVirtual = strstr(line, "REMOTEPLAY") != NULL ||
strstr(line, "type:4") != NULL || strstr(line, "type=4") != NULL ||
strstr(line, "subType:2") != NULL || strstr(line, "subType=2") != NULL;
return c4fIsAddLine(line) && looksVirtual;
}
int c4fKlogFindDeviceId(int fd, uint64_t *outDeviceId, int timeoutMs)
{
char buf[512];
char line[1024];
size_t lineLen = 0;
uint64_t weakId = 0;
int loops = timeoutMs / 20;
int result = -1;
int savedKlog;
int i;
if (fd < 0 || !outDeviceId) return -1;
if (loops < 1) loops = 1;
/* Our own writes would be read straight back and bury the kernel lines. */
savedKlog = c4fLogKlogEnabled();
c4fLogSetKlog(0);
c4fLog("scanning /dev/klog for a virtual DeviceId, up to %d ms\n", timeoutMs);
for (i = 0; i < loops && result != 0; i++) {
if (g_waitCallback) g_waitCallback(g_waitContext);
ssize_t n = read(fd, buf, sizeof(buf));
ssize_t k;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(20000);
continue;
}
c4fLog("klog read errno=%d\n", errno);
break;
}
if (n == 0) {
usleep(20000);
continue;
}
for (k = 0; k < n; k++) {
char c = buf[k];
uint64_t id;
if (c == '\r') continue;
if (c != '\n' && lineLen + 1 < sizeof(line)) {
line[lineLen++] = c;
continue;
}
line[lineLen] = '\0';
lineLen = 0;
id = c4fParseDeviceId(line);
if (id && c4fIsVirtualAddLine(line)) {
*outDeviceId = id;
c4fLog("matched virtual device line: %s\n", line);
result = 0;
break;
}
/* Any other "device added" line is a weaker candidate: keep the
* first one in case the exact match never arrives. */
if (id && c4fIsAddLine(line) && !weakId) {
weakId = id;
c4fLog("weak candidate DeviceId=0x%llx from: %s\n",
(unsigned long long)weakId, line);
}
}
}
if (result != 0 && weakId) {
*outDeviceId = weakId;
c4fLog("no exact match; taking weak candidate DeviceId=0x%llx\n",
(unsigned long long)weakId);
result = 0;
}
if (result != 0) c4fLog("no DeviceId seen in klog\n");
c4fLogSetKlog(savedKlog);
return result;
}
/* ---- device lifecycle ---- */
int c4fVirtualPadAdd(C4fVirtualPad *out, int32_t userId, int klogFd)
{
return c4fVirtualPadAddAs(out, userId, c4fVdaUserId(userId), klogFd);
}
int c4fVirtualPadAddAs(C4fVirtualPad *out, int32_t userId, int32_t vdaUser, int klogFd)
{
C4fVdaParam param;
uint64_t deviceId = 0;
int32_t ret;
int i;
(void)memset(out, 0, sizeof(*out));
out->handle = -1;
out->userId = C4F_USER_ID_INVALID;
(void)memset(&param, 0, sizeof(param));
param.size = (int32_t)sizeof(param);
param.userId = vdaUser;
/* A marker, so the log shows whether the call writes anything back. */
for (i = 0; i < 6; i++) param.pad[i] = (int32_t)0xdeadbeef;
c4fLog("AddDevice: size=%d userId=0x%08x type=%d\n",
param.size, (uint32_t)param.userId, C4F_VIRTUAL_DEVICE_TYPE);
ret = scePadVirtualDeviceAddDevice(&param, C4F_VIRTUAL_DEVICE_TYPE);
c4fLog("AddDevice = 0x%08x pad=[0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x]\n",
(uint32_t)ret,
(uint32_t)param.pad[0], (uint32_t)param.pad[1], (uint32_t)param.pad[2],
(uint32_t)param.pad[3], (uint32_t)param.pad[4], (uint32_t)param.pad[5]);
/* A non-zero return does not have to mean failure: on PS5 SplashDown sees
* 0x803b0006 and the device is created anyway. So look for it regardless. */
if (klogFd >= 0 && c4fKlogFindDeviceId(klogFd, &deviceId, 2500) == 0) {
out->deviceId = deviceId;
out->handle = (int32_t)(deviceId & 0xffffffffu);
out->owned = 1;
c4fLog("DeviceId=0x%llx handle=0x%08x\n",
(unsigned long long)deviceId, (uint32_t)out->handle);
}
if (out->handle < 0) {
for (i = 0; i < 6; i++) {
if (param.pad[i] > 0 && param.pad[i] != (int32_t)0xdeadbeef) {
out->handle = param.pad[i];
out->deviceId = (uint32_t)out->handle;
out->owned = 1;
c4fLog("handle from written-back pad[%d]=0x%08x\n",
i, (uint32_t)out->handle);
break;
}
}
}
if (out->handle < 0) {
c4fLog("ERROR: no virtual device handle\n");
return -1;
}
out->userId = userId;
out->vdaUserId = vdaUser;
return 0;
}
int c4fVirtualPadBind(C4fVirtualPad *pad, int32_t userId)
{
int32_t ret;
if (!g_mbusBind) {
c4fLog("bind skipped: MBus not resolved\n");
return -1;
}
if (!pad->deviceId) {
c4fLog("bind skipped: no DeviceId\n");
return -1;
}
ret = g_mbusBind(pad->deviceId, userId);
c4fLog("sceMbusBindDeviceWithUserId(0x%llx, 0x%08x) = 0x%08x\n",
(unsigned long long)pad->deviceId, (uint32_t)userId, (uint32_t)ret);
if (ret != 0) return -1;
pad->bound = 1;
pad->userId = userId;
return 0;
}
/* Ghostcontrol's PS4 path makes this call with SceShellCore's authid switched
* on for just the call, then puts the old one back. We called it with
* SplashDown's 0x3800000000010003 and the host process crashed (stage 4,
* 2026-10-04), so the credentials are the first suspect. */
int c4fVirtualPadBindAs(C4fVirtualPad *pad, int32_t userId, uint64_t authid)
{
pid_t pid = getpid();
uint64_t saved = kernel_get_ucred_authid(pid);
int result;
if (authid) {
c4fLog("bind: authid 0x%016llx -> 0x%016llx for the call\n",
(unsigned long long)saved, (unsigned long long)authid);
(void)kernel_set_ucred_authid(pid, authid);
}
result = c4fVirtualPadBind(pad, userId);
if (authid) (void)kernel_set_ucred_authid(pid, saved);
return result;
}
void c4fPadDataNeutral(ScePadData *data)
{
(void)memset(data, 0, sizeof(*data));
data->lx = 128;
data->ly = 128;
data->rx = 128;
data->ry = 128;
data->quat[3] = 1.0f; /* identity rotation */
data->connected = 1;
data->count = 1;
}
/* A real pad stamps every report with a rising time and a rising counter. Ours
* sent the same zero every frame, which anything that de-duplicates reports
* would see as "no new data". Give each sample its own stamp. */
static uint64_t g_sampleStamp;
static uint8_t g_sampleCount;
int32_t c4fVirtualPadInsert(const C4fVirtualPad *pad, const ScePadData *data)
{
ScePadData stamped = *data;
g_sampleStamp += C4F_FRAME_MS * 1000u; /* microseconds, like a real pad */
g_sampleCount++;
stamped.timestamp = g_sampleStamp;
stamped.count = g_sampleCount;
return scePadVirtualDeviceInsertData(pad->handle, &stamped);
}
int32_t c4fVirtualPadHold(const C4fVirtualPad *pad, uint32_t buttons,
int durationMs, const char *label)
{
ScePadData data;
int32_t ret = 0;
int frames = 0;
int elapsed;
if (durationMs < C4F_FRAME_MS) durationMs = C4F_FRAME_MS;
c4fPadDataNeutral(&data);
data.buttons = buttons;
/* A real pad reports continuously, so one lone sample is easily missed. */
for (elapsed = 0; elapsed < durationMs; elapsed += C4F_FRAME_MS) {
ret = c4fVirtualPadInsert(pad, &data);
frames++;
usleep(C4F_FRAME_MS * 1000);
}
c4fLog("%s: buttons=0x%08x %dms %d frames last=0x%08x\n",
label ? label : "hold", buttons, durationMs, frames, (uint32_t)ret);
return ret;
}
int32_t c4fProbeAssignment(int32_t realUserId)
{
/* Device type 3 is what we created; 0 is the ordinary "any pad" type that
* the shell uses. Both are worth asking about. */
static const int32_t types[] = { 0, 3 };
int32_t users[3];
size_t u, t;
int32_t idx;
users[0] = realUserId;
users[1] = C4F_VDA_USER_FALLBACK; /* 0x10000000 */
users[2] = 1;
for (u = 0; u < sizeof(users) / sizeof(users[0]); u++) {
for (t = 0; t < sizeof(types) / sizeof(types[0]); t++) {
for (idx = 0; idx < 4; idx++) {
int32_t handle = scePadGetHandle(users[u], types[t], idx);
if (handle >= 0) {
c4fLog("assigned: GetHandle(user=0x%08x type=%d idx=%d) = 0x%08x\n",
(uint32_t)users[u], (int)types[t], (int)idx, (uint32_t)handle);
return users[u];
}
}
}
}
c4fLog("not assigned yet (no GetHandle answered)\n");
return C4F_USER_ID_INVALID;
}
/* Only ever called with the user the virtual device was created for. Asking for
* the signed-in account instead hands back that person's real DualShock, which
* reads as all-zero (nobody is touching it) and is not ours to open and close. */
int32_t c4fOpenReadHandle(int32_t userId)
{
/* Type 3 is what the device was created as, so try it before the generic 0. */
static const int32_t types[] = { C4F_VIRTUAL_DEVICE_TYPE, 0 };
size_t t;
int32_t idx;
for (t = 0; t < sizeof(types) / sizeof(types[0]); t++) {
for (idx = 0; idx < 4; idx++) {
int32_t handle = scePadGetHandle(userId, types[t], idx);
if (handle >= 0) {
c4fLog("read handle from GetHandle(user=0x%08x type=%d idx=%d) = 0x%08x\n",
(uint32_t)userId, (int)types[t], (int)idx, (uint32_t)handle);
return handle;
}
}
}
for (t = 0; t < sizeof(types) / sizeof(types[0]); t++) {
int32_t handle = scePadOpen(userId, types[t], 0, NULL);
c4fLog("scePadOpen(user=0x%08x type=%d) = 0x%08x\n",
(uint32_t)userId, (int)types[t], (uint32_t)handle);
if (handle >= 0) return handle;
}
return -1;
}
/* Candidate input bits. The documented read-side layout first, then the two
* bits SplashDown reported for this path. */
typedef struct {
uint32_t bit;
const char *name;
} C4fBitCandidate;
static const C4fBitCandidate c4fBitCandidates[] = {
{ C4F_PAD_L3, "L3?" },
{ C4F_PAD_R3, "R3?" },
{ C4F_PAD_OPTIONS, "OPTIONS?" },
{ C4F_PAD_UP, "UP?" },
{ C4F_PAD_RIGHT, "RIGHT?" },
{ C4F_PAD_DOWN, "DOWN?" },
{ C4F_PAD_LEFT, "LEFT?" },
{ C4F_PAD_L2, "L2?" },
{ C4F_PAD_R2, "R2?" },
{ C4F_PAD_L1, "L1?" },
{ C4F_PAD_R1, "R1?" },
{ C4F_PAD_TRIANGLE, "TRIANGLE?" },
{ C4F_PAD_CIRCLE, "CIRCLE?" },
{ C4F_PAD_CROSS, "CROSS?" },
{ C4F_PAD_SQUARE, "SQUARE?" },
{ C4F_PAD_TOUCHPAD, "TOUCHPAD?" },
{ 0x00020000u, "SplashDown's Cross bit" },
};
void c4fProbeButtonMap(const C4fVirtualPad *pad, int32_t readHandle)
{
ScePadData sample;
ScePadData readback;
size_t i;
int frame;
int32_t ret;
/* What the pad reports with nothing held: anything set here is noise that
* has to be subtracted from every result below. */
c4fPadDataNeutral(&sample);
for (frame = 0; frame < 5; frame++) {
(void)c4fVirtualPadInsert(pad, &sample);
usleep(C4F_FRAME_MS * 1000);
}
(void)memset(&readback, 0, sizeof(readback));
ret = scePadReadState(readHandle, &readback);
c4fLog("probe baseline: ReadState = 0x%08x buttons=0x%08x connected=%u\n",
(uint32_t)ret, readback.buttons, (unsigned)readback.connected);
if (ret < 0) {
c4fLog("probe aborted: cannot read the pad back\n");
return;
}
for (i = 0; i < sizeof(c4fBitCandidates) / sizeof(c4fBitCandidates[0]); i++) {
uint32_t seen = 0;
c4fPadDataNeutral(&sample);
sample.buttons = c4fBitCandidates[i].bit;
for (frame = 0; frame < 5; frame++) {
(void)c4fVirtualPadInsert(pad, &sample);
usleep(C4F_FRAME_MS * 1000);
(void)memset(&readback, 0, sizeof(readback));
if (scePadReadState(readHandle, &readback) >= 0) seen |= readback.buttons;
}
c4fLog("probe bit 0x%08x (%s) -> reported 0x%08x\n",
c4fBitCandidates[i].bit, c4fBitCandidates[i].name, seen);
/* Release, so presses do not run together. */
c4fPadDataNeutral(&sample);
for (frame = 0; frame < 5; frame++) {
(void)c4fVirtualPadInsert(pad, &sample);
usleep(C4F_FRAME_MS * 1000);
}
}
c4fLog("probe done\n");
}
void c4fVirtualPadRemove(C4fVirtualPad *pad)
{
int32_t ret;
if (!pad->owned || pad->handle < 0) return;
/* DeleteDevice alone, as SplashDown does. sceMbusDisconnectDevice is resolved
* (g_mbusDisconnect) but calling it before the delete has not been tried on
* hardware, so it stays out of the cleanup path until it has. */
ret = scePadVirtualDeviceDeleteDevice(pad->handle);
c4fLog("DeleteDevice(0x%08x) = 0x%08x\n", (uint32_t)pad->handle, (uint32_t)ret);
pad->owned = 0;
pad->bound = 0;
pad->handle = -1;
pad->deviceId = 0;
}
+370
View File
@@ -0,0 +1,370 @@
/* Browser-controlled VDA lifecycle. No automatic button presses or user binds.
* The page talks JSON over a WebSocket: info, status, claim, u (input), leave, stop. */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define JSMN_STATIC
#define JSMN_STRICT
#include "jsmn.h"
#include "c4f_log.h"
#include "c4f_net.h"
#include "c4f_vda.h"
#include "c4f_web.h"
#define C4F_VERSION "0.2.0"
#define C4F_INPUT_MASK 0x0011ffffu
#define C4F_REPORT_MS 16
#ifndef C4F_STALE_MS
#define C4F_STALE_MS 3000
#endif
#ifndef C4F_RELEASE_MS
#define C4F_RELEASE_MS 15000
#endif
#define C4F_QUEUE_SIZE 32
typedef struct {
C4fVirtualPad device;
C4fNetClient *owner;
ScePadData current, queue[C4F_QUEUE_SIZE];
unsigned head, count;
uint64_t lastInput, nextReport, detachedAt, reports;
int active, stale, error, assigned;
uint32_t userId;
} C4fWebPad;
typedef struct {
C4fNet net;
C4fWebPad pads[C4F_MAX_PADS];
int klogFd, stop, changed, creationBlocked;
char klogLine[1024];
size_t klogUsed;
uint64_t broadcastAt, stopAt;
} C4fWeb;
typedef struct {
char method[24];
int64_t id, args[16];
unsigned argc;
int hasId;
} C4fRequest;
static int c4fTokenEquals(const char *json, const jsmntok_t *t, const char *s)
{ return t->type == JSMN_STRING && (size_t)(t->end-t->start) == strlen(s) && !memcmp(json+t->start, s, strlen(s)); }
static int c4fInteger(const char *json, const jsmntok_t *t, int64_t *out)
{
char tmp[24], *end;
int n = t->end-t->start;
if (t->type != JSMN_PRIMITIVE || n < 1 || n >= (int)sizeof(tmp)) return -1;
memcpy(tmp, json+t->start, (size_t)n); tmp[n] = 0;
for (int i = tmp[0] == '-' ? 1 : 0; i < n; i++) if (tmp[i] < '0' || tmp[i] > '9') return -1;
errno = 0; *out = strtoll(tmp, &end, 10);
return errno || end == tmp || *end ? -1 : 0;
}
static int c4fParseRequest(const char *json, size_t len, C4fRequest *r)
{
jsmn_parser parser;
jsmntok_t tok[64];
int count, method = 0, params = 0;
if (strlen(json) != len) return -1;
memset(r, 0, sizeof(*r)); jsmn_init(&parser);
count = jsmn_parse(&parser, json, len, tok, 64);
if (count < 1 || tok[0].type != JSMN_OBJECT) return -1;
for (size_t i = (size_t)tok[0].end; i < len; i++) if (!strchr(" \r\n\t", json[i])) return -1;
for (int i = 1; i < count;) {
jsmntok_t *key = &tok[i++], *value;
if (i >= count) return -1;
value = &tok[i++];
if (c4fTokenEquals(json, key, "id")) {
if (r->hasId || c4fInteger(json, value, &r->id) || r->id < 0 || r->id > 9007199254740991LL) return -1;
r->hasId = 1;
} else if (c4fTokenEquals(json, key, "method")) {
int n = value->end-value->start;
if (method++ || value->type != JSMN_STRING || n < 1 || n >= (int)sizeof(r->method)) return -1;
memcpy(r->method, json+value->start, (size_t)n); r->method[n] = 0;
} else if (c4fTokenEquals(json, key, "params")) {
if (params++ || value->type != JSMN_ARRAY || value->size > 16) return -1;
r->argc = (unsigned)value->size;
for (unsigned a = 0; a < r->argc; a++) {
if (i >= count || c4fInteger(json, &tok[i++], &r->args[a])) return -1;
}
} else if (c4fTokenEquals(json, key, "jsonrpc")) {
if (!c4fTokenEquals(json, value, "2.0")) return -1;
} else return -1;
}
return method == 1 && params == 1 ? 0 : -1;
}
static void c4fError(C4fNetClient *c, const C4fRequest *r, int code, const char *message)
{
char reply[384];
/* Messages are constant ASCII strings under our control. */
if (r && r->hasId) snprintf(reply, sizeof(reply), "{\"id\":%lld,\"error\":{\"code\":%d,\"message\":\"%s\"}}", (long long)r->id, code, message);
else snprintf(reply, sizeof(reply), "{\"method\":\"error\",\"params\":{\"message\":\"%s\"}}", message);
c4fNetText(c, reply);
}
static void c4fStatus(C4fWeb *app, C4fNetClient *c, const C4fRequest *request)
{
char json[3072];
size_t pos;
if (request && request->hasId) pos = (size_t)snprintf(json, sizeof(json), "{\"id\":%lld,\"result\":", (long long)request->id);
else pos = (size_t)snprintf(json, sizeof(json), "{\"method\":\"s\",\"params\":");
pos += (size_t)snprintf(json+pos, sizeof(json)-pos, "{\"version\":\"%s\",\"protocol\":2,\"pads\":[", C4F_VERSION);
for (int i = 0; i < C4F_MAX_PADS; i++) {
C4fWebPad *p = &app->pads[i];
const char *state = p->error ? "error" : !p->active ? "free" : !p->owner || p->stale ? "paused" : p->assigned ? "ready" : "select";
unsigned colors[4][3] = {{32,96,255},{255,48,64},{48,200,96},{255,80,180}};
pos += (size_t)snprintf(json+pos, sizeof(json)-pos,
"%s{\"pad\":%d,\"name\":\"Controller %d\",\"enabled\":true,\"open\":%s,\"connected\":%s,\"clients\":%d,\"mine\":%s,\"state\":\"%s\",\"uid\":\"%s%08x\",\"color\":[%u,%u,%u],\"reports\":%llu,\"error\":%d}",
i ? "," : "", i, i+1, p->active ? "true" : "false", p->owner && !p->stale ? "true" : "false", p->owner ? 1 : 0,
p->owner == c ? "true" : "false", state, p->assigned ? "" : "unassigned-", p->userId,
colors[i][0], colors[i][1], colors[i][2], (unsigned long long)p->reports, p->error);
}
snprintf(json+pos, sizeof(json)-pos, "]}}"); c4fNetText(c, json);
}
static void c4fNeutralize(C4fWebPad *p)
{
p->head = p->count = 0;
c4fPadDataNeutral(&p->current);
}
static void c4fRemove(C4fWeb *app, C4fWebPad *p)
{
if (p->active) {
c4fNeutralize(p);
(void)c4fVirtualPadInsert(&p->device, &p->current);
c4fVirtualPadRemove(&p->device);
}
memset(p, 0, sizeof(*p)); app->changed = 1;
}
/* Also called during VDA creation waits; never re-enter the network or klog. */
static void c4fReportPads(void *context)
{
C4fWeb *app = context;
uint64_t now = c4fTimeMs();
for (int i = 0; i < C4F_MAX_PADS; i++) {
C4fWebPad *p = &app->pads[i];
if (!p->active) continue;
if (p->owner && now-p->lastInput >= C4F_STALE_MS && !p->stale) {
c4fNeutralize(p); p->stale = 1; app->changed = 1;
}
if (now < p->nextReport) continue;
if (p->count) {
p->current = p->queue[p->head]; p->head = (p->head+1) % C4F_QUEUE_SIZE; p->count--;
}
int ret = c4fVirtualPadInsert(&p->device, &p->current);
p->reports++; p->nextReport = now + C4F_REPORT_MS;
if (ret < 0 && !p->error) {
p->error = ret; app->changed = 1;
c4fNeutralize(p);
c4fLog("web controller %d InsertData = 0x%08x\n", i+1, (uint32_t)ret);
}
}
}
/* Preserve button/touch edges for at least one report. Coalesce analogue-only
* changes so mouse/gamepad updates cannot create seconds of input latency. */
static void c4fEnqueue(C4fWebPad *p, const ScePadData *data)
{
ScePadData *last = p->count ? &p->queue[(p->head+p->count-1)%C4F_QUEUE_SIZE] : &p->current;
if (p->count && last->buttons == data->buttons && last->touchData.fingers == data->touchData.fingers) { *last = *data; return; }
if (p->count == C4F_QUEUE_SIZE) {
/* Excessive input cannot leave an old press latched. Keep the latest
* state and clear obsolete history instead of growing a backlog. */
p->head = p->count = 0;
}
p->queue[(p->head+p->count)%C4F_QUEUE_SIZE] = *data; p->count++;
}
static void c4fReadKlog(C4fWeb *app)
{
char buf[2048];
if (app->klogFd < 0) return;
for (int batch = 0; batch < 8; batch++) {
ssize_t n = read(app->klogFd, buf, sizeof(buf));
if (n < 0 && errno == EINTR) continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return;
if (n <= 0) {
close(app->klogFd); app->klogFd = -1;
c4fLog("klog source closed; reconnecting at the next controller request\n");
return;
}
for (ssize_t j = 0; j < n; j++) {
if (buf[j] == '\n') {
unsigned long long device;
unsigned user;
char *event;
app->klogLine[app->klogUsed] = 0;
event = strstr(app->klogLine, "DEVICE_OWNER_CHANGED [DeviceId:");
if (event && sscanf(event, "DEVICE_OWNER_CHANGED [DeviceId:0x%llx][UserId:0x%x]", &device, &user) == 2) {
for (int i = 0; i < C4F_MAX_PADS; i++) {
C4fWebPad *p = &app->pads[i];
if (p->active && p->device.deviceId == device) {
p->assigned = user != 0xffffffffu; p->userId = user; app->changed = 1;
c4fLog("web controller %d assignment confirmed=%d\n", i+1, p->assigned);
}
}
}
app->klogUsed = 0;
} else if (buf[j] != '\r' && app->klogUsed+1 < sizeof(app->klogLine)) app->klogLine[app->klogUsed++] = buf[j];
}
}
}
static void c4fClaim(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
{
unsigned wanted = 0, created = 0;
for (unsigned i = 0; i < r->argc; i++) {
if (r->args[i] < 0 || r->args[i] >= C4F_MAX_PADS) { c4fError(c, r, 400, "Invalid controller"); return; }
wanted |= 1u << r->args[i];
}
/* GoldHEN's AutoRun can start us before its klog server listens, and the
* connection can drop later. Reconnect when a new device needs it. */
if (app->klogFd < 0 && !app->creationBlocked)
for (int i = 0; i < C4F_MAX_PADS; i++)
if ((wanted & (1u << i)) && !app->pads[i].active) { app->klogFd = c4fKlogOpen(); break; }
/* Validate all claims before changing any ownership. */
for (int i = 0; i < C4F_MAX_PADS; i++) if (wanted & (1u << i)) {
C4fWebPad *p = &app->pads[i];
if (p->owner && p->owner != c) { c4fError(c, r, 409, "Controller is in use on another device"); return; }
if (!p->active && (app->klogFd < 0 || app->creationBlocked)) { c4fError(c, r, 503, "Controller creation unavailable; restart payload and check klog"); return; }
}
for (int i = 0; i < C4F_MAX_PADS; i++) if ((wanted & (1u << i)) && !app->pads[i].active) {
C4fWebPad *p = &app->pads[i];
c4fKlogDrain(app->klogFd); app->klogUsed = 0;
if (c4fVirtualPadAddAs(&p->device, C4F_USER_ID_INVALID, 1, app->klogFd)) {
/* AddDevice may have created an orphan whose handle was lost. Do
* not accumulate more devices through automatic retries. */
app->creationBlocked = 1;
for (int k = 0; k < C4F_MAX_PADS; k++) if (created & (1u << k)) c4fRemove(app, &app->pads[k]);
c4fError(c, r, 503, "Could not create controller; restart payload before retrying"); return;
}
p->active = 1; p->userId = 0xffffffffu; p->lastInput = c4fTimeMs();
c4fNeutralize(p); created |= 1u << i;
c4fReportPads(app);
}
for (int i = 0; i < C4F_MAX_PADS; i++) {
C4fWebPad *p = &app->pads[i];
if (wanted & (1u << i)) {
if (p->owner != c) c4fNeutralize(p);
p->owner = c; p->detachedAt = 0; p->lastInput = c4fTimeMs(); p->stale = 0;
} else if (p->owner == c) c4fRemove(app, p);
}
app->changed = 1; c4fStatus(app, c, r);
}
static void c4fUpdate(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
{
if (r->argc < 9 || r->args[0] < 0 || r->args[0] >= C4F_MAX_PADS) { c4fError(c, r, 400, "Invalid input"); return; }
C4fWebPad *p = &app->pads[r->args[0]];
if (p->owner != c || !p->active) return; /* input never implicitly claims a pad */
if (r->args[1] < 0 || r->args[1] > C4F_INPUT_MASK || (r->args[1] & ~C4F_INPUT_MASK)) { c4fError(c, r, 400, "Invalid buttons"); return; }
for (unsigned i = 2; i < 8; i++) if (r->args[i] < 0 || r->args[i] > 255) { c4fError(c, r, 400, "Invalid axis"); return; }
if (r->args[8] < 0 || r->args[8] > 2 || r->argc != 9+3*r->args[8]) { c4fError(c, r, 400, "Invalid touch data"); return; }
ScePadData data;
c4fPadDataNeutral(&data); data.buttons = (uint32_t)r->args[1];
data.lx = r->args[2]; data.ly = r->args[3]; data.rx = r->args[4]; data.ry = r->args[5]; data.l2 = r->args[6]; data.r2 = r->args[7];
data.touchData.fingers = r->args[8];
for (unsigned i = 0; i < data.touchData.fingers; i++) {
int64_t *t = &r->args[9+3*i];
if (t[0] < 0 || t[0] > 127 || t[1] < 0 || t[1] > 1919 || t[2] < 0 || t[2] > 941) { c4fError(c, r, 400, "Invalid touch position"); return; }
data.touchData.touch[i].finger = t[0]; data.touchData.touch[i].x = t[1]; data.touchData.touch[i].y = t[2];
}
if (p->stale) { p->stale = 0; app->changed = 1; }
p->lastInput = c4fTimeMs(); c4fEnqueue(p, &data);
}
static void c4fWebEvent(C4fNetClient *c, int event, const char *text, size_t len, void *context)
{
C4fWeb *app = context;
if (event == C4F_NET_HTTP_STATUS) {
int active = 0;
char reply[192];
for (int i = 0; i < C4F_MAX_PADS; i++) active += app->pads[i].active != 0;
snprintf(reply, sizeof(reply), "{\"application\":\"Control4Free\",\"api\":1,\"version\":\"%s\",\"controllers\":%d,\"stopping\":%s}", C4F_VERSION, active, app->stopAt ? "true" : "false");
c4fNetHttpJson(c, reply); return;
}
if (event == C4F_NET_HTTP_STOP) {
for (int i = 0; i < C4F_MAX_PADS; i++) c4fRemove(app, &app->pads[i]);
c4fNetHttpJson(c, "{\"application\":\"Control4Free\",\"stopping\":true}");
if (!app->stopAt) app->stopAt = c4fTimeMs() + 250;
return;
}
if (event == C4F_NET_OPEN) { c4fStatus(app, c, NULL); return; }
if (event == C4F_NET_CLOSE) {
for (int i = 0; i < C4F_MAX_PADS; i++) if (app->pads[i].owner == c) {
C4fWebPad *p = &app->pads[i];
c4fNeutralize(p); p->owner = NULL; p->detachedAt = c4fTimeMs(); app->changed = 1;
}
return;
}
C4fRequest r;
if (c4fParseRequest(text, len, &r)) { c4fError(c, NULL, 400, "Invalid request"); return; }
if (app->stopAt) { c4fError(c, &r, 503, "Control4Free is stopping"); return; }
if (!strcmp(r.method, "info")) {
char reply[160];
if (r.hasId) { snprintf(reply, sizeof(reply), "{\"id\":%lld,\"result\":{\"version\":\"%s\",\"protocol\":2,\"pads\":%d}}", (long long)r.id, C4F_VERSION, C4F_MAX_PADS); c4fNetText(c, reply); }
} else if (!strcmp(r.method, "status")) c4fStatus(app, c, &r);
else if (!strcmp(r.method, "claim")) c4fClaim(app, c, &r);
else if (!strcmp(r.method, "u")) c4fUpdate(app, c, &r);
else if (!strcmp(r.method, "leave")) {
if (r.argc != 1 || r.args[0] < 0 || r.args[0] >= C4F_MAX_PADS || app->pads[r.args[0]].owner != c) { c4fError(c, &r, 409, "You do not control this controller"); return; }
c4fRemove(app, &app->pads[r.args[0]]); c4fStatus(app, c, &r);
} else if (!strcmp(r.method, "stop")) {
int occupied = 0;
for (int i = 0; i < C4F_MAX_PADS; i++) if (app->pads[i].owner && app->pads[i].owner != c) occupied = 1;
if (occupied) { c4fError(c, &r, 409, "Another device is using a controller"); return; }
app->stop = 1;
} else c4fError(c, &r, 404, "Unknown method");
}
int c4fWebRun(int klogFd)
{
/* Keep network and pad queues off the payload thread's small stack. */
C4fWeb *app = calloc(1, sizeof(*app));
if (!app) { if (klogFd >= 0) close(klogFd); return -1; }
app->klogFd = klogFd;
if (c4fNetOpen(&app->net, C4F_WEB_PORT, c4fWebEvent, app)) {
c4fLog("web listen failed errno=%d\n", errno);
if (klogFd >= 0) close(klogFd);
free(app); return -1;
}
c4fVdaSetWaitCallback(c4fReportPads, app);
c4fLog("Control4Free %s: browser controller on port %d\n", C4F_VERSION, C4F_WEB_PORT);
c4fNotify("Control4Free: open PS4 IP:%d in your browser", C4F_WEB_PORT);
while (!app->stop) {
uint64_t now = c4fTimeMs();
if (app->stopAt && now >= app->stopAt) break;
c4fReadKlog(app); c4fReportPads(app);
for (int i = 0; i < C4F_MAX_PADS; i++) {
C4fWebPad *p = &app->pads[i];
if (p->active && ((!p->owner && now-p->detachedAt > C4F_RELEASE_MS) || (p->owner && now-p->lastInput > C4F_RELEASE_MS))) {
C4fNetClient *owner = p->owner;
c4fRemove(app, p);
if (owner) c4fError(owner, NULL, 408, "Controller disconnected after inactivity; select it again");
}
}
if (app->changed || now >= app->broadcastAt) {
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
C4fNetClient *c = &app->net.clients[i];
if (c->fd >= 0 && c->websocket && !c->closing) c4fStatus(app, c, NULL);
}
app->changed = 0; app->broadcastAt = now+1000;
}
c4fNetPoll(&app->net, 8);
}
c4fVdaSetWaitCallback(NULL, NULL);
for (int i = 0; i < C4F_MAX_PADS; i++) c4fRemove(app, &app->pads[i]);
c4fNetClose(&app->net);
if (app->klogFd >= 0) close(app->klogFd);
free(app);
c4fLog("Control4Free web controller stopped\n");
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
"""Deterministically embed the standalone phone client in the payload."""
import gzip
from pathlib import Path
import sys
source, output = map(Path, sys.argv[1:])
data = gzip.compress(source.read_bytes(), compresslevel=9, mtime=0)
lines = ["/* Generated by tools/embed_client.py. */", "#include <stddef.h>",
"const unsigned char c4fPage[] = {"]
lines.extend(" " + ",".join(str(n) for n in data[i:i+24]) + "," for i in range(0, len(data), 24))
lines.extend(["};", "const size_t c4fPageSize = sizeof(c4fPage);", ""])
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("\n".join(lines))
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2010 Serge A. Zaitsev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+471
View File
@@ -0,0 +1,471 @@
/*
* MIT License
*
* Copyright (c) 2010 Serge Zaitsev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef JSMN_H
#define JSMN_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef JSMN_STATIC
#define JSMN_API static
#else
#define JSMN_API extern
#endif
/**
* JSON type identifier. Basic types are:
* o Object
* o Array
* o String
* o Other primitive: number, boolean (true/false) or null
*/
typedef enum {
JSMN_UNDEFINED = 0,
JSMN_OBJECT = 1 << 0,
JSMN_ARRAY = 1 << 1,
JSMN_STRING = 1 << 2,
JSMN_PRIMITIVE = 1 << 3
} jsmntype_t;
enum jsmnerr {
/* Not enough tokens were provided */
JSMN_ERROR_NOMEM = -1,
/* Invalid character inside JSON string */
JSMN_ERROR_INVAL = -2,
/* The string is not a full JSON packet, more bytes expected */
JSMN_ERROR_PART = -3
};
/**
* JSON token description.
* type type (object, array, string etc.)
* start start position in JSON data string
* end end position in JSON data string
*/
typedef struct jsmntok {
jsmntype_t type;
int start;
int end;
int size;
#ifdef JSMN_PARENT_LINKS
int parent;
#endif
} jsmntok_t;
/**
* JSON parser. Contains an array of token blocks available. Also stores
* the string being parsed now and current position in that string.
*/
typedef struct jsmn_parser {
unsigned int pos; /* offset in the JSON string */
unsigned int toknext; /* next token to allocate */
int toksuper; /* superior token node, e.g. parent object or array */
} jsmn_parser;
/**
* Create JSON parser over an array of tokens
*/
JSMN_API void jsmn_init(jsmn_parser *parser);
/**
* Run JSON parser. It parses a JSON data string into and array of tokens, each
* describing
* a single JSON object.
*/
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
jsmntok_t *tokens, const unsigned int num_tokens);
#ifndef JSMN_HEADER
/**
* Allocates a fresh unused token from the token pool.
*/
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *tok;
if (parser->toknext >= num_tokens) {
return NULL;
}
tok = &tokens[parser->toknext++];
tok->start = tok->end = -1;
tok->size = 0;
#ifdef JSMN_PARENT_LINKS
tok->parent = -1;
#endif
return tok;
}
/**
* Fills token type and boundaries.
*/
static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
const int start, const int end) {
token->type = type;
token->start = start;
token->end = end;
token->size = 0;
}
/**
* Fills next available token with JSON primitive.
*/
static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
const size_t len, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *token;
int start;
start = parser->pos;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
switch (js[parser->pos]) {
#ifndef JSMN_STRICT
/* In strict mode primitive must be followed by "," or "}" or "]" */
case ':':
#endif
case '\t':
case '\r':
case '\n':
case ' ':
case ',':
case ']':
case '}':
goto found;
default:
/* to quiet a warning from gcc*/
break;
}
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
#ifdef JSMN_STRICT
/* In strict mode primitive must be followed by a comma/object/array */
parser->pos = start;
return JSMN_ERROR_PART;
#endif
found:
if (tokens == NULL) {
parser->pos--;
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
parser->pos--;
return 0;
}
/**
* Fills next token with JSON string.
*/
static int jsmn_parse_string(jsmn_parser *parser, const char *js,
const size_t len, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *token;
int start = parser->pos;
/* Skip starting quote */
parser->pos++;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c = js[parser->pos];
/* Quote: end of string */
if (c == '\"') {
if (tokens == NULL) {
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
return 0;
}
/* Backslash: Quoted symbol expected */
if (c == '\\' && parser->pos + 1 < len) {
int i;
parser->pos++;
switch (js[parser->pos]) {
/* Allowed escaped symbols */
case '\"':
case '/':
case '\\':
case 'b':
case 'f':
case 'r':
case 'n':
case 't':
break;
/* Allows escaped symbol \uXXXX */
case 'u':
parser->pos++;
for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0';
i++) {
/* If it isn't a hex character we have an error */
if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
(js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
(js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
parser->pos = start;
return JSMN_ERROR_INVAL;
}
parser->pos++;
}
parser->pos--;
break;
/* Unexpected symbol */
default:
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
}
parser->pos = start;
return JSMN_ERROR_PART;
}
/**
* Parse JSON string and fill tokens.
*/
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
jsmntok_t *tokens, const unsigned int num_tokens) {
int r;
int i;
jsmntok_t *token;
int count = parser->toknext;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c;
jsmntype_t type;
c = js[parser->pos];
switch (c) {
case '{':
case '[':
count++;
if (tokens == NULL) {
break;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
return JSMN_ERROR_NOMEM;
}
if (parser->toksuper != -1) {
jsmntok_t *t = &tokens[parser->toksuper];
#ifdef JSMN_STRICT
/* In strict mode an object or array can't become a key */
if (t->type == JSMN_OBJECT) {
return JSMN_ERROR_INVAL;
}
#endif
t->size++;
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
}
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
token->start = parser->pos;
parser->toksuper = parser->toknext - 1;
break;
case '}':
case ']':
if (tokens == NULL) {
break;
}
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
#ifdef JSMN_PARENT_LINKS
if (parser->toknext < 1) {
return JSMN_ERROR_INVAL;
}
token = &tokens[parser->toknext - 1];
for (;;) {
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
token->end = parser->pos + 1;
parser->toksuper = token->parent;
break;
}
if (token->parent == -1) {
if (token->type != type || parser->toksuper == -1) {
return JSMN_ERROR_INVAL;
}
break;
}
token = &tokens[token->parent];
}
#else
for (i = parser->toknext - 1; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
parser->toksuper = -1;
token->end = parser->pos + 1;
break;
}
}
/* Error if unmatched closing bracket */
if (i == -1) {
return JSMN_ERROR_INVAL;
}
for (; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
parser->toksuper = i;
break;
}
}
#endif
break;
case '\"':
r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
if (r < 0) {
return r;
}
count++;
if (parser->toksuper != -1 && tokens != NULL) {
tokens[parser->toksuper].size++;
}
break;
case '\t':
case '\r':
case '\n':
case ' ':
break;
case ':':
parser->toksuper = parser->toknext - 1;
break;
case ',':
if (tokens != NULL && parser->toksuper != -1 &&
tokens[parser->toksuper].type != JSMN_ARRAY &&
tokens[parser->toksuper].type != JSMN_OBJECT) {
#ifdef JSMN_PARENT_LINKS
parser->toksuper = tokens[parser->toksuper].parent;
#else
for (i = parser->toknext - 1; i >= 0; i--) {
if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
if (tokens[i].start != -1 && tokens[i].end == -1) {
parser->toksuper = i;
break;
}
}
}
#endif
}
break;
#ifdef JSMN_STRICT
/* In strict mode primitives are: numbers and booleans */
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 't':
case 'f':
case 'n':
/* And they must not be keys of the object */
if (tokens != NULL && parser->toksuper != -1) {
const jsmntok_t *t = &tokens[parser->toksuper];
if (t->type == JSMN_OBJECT ||
(t->type == JSMN_STRING && t->size != 0)) {
return JSMN_ERROR_INVAL;
}
}
#else
/* In non-strict mode every unquoted value is a primitive */
default:
#endif
r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
if (r < 0) {
return r;
}
count++;
if (parser->toksuper != -1 && tokens != NULL) {
tokens[parser->toksuper].size++;
}
break;
#ifdef JSMN_STRICT
/* Unexpected char in strict mode */
default:
return JSMN_ERROR_INVAL;
#endif
}
}
if (tokens != NULL) {
for (i = parser->toknext - 1; i >= 0; i--) {
/* Unmatched opened object or array */
if (tokens[i].start != -1 && tokens[i].end == -1) {
return JSMN_ERROR_PART;
}
}
}
return count;
}
/**
* Creates a new parser based over a given buffer with an array of tokens
* available.
*/
JSMN_API void jsmn_init(jsmn_parser *parser) {
parser->pos = 0;
parser->toknext = 0;
parser->toksuper = -1;
}
#endif /* JSMN_HEADER */
#ifdef __cplusplus
}
#endif
#endif /* JSMN_H */