mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-06 10:00:28 +02:00
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:
commit
6d931a05ce
21 files changed
+6840
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
build/
|
||||
*.elf
|
||||
*.o
|
||||
*.d
|
||||
__pycache__/
|
||||
@@ -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>.
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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(¶m, 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(¶m, 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
Vendored
+20
@@ -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.
|
||||
|
||||
Vendored
+471
@@ -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 */
|
||||
Reference in new issue
Block a user