Working version
This commit is contained in:
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
## WVGuesserExtension-NextGen
|
||||
|
||||
#### For more details, see [docs](https://github.com/FoxRefire/wvg/wiki)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Download or clone this code
|
||||
2. At the same directory of `manifest.json`(root directory of this extension), put the one of the following Android L3 CDM key pair file(s).
|
||||
|
||||
*Don't know how to get these files? See [How to dump CDM key pair](https://github.com/FoxRefire/wvg/wiki/How-to-dump-CDM-key-pair) for more informations.*
|
||||
|
||||
* Supported CDM Key Pair Formats
|
||||
|
||||
1. `device.wvd`
|
||||
|
||||
2. `device_client_id_blob` + `device_private_key`
|
||||
|
||||
3. `client_id.bin` + `private_key.pem`
|
||||
|
||||
4. `remote.json` ([How to use Remote CDM](https://github.com/FoxRefire/wvg/wiki/Using-with-Remote-CDM))
|
||||
3. Install extension
|
||||
|
||||
* Firefox ([Permanent method](https://github.com/FoxRefire/wvg/wiki/Permanent-install-method-for-Firefox))
|
||||
|
||||
1\. Navigate to `about:debugging#/runtime/this-firefox`
|
||||
|
||||
2\. Load temporary addon
|
||||
|
||||
* Chrome
|
||||
|
||||
1\. Navigate to `chrome://extensions/`
|
||||
|
||||
2\. Load unpacked
|
||||
|
||||
* Kiwi Browser(Android)
|
||||
|
||||
1\. Navigate to ︙ --> Extensions
|
||||
|
||||
2\. \+(from .zip/.crx/.user.js)
|
||||
|
||||
### Demo
|
||||
[Screencast_20240505_014046.webm](https://github.com/FoxRefire/wvg/assets/155989196/dbb07fde-a368-40f7-8209-711d5586009e)
|
||||
|
||||
## Third-party libraries
|
||||
* [Pyodide](https://github.com/pyodide/pyodide) ([MPL-2.0](https://github.com/pyodide/pyodide/blob/main/LICENSE))
|
||||
* [Pywidevine](https://github.com/devine-dl/pywidevine) ([GPL-3.0](https://github.com/devine-dl/pywidevine/blob/master/LICENSE))
|
||||
* [json-view](https://github.com/pgrabovets/json-view) ([MIT](https://github.com/pgrabovets/json-view/blob/master/LICENSE))
|
||||
|
||||
### Big Thanks and inspired by
|
||||
https://github.com/emarsden/pssh-box-wasm/
|
||||
|
||||
150
background.js
Normal file
150
background.js
Normal file
@@ -0,0 +1,150 @@
|
||||
(async () => {
|
||||
window.psshs=[];
|
||||
window.requests=[];
|
||||
window.bodys=[];
|
||||
window.targetIds=[];
|
||||
window.pageURL="";
|
||||
window.clearkey="";
|
||||
// Ajouter un tableau pour stocker les MPD
|
||||
window.detectedMPDs = window.detectedMPDs || [];
|
||||
|
||||
chrome.storage.local.get("isBlock", (value) => {
|
||||
window.isBlock = value.isBlock;
|
||||
})
|
||||
|
||||
function convertHeaders(obj){
|
||||
return JSON.stringify(Object.fromEntries(obj.map(header => [header.name, header.value])))
|
||||
}
|
||||
|
||||
window.blockRules = await fetch("blockRules.conf").then((r)=>r.text());
|
||||
window.blockRules = window.blockRules.replace(/\n^\s*$|\s*\/\/.*|\s*$/gm, "").split("\n");
|
||||
|
||||
function testBlock(url) {
|
||||
return window.isBlock && window.blockRules.some(e => url.includes(e));
|
||||
}
|
||||
|
||||
// Fonction badge simple
|
||||
function updateBadge() {
|
||||
const browserAction = chrome.action || chrome.browserAction;
|
||||
if (!browserAction) return;
|
||||
|
||||
const hasContent = (window.psshs && window.psshs.length > 0) || (window.clearkey && window.clearkey.length > 0);
|
||||
const hasMPD = window.detectedMPDs && window.detectedMPDs.length > 0;
|
||||
|
||||
if (hasContent && hasMPD) {
|
||||
browserAction.setBadgeText({ text: "●" });
|
||||
browserAction.setBadgeBackgroundColor({ color: "#dc3545" });
|
||||
} else if (hasContent) {
|
||||
browserAction.setBadgeText({ text: "!" });
|
||||
browserAction.setBadgeBackgroundColor({ color: "#fd7e14" });
|
||||
} else {
|
||||
browserAction.setBadgeText({ text: "" });
|
||||
}
|
||||
}
|
||||
|
||||
//Get URL and headers from POST requests
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
function(details) {
|
||||
if (details.method === "POST") {
|
||||
window.requests.push({
|
||||
url:details.url,
|
||||
headers:convertHeaders(details.requestHeaders),
|
||||
body:window.bodys.find((b) => b.id == details.requestId).body
|
||||
});
|
||||
if(testBlock(details.url)){
|
||||
return {cancel:true}
|
||||
}
|
||||
}
|
||||
},
|
||||
{urls: ["<all_urls>"]},
|
||||
["requestHeaders", "blocking"]
|
||||
);
|
||||
|
||||
//Get requestBody from POST requests
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
function(details) {
|
||||
// Ton code existant pour les POST
|
||||
if (details.method === "POST") {
|
||||
window.bodys.push({
|
||||
body:details.requestBody.raw ? btoa(String.fromCharCode(...new Uint8Array(details.requestBody.raw[0]['bytes']))) : "",
|
||||
id:details.requestId
|
||||
});
|
||||
}
|
||||
|
||||
// Nouveau : capturer les MPD
|
||||
if (details.url.includes('.mpd') ||
|
||||
details.url.includes('manifest') ||
|
||||
details.url.includes('dash.mpd') ||
|
||||
details.url.match(/\.(mpd|m3u8)(\?|$)/)) {
|
||||
|
||||
if (!window.detectedMPDs.includes(details.url)) {
|
||||
window.detectedMPDs.push(details.url);
|
||||
console.log('MPD detected:', details.url);
|
||||
updateBadge(); // Mettre à jour badge
|
||||
}
|
||||
}
|
||||
},
|
||||
{urls: ["<all_urls>"]},
|
||||
["requestBody"]
|
||||
);
|
||||
|
||||
//Receive PSSH from content.js
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function (request, sender, sendResponse) {
|
||||
switch(request.type){
|
||||
case "RESET":
|
||||
location.reload()
|
||||
break;
|
||||
case "PSSH":
|
||||
window.psshs.push(request.text)
|
||||
window.pageURL=sender.tab.url
|
||||
window.targetIds=[sender.tab.id, sender.frameId]
|
||||
updateBadge(); // Mettre à jour badge
|
||||
break;
|
||||
case "CLEARKEY":
|
||||
window.clearkey=request.text
|
||||
updateBadge(); // Mettre à jour badge
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} )()
|
||||
|
||||
chrome.browserAction.onClicked.addListener(tab => {
|
||||
if(chrome.windows){
|
||||
chrome.windows.create({
|
||||
url: "popup/main.html",
|
||||
type: "popup",
|
||||
width: 1200,
|
||||
height: 800
|
||||
});
|
||||
} else {
|
||||
chrome.tabs.create({url: 'popup/main.html'})
|
||||
}
|
||||
});
|
||||
|
||||
function createMenu(){
|
||||
chrome.storage.local.set({'isBlock': false}, null);
|
||||
chrome.contextMenus.create({
|
||||
id: "toggleBlocking",
|
||||
title: "Enable License Blocking"
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(createMenu)
|
||||
chrome.runtime.onStartup.addListener(createMenu)
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(item => {
|
||||
if(item.menuItemId == "toggleBlocking"){
|
||||
chrome.storage.local.get("isBlock", (value) => {
|
||||
if(value.isBlock){
|
||||
chrome.storage.local.set({'isBlock': false}, null);
|
||||
chrome.contextMenus.update("toggleBlocking",{title: "Enable License Blocking"})
|
||||
} else {
|
||||
chrome.storage.local.set({'isBlock': true}, null);
|
||||
chrome.contextMenus.update("toggleBlocking",{title: "Disable License Blocking"})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
6
blockRules.conf
Normal file
6
blockRules.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
api.fantop.jp/api/v1/drm/widevine-license
|
||||
drm.lemino.docomo.ne.jp/widevine_license
|
||||
license.vdocipher.com
|
||||
ngenix.net/license
|
||||
shield-drm.imggaming.com/api/v2/license
|
||||
wvls/contentlicenseservice/v1/licenses
|
||||
BIN
client_id.bin
Normal file
BIN
client_id.bin
Normal file
Binary file not shown.
45
content.js
Normal file
45
content.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.defer = false;
|
||||
script.async = false;
|
||||
script.src = chrome.runtime.getURL("inject.js");
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
//Reset variables at every page load in background.js
|
||||
if (window === window.parent){
|
||||
chrome.runtime.sendMessage({type: "RESET"},null);
|
||||
}
|
||||
|
||||
//Send PSSH into background.js
|
||||
document.addEventListener('pssh', (e) => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "PSSH",
|
||||
text: e.detail
|
||||
},null);
|
||||
});
|
||||
|
||||
//Send Clearkey into background.js
|
||||
document.addEventListener('clearkey', (e) => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "CLEARKEY",
|
||||
text: e.detail
|
||||
},null);
|
||||
});
|
||||
|
||||
//Fetch from original origin
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function (request, sender, sendResponse) {
|
||||
if(request.type=="FETCH"){
|
||||
let res = fetch(request.u, {
|
||||
method: request.m,
|
||||
headers: JSON.parse(request.h),
|
||||
body: Uint8Array.from(atob(request.b), c => c.charCodeAt(0))
|
||||
}).then((r)=>r.arrayBuffer()).then((r)=>{
|
||||
sendResponse(
|
||||
btoa(String.fromCharCode(...new Uint8Array(r)))
|
||||
);
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
);
|
||||
50
inject.js
Normal file
50
inject.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Refactored conversion functions
|
||||
const hexStrToU8 = hexString => Uint8Array.from(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
||||
|
||||
const u8ToHexStr = bytes => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
||||
|
||||
const b64ToHexStr = b64 => [...atob(b64)].map(c=> c.charCodeAt(0).toString(16).padStart(2,0)).join``
|
||||
|
||||
// initData to PSSH
|
||||
function getPssh(buffer) {
|
||||
const bytes = hexStrToU8(u8ToHexStr(new Uint8Array(buffer)).match(/000000..70737368.*/)[0]);
|
||||
return window.btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
// Get Clearkey keys
|
||||
function getClearkey(response) {
|
||||
let obj=JSON.parse((new TextDecoder("utf-8")).decode(response))
|
||||
obj = obj["keys"].map(o => [o["kid"], o["k"]]);
|
||||
obj = obj.map(o => o.map(a => a.replace(/-/g, '+').replace(/_/g, '/')+"=="))
|
||||
return obj.map(o => `${b64ToHexStr(o[0])}:${b64ToHexStr(o[1])}`).join("\n")
|
||||
|
||||
}
|
||||
|
||||
// Widevine PSSH extraction from init
|
||||
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
|
||||
MediaKeySession.prototype.generateRequest = function(initDataType, initData) {
|
||||
const result = originalGenerateRequest.call(this, initDataType, initData);
|
||||
//Get PSSH and pass into content.js
|
||||
try {
|
||||
console.log("[PSSH] " + getPssh(initData))
|
||||
document.dispatchEvent(new CustomEvent('pssh', {
|
||||
detail: getPssh(initData)
|
||||
}));
|
||||
} finally {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
//Clearkey Support
|
||||
const originalUpdate = MediaKeySession.prototype.update;
|
||||
MediaKeySession.prototype.update = function(response) {
|
||||
const result = originalUpdate.call(this, response);
|
||||
try {
|
||||
console.log("[CLEARKEY] " + getClearkey(response));
|
||||
document.dispatchEvent(new CustomEvent('clearkey', {
|
||||
detail: getClearkey(response)
|
||||
}));
|
||||
} finally {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
1
libs/jsonview.js
Normal file
1
libs/jsonview.js
Normal file
File diff suppressed because one or more lines are too long
134
libs/pyodide/package.json
Normal file
134
libs/pyodide/package.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"name": "pyodide",
|
||||
"version": "0.25.1",
|
||||
"description": "The Pyodide JavaScript package",
|
||||
"keywords": [
|
||||
"python",
|
||||
"webassembly"
|
||||
],
|
||||
"homepage": "https://github.com/pyodide/pyodide",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pyodide/pyodide"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pyodide/pyodide/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/assert": "^1.5.6",
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/ws": "^8.5.3",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dts-bundle-generator": "^8.1.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"esbuild": "^0.17.12",
|
||||
"express": "^4.17.3",
|
||||
"mocha": "^9.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.1",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"tsd": "^0.24.1",
|
||||
"typedoc": "^0.25.1",
|
||||
"typescript": "^4.6.4",
|
||||
"wabt": "^1.0.32"
|
||||
},
|
||||
"main": "pyodide.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./pyodide.js",
|
||||
"import": "./pyodide.mjs",
|
||||
"types": "./pyodide.d.ts"
|
||||
},
|
||||
"./ffi": {
|
||||
"types": "./ffi.d.ts"
|
||||
},
|
||||
"./pyodide.asm.wasm": "./pyodide.asm.wasm",
|
||||
"./pyodide.asm.js": "./pyodide.asm.js",
|
||||
"./python_stdlib.zip": "./python_stdlib.zip",
|
||||
"./pyodide.mjs": "./pyodide.mjs",
|
||||
"./pyodide.js": "./pyodide.js",
|
||||
"./package.json": "./package.json",
|
||||
"./pyodide-lock.json": "./pyodide-lock.json"
|
||||
},
|
||||
"files": [
|
||||
"pyodide.asm.js",
|
||||
"pyodide.asm.wasm",
|
||||
"python_stdlib.zip",
|
||||
"pyodide.mjs",
|
||||
"pyodide.js.map",
|
||||
"pyodide.mjs.map",
|
||||
"pyodide.d.ts",
|
||||
"ffi.d.ts",
|
||||
"pyodide-lock.json",
|
||||
"console.html"
|
||||
],
|
||||
"browser": {
|
||||
"child_process": false,
|
||||
"crypto": false,
|
||||
"fs": false,
|
||||
"fs/promises": false,
|
||||
"path": false,
|
||||
"url": false,
|
||||
"vm": false,
|
||||
"ws": false
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && node esbuild.config.mjs",
|
||||
"test": "npm-run-all test:*",
|
||||
"test:unit": "cross-env TEST_NODE=1 ts-mocha --node-option=experimental-loader=./test/loader.mjs --node-option=experimental-wasm-stack-switching -p tsconfig.test.json test/unit/**/*.test.*",
|
||||
"test:node": "cross-env TEST_NODE=1 mocha test/integration/**/*.test.js",
|
||||
"test:browser": "mocha test/integration/**/*.test.js",
|
||||
"tsc": "tsc --noEmit",
|
||||
"coverage": "cross-env TEST_NODE=1 npm-run-all coverage:*",
|
||||
"coverage:build": "nyc npm run test:node"
|
||||
},
|
||||
"mocha": {
|
||||
"bail": false,
|
||||
"timeout": 30000,
|
||||
"full-trace": true,
|
||||
"inline-diffs": true,
|
||||
"check-leaks": false,
|
||||
"global": [
|
||||
"pyodide",
|
||||
"page",
|
||||
"chai"
|
||||
]
|
||||
},
|
||||
"nyc": {
|
||||
"reporter": [
|
||||
"html",
|
||||
"text-summary"
|
||||
],
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"all": true,
|
||||
"clean": true,
|
||||
"cache": false,
|
||||
"instrument": false,
|
||||
"checkCoverage": true,
|
||||
"statements": 95,
|
||||
"functions": 95,
|
||||
"branches": 80,
|
||||
"lines": 95
|
||||
},
|
||||
"tsd": {
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"DOM"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"base-64": "^1.0.0",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"types": "./pyodide.d.ts"
|
||||
}
|
||||
1
libs/pyodide/pyodide-lock.json
Normal file
1
libs/pyodide/pyodide-lock.json
Normal file
File diff suppressed because one or more lines are too long
17
libs/pyodide/pyodide.asm.js
Normal file
17
libs/pyodide/pyodide.asm.js
Normal file
File diff suppressed because one or more lines are too long
BIN
libs/pyodide/pyodide.asm.wasm
Executable file
BIN
libs/pyodide/pyodide.asm.wasm
Executable file
Binary file not shown.
12
libs/pyodide/pyodide.js
Normal file
12
libs/pyodide/pyodide.js
Normal file
File diff suppressed because one or more lines are too long
10
libs/pyodide/pyodide.mjs
Normal file
10
libs/pyodide/pyodide.mjs
Normal file
File diff suppressed because one or more lines are too long
BIN
libs/pyodide/python_stdlib.zip
Normal file
BIN
libs/pyodide/python_stdlib.zip
Normal file
Binary file not shown.
BIN
libs/wheels/certifi-2024.2.2-py3-none-any.whl
Normal file
BIN
libs/wheels/certifi-2024.2.2-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/charset_normalizer-3.3.2-py3-none-any.whl
Normal file
BIN
libs/wheels/charset_normalizer-3.3.2-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/construct-2.8.8-py2.py3-none-any.whl
Normal file
BIN
libs/wheels/construct-2.8.8-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/idna-3.6-py3-none-any.whl
Normal file
BIN
libs/wheels/idna-3.6-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/packaging-23.2-py3-none-any.whl
Normal file
BIN
libs/wheels/packaging-23.2-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
libs/wheels/pymp4-1.4.0-py3-none-any.whl
Normal file
BIN
libs/wheels/pymp4-1.4.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/pyodide_http-0.2.1-py3-none-any.whl
Normal file
BIN
libs/wheels/pyodide_http-0.2.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/pywidevine-1.8.0-py3-none-any.whl
Normal file
BIN
libs/wheels/pywidevine-1.8.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/requests-2.31.0-py3-none-any.whl
Normal file
BIN
libs/wheels/requests-2.31.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
libs/wheels/urllib3-2.2.1-py3-none-any.whl
Normal file
BIN
libs/wheels/urllib3-2.2.1-py3-none-any.whl
Normal file
Binary file not shown.
35
manifest.json
Normal file
35
manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Widevine L3 Decrypter",
|
||||
"version": "241029.1",
|
||||
"version_name": "4316530",
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"storage",
|
||||
"tabs",
|
||||
"contextMenus"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at":"document_start",
|
||||
"matches":["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
"default_title": "Widevine Decrypter"
|
||||
},
|
||||
"web_accessible_resources": ["inject.js"],
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
40
popup/drawList.js
Normal file
40
popup/drawList.js
Normal file
@@ -0,0 +1,40 @@
|
||||
let psshs=chrome.extension.getBackgroundPage().psshs;
|
||||
let requests=chrome.extension.getBackgroundPage().requests;
|
||||
var userInputs={};
|
||||
|
||||
document.getElementById('psshButton').addEventListener("click", () => drawList(psshs, 'pssh'));
|
||||
document.getElementById('licenseButton').addEventListener("click", () => drawList(requests.map(r => r['url']), 'license'));
|
||||
|
||||
function writeListElement(items, outputVar, searchStr) {
|
||||
document.getElementById("items").innerHTML = '';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (!searchStr || item.includes(searchStr)) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = item;
|
||||
li.addEventListener('click', () => itemSelected(index, item, outputVar));
|
||||
document.getElementById("items").appendChild(li);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawList(items, outputVar) {
|
||||
document.getElementById('home').style.display='none';
|
||||
document.getElementById('chooserContainer').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='none';
|
||||
|
||||
writeListElement(items, outputVar, null)
|
||||
document.getElementById("chooserSearch").addEventListener('input', event => {
|
||||
const searchStr = event.target.value.toLowerCase();
|
||||
writeListElement(items, outputVar, searchStr)
|
||||
});
|
||||
}
|
||||
|
||||
function itemSelected(index, item, outputVar){
|
||||
userInputs[outputVar]=index;
|
||||
document.getElementById(outputVar).value=item;
|
||||
document.getElementById('chooserContainer').style.display='none';
|
||||
document.getElementById('home').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='grid';
|
||||
document.getElementById("chooserSearch").value=""
|
||||
}
|
||||
15
popup/editScheme.js
Normal file
15
popup/editScheme.js
Normal file
@@ -0,0 +1,15 @@
|
||||
document.getElementById("editSchemeButton").addEventListener("click", () => {
|
||||
document.getElementById("editSchemeContainer").style.display = "grid"
|
||||
document.getElementById('home').style.display='none';
|
||||
document.getElementById('toggleHistory').style.display='none';
|
||||
})
|
||||
|
||||
document.getElementById("editSchemeOK").addEventListener("click", () => {
|
||||
document.getElementById("editSchemeContainer").style.display = "none"
|
||||
document.getElementById('home').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='grid';
|
||||
})
|
||||
|
||||
document.getElementById("schemeSelect").addEventListener("input", async () => {
|
||||
document.getElementById("schemeCode").value = await fetch(`/python/schemes/${document.getElementById("schemeSelect").value}.py`).then(res=>res.text())
|
||||
})
|
||||
17
popup/history.html
Normal file
17
popup/history.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Widevine L3 Guessor - History</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="history">
|
||||
<a href="./main.html"><button>🔙 Back</button></a>
|
||||
<button id="saveHistory">💾 Save History</button>
|
||||
<button id="clearHistory">❌ Clear History</button>
|
||||
<div id="histDisp"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="/libs/jsonview.js"></script>
|
||||
<script src="./history.js"></script>
|
||||
</html>
|
||||
28
popup/history.js
Normal file
28
popup/history.js
Normal file
@@ -0,0 +1,28 @@
|
||||
let psshs=chrome.extension.getBackgroundPage().psshs;
|
||||
function showHistory(){
|
||||
chrome.storage.local.get(null, (data => {
|
||||
let tree=jsonview.renderJSON(JSON.stringify(data), document.getElementById('histDisp'));
|
||||
jsonview.toggleNode(tree);
|
||||
}));
|
||||
}
|
||||
|
||||
function saveHistory(){
|
||||
chrome.storage.local.get(null, (data => {
|
||||
let blob = new Blob([JSON.stringify(data, null, "\t")], {type: "text/plain"});
|
||||
let a = document.createElement('a');
|
||||
a.download = 'wvgHistory.json';
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.click();
|
||||
}));
|
||||
}
|
||||
|
||||
function clearHistory(){
|
||||
if(confirm("Do you really want to clear history?")){
|
||||
chrome.storage.local.clear();
|
||||
document.getElementById('histDisp').innerHTML="";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('saveHistory').addEventListener("click", saveHistory);
|
||||
document.getElementById('clearHistory').addEventListener("click", clearHistory);
|
||||
showHistory()
|
||||
312
popup/main.html
Normal file
312
popup/main.html
Normal file
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Widevine L3 Guessor 2024</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<script src="/libs/pyodide/pyodide.js"></script>
|
||||
<style>
|
||||
body { min-height: 100vh; }
|
||||
.section-card { margin-bottom: 1.5rem; }
|
||||
.main-container { max-width: 1200px; }
|
||||
.extraction-section { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); }
|
||||
.crawlflix-section { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); }
|
||||
.clearkey-section { background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 20%); }
|
||||
.form-label { font-weight: 600; }
|
||||
.card-header h5 { margin: 0; }
|
||||
.status-area { min-height: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container main-container p-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="text-center">
|
||||
<h2 class="mb-3"><i class="fas fa-shield-alt text-primary"></i> Widevine L3 Guessor 2024</h2>
|
||||
<p class="text-muted mb-3">Extract Widevine keys and send directly to CrawlFlix for seamless content processing</p>
|
||||
<a href="./history.html" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-history"></i> Show History
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No EME Detection -->
|
||||
<div id="noEME" class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning text-center py-4">
|
||||
<h4><i class="fas fa-exclamation-triangle"></i> No Widevine Content Detected</h4>
|
||||
<p class="mb-0">Open a widevine-protected website and try again!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div id="home" class="hidden">
|
||||
<div class="row">
|
||||
<!-- Key Extraction Column -->
|
||||
<div class="col-lg-6 col-xl-5">
|
||||
<div class="card section-card extraction-section">
|
||||
<div class="card-header py-3">
|
||||
<h5><i class="fas fa-key text-success"></i> Key Extraction</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form id="wvForm">
|
||||
<div class="mb-3">
|
||||
<label for="pssh" class="form-label">PSSH</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="pssh" class="form-control font-monospace" disabled>
|
||||
<button type="button" id="psshButton" class="btn btn-outline-primary">
|
||||
<i class="fas fa-mouse-pointer"></i> Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="license" class="form-label">License URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="license" class="form-control font-monospace" disabled>
|
||||
<button type="button" id="licenseButton" class="btn btn-outline-primary">
|
||||
<i class="fas fa-mouse-pointer"></i> Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-8">
|
||||
<label for="schemeSelect" class="form-label">Challenge Scheme</label>
|
||||
<select id="schemeSelect" class="form-select">
|
||||
<option value="Amazon">Amazon</option>
|
||||
<option value="Allente">Allente</option>
|
||||
<option value="CanalPlusVOD">CanalPlus (VOD)</option>
|
||||
<option value="CanalPlusLive">CanalPlus (Live)</option>
|
||||
<option value="Comcast">Comcast Xfinity</option>
|
||||
<option value="CommonWV" selected>CommonWV</option>
|
||||
<option value="DRMToday">DRMToday</option>
|
||||
<option value="Fantop">Fantop</option>
|
||||
<option value="GlobalTV">GlobalTV</option>
|
||||
<option value="Heuristic">Heuristic</option>
|
||||
<option value="moTV">moTV</option>
|
||||
<option value="NosTV">NosTV</option>
|
||||
<option value="oqee">Oqee</option>
|
||||
<option value="PolSatBoxGo">PolSatBoxGo</option>
|
||||
<option value="RedBee">Red Bee Media</option>
|
||||
<option value="Sling">Sling</option>
|
||||
<option value="thePlatform">thePlatform</option>
|
||||
<option value="VdoCipher">VdoCipher</option>
|
||||
<option value="VUDRM">VUDRM</option>
|
||||
<option value="Vodafone">Vodafone</option>
|
||||
<option value="Youku">Youku</option>
|
||||
<option value="YouTube">YouTube</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-end">
|
||||
<button type="button" id="editSchemeButton" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="guess" class="btn btn-primary btn-lg w-100 mb-4">
|
||||
<i class="fas fa-magic me-2"></i> Extract Widevine Keys
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<label for="result" class="form-label">Extracted Keys</label>
|
||||
<textarea id="result" class="form-control font-monospace" rows="8"
|
||||
placeholder="Extracted keys will appear here..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CrawlFlix Integration Column -->
|
||||
<div class="col-lg-6 col-xl-7">
|
||||
<div class="card section-card crawlflix-section">
|
||||
<div class="card-header py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-download text-info"></i> CrawlFlix Integration</h5>
|
||||
<span class="badge bg-info">Auto-Workflow</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="crawlFlixUrl" class="form-label">
|
||||
<i class="fas fa-server"></i> CrawlFlix Server
|
||||
</label>
|
||||
<input type="text" id="crawlFlixUrl" class="form-control"
|
||||
placeholder="http://localhost:3000" value="http://localhost:3000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="mpdSelect" class="form-label">
|
||||
<i class="fas fa-file-video"></i> Detected MPD Manifests
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<select id="mpdSelect" class="form-select">
|
||||
<option value="">-- Auto-detected MPDs --</option>
|
||||
</select>
|
||||
<button type="button" id="refreshMPDs" class="btn btn-success">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="mpdUrl" class="form-label">MPD URL (Manual Entry)</label>
|
||||
<input type="text" id="mpdUrl" class="form-control font-monospace"
|
||||
placeholder="Or paste MPD URL manually...">
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<button type="button" id="sendToCrawlFlix" class="btn btn-primary btn-lg w-100 mb-2">
|
||||
<i class="fas fa-paper-plane me-2"></i> Send to CrawlFlix
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button type="button" id="copyKeys" class="btn btn-outline-secondary btn-lg w-100 mb-2">
|
||||
<i class="fas fa-copy me-2"></i> Copy Keys Only
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="crawlFlixStatus" class="status-area"></div>
|
||||
|
||||
<!-- Workflow Steps -->
|
||||
<div class="mt-4 p-3 bg-light rounded">
|
||||
<h6 class="text-muted mb-3"><i class="fas fa-list-ol"></i> Automated Workflow</h6>
|
||||
<div class="row text-center">
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-shield-alt fa-2x text-primary mb-2"></i>
|
||||
<p class="small mb-0">Extract Keys</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-search fa-2x text-success mb-2"></i>
|
||||
<p class="small mb-0">Detect MPD</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-paper-plane fa-2x text-info mb-2"></i>
|
||||
<p class="small mb-0">Send Data</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-download fa-2x text-warning mb-2"></i>
|
||||
<p class="small mb-0">Start Download</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ClearKey Section -->
|
||||
<div id="ckHome" class="hidden">
|
||||
<div class="card section-card">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="mb-0"><i class="fas fa-unlock text-warning"></i> ClearKey Detected</h6>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<label for="ckResult" class="form-label">ClearKey Result:</label>
|
||||
<textarea id="ckResult" class="form-control font-monospace" rows="6" placeholder="ClearKey data will appear here..."></textarea>
|
||||
|
||||
<!-- CrawlFlix for ClearKey -->
|
||||
<div class="mt-3 pt-3 border-top">
|
||||
<h6><i class="fas fa-download text-info"></i> Send to CrawlFlix</h6>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-12">
|
||||
<label for="crawlFlixUrlCK" class="form-label">CrawlFlix Server</label>
|
||||
<input type="text" id="crawlFlixUrlCK" class="form-control form-control-sm"
|
||||
placeholder="http://localhost:3000" value="http://localhost:3000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-10">
|
||||
<label for="mpdSelectCK" class="form-label">MPD Manifest</label>
|
||||
<select id="mpdSelectCK" class="form-select form-select-sm">
|
||||
<option value="">-- Detected MPDs --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
<button type="button" id="refreshMPDsCK" class="btn btn-success btn-sm w-100">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="text" id="mpdUrlCK" class="form-control form-control-sm"
|
||||
placeholder="Or paste MPD URL manually...">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="button" id="sendToCrawlFlixCK" class="btn btn-primary btn-sm flex-fill">
|
||||
<i class="fas fa-paper-plane"></i> Send to CrawlFlix
|
||||
</button>
|
||||
<button type="button" id="copyKeysCK" class="btn btn-outline-secondary btn-sm flex-fill">
|
||||
<i class="fas fa-copy"></i> Copy Keys
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="crawlFlixStatusCK" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden containers -->
|
||||
<div id="chooserContainer" class="hidden">
|
||||
<div class="card">
|
||||
<div class="card-body p-2">
|
||||
<input type="text" id="chooserSearch" class="form-control form-control-sm mb-2" placeholder="Search">
|
||||
<ul id="items" class="list-group list-group-flush"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editSchemeContainer" class="hidden">
|
||||
<div class="card">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="mb-0">Edit Scheme</h6>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<textarea id="schemeCode" class="form-control font-monospace" rows="8"></textarea>
|
||||
<button type="button" id="editSchemeOK" class="btn btn-primary btn-sm mt-2">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="updateNotice" class="alert alert-info alert-sm hidden">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Version =VER= update available!
|
||||
<a href="https://github.com/FoxRefire/wvg/archive/=HASH=.zip" class="alert-link">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./main.js" type="module"></script>
|
||||
<script src="./drawList.js"></script>
|
||||
<script src="./editScheme.js"></script>
|
||||
<script src="./updateNotice.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
701
popup/main.js
Normal file
701
popup/main.js
Normal file
@@ -0,0 +1,701 @@
|
||||
// === GLOBAL VARIABLES ===
|
||||
let psshs = chrome.extension.getBackgroundPage().psshs;
|
||||
let requests = chrome.extension.getBackgroundPage().requests;
|
||||
let pageURL = chrome.extension.getBackgroundPage().pageURL;
|
||||
let targetIds = chrome.extension.getBackgroundPage().targetIds;
|
||||
let clearkey = chrome.extension.getBackgroundPage().clearkey;
|
||||
let userInputs = {};
|
||||
|
||||
// === WIDEVINE KEY EXTRACTION ===
|
||||
class WidevineExtractor {
|
||||
static async extractKeys() {
|
||||
const guessButton = document.getElementById("guess");
|
||||
const resultTextarea = document.getElementById('result');
|
||||
|
||||
try {
|
||||
// UI feedback
|
||||
UIHelpers.setLoadingState(guessButton, true);
|
||||
document.body.style.cursor = "wait";
|
||||
|
||||
// Initialize Pyodide
|
||||
const pyodide = await loadPyodide();
|
||||
await pyodide.loadPackage([
|
||||
"certifi-2024.2.2-py3-none-any.whl",
|
||||
"charset_normalizer-3.3.2-py3-none-any.whl",
|
||||
"construct-2.8.8-py2.py3-none-any.whl",
|
||||
"idna-3.6-py3-none-any.whl",
|
||||
"packaging-23.2-py3-none-any.whl",
|
||||
"protobuf-4.24.4-cp312-cp312-emscripten_3_1_52_wasm32.whl",
|
||||
"pycryptodome-3.20.0-cp35-abi3-emscripten_3_1_52_wasm32.whl",
|
||||
"pymp4-1.4.0-py3-none-any.whl",
|
||||
"pyodide_http-0.2.1-py3-none-any.whl",
|
||||
"pywidevine-1.8.0-py3-none-any.whl",
|
||||
"requests-2.31.0-py3-none-any.whl",
|
||||
"urllib3-2.2.1-py3-none-any.whl"
|
||||
].map(e => "/libs/wheels/" + e));
|
||||
|
||||
// Configure Guesser
|
||||
pyodide.globals.set("pssh", document.getElementById('pssh').value);
|
||||
pyodide.globals.set("licUrl", requests[userInputs['license']]['url']);
|
||||
pyodide.globals.set("licHeaders", requests[userInputs['license']]['headers']);
|
||||
pyodide.globals.set("licBody", requests[userInputs['license']]['body']);
|
||||
|
||||
// Load Python scripts
|
||||
const [pre, after, scheme] = await Promise.all([
|
||||
fetch('/python/pre.py').then(res => res.text()),
|
||||
fetch('/python/after.py').then(res => res.text()),
|
||||
Promise.resolve(document.getElementById("schemeCode").value)
|
||||
]);
|
||||
|
||||
// Execute Python script
|
||||
const result = await pyodide.runPythonAsync([pre, scheme, after].join("\n"));
|
||||
resultTextarea.value = result;
|
||||
|
||||
// Save to history
|
||||
this.saveToHistory(result);
|
||||
|
||||
// Auto-update CrawlFlix integration
|
||||
CrawlFlixIntegration.updateAfterKeyExtraction();
|
||||
|
||||
StatusManager.show('Keys extracted successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Key extraction failed:', error);
|
||||
StatusManager.show(`Key extraction failed: ${error.message}`, 'error');
|
||||
} finally {
|
||||
// Reset UI
|
||||
UIHelpers.setLoadingState(guessButton, false);
|
||||
document.body.style.cursor = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
static saveToHistory(result) {
|
||||
const historyData = {
|
||||
PSSH: document.getElementById('pssh').value,
|
||||
KEYS: result.split("\n").slice(0, -1)
|
||||
};
|
||||
chrome.storage.local.set({[pageURL]: historyData}, null);
|
||||
}
|
||||
|
||||
static async autoSelect() {
|
||||
userInputs["license"] = 0;
|
||||
document.getElementById("license").value = requests[0]['url'];
|
||||
document.getElementById('pssh').value = psshs[0];
|
||||
|
||||
try {
|
||||
const selectRules = await fetch("/selectRules.conf").then(r => r.text());
|
||||
const rules = selectRules
|
||||
.replace(/\n^\s*$|\s*\/\/.*|\s*$/gm, "")
|
||||
.split("\n")
|
||||
.map(row => row.split("$$"));
|
||||
|
||||
for (const item of rules) {
|
||||
const search = requests.map(r => r['url']).findIndex(e => e.includes(item[0]));
|
||||
if (search >= 0) {
|
||||
if (item[1]) document.getElementById("schemeSelect").value = item[1];
|
||||
userInputs["license"] = search;
|
||||
document.getElementById("license").value = requests[search]['url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("schemeSelect").dispatchEvent(new Event("input"));
|
||||
} catch (error) {
|
||||
console.error('Auto-select failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === SMART MPD SELECTOR ===
|
||||
class SmartMPDSelector {
|
||||
static scoreAndRankMPDs(mpdUrls) {
|
||||
const scoredMPDs = mpdUrls.map(url => ({
|
||||
url,
|
||||
score: this.calculateMPDScore(url),
|
||||
reason: this.getSelectionReason(url)
|
||||
}));
|
||||
|
||||
// Trier par score décroissant
|
||||
return scoredMPDs.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
static calculateMPDScore(url) {
|
||||
let score = 0;
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
// === CRITÈRES NÉGATIFS (à éviter) ===
|
||||
|
||||
// Éliminer les URLs non-MPD
|
||||
if (urlLower.includes('github.com') || urlLower.includes('manifest.json')) {
|
||||
return -1000; // Score très négatif
|
||||
}
|
||||
|
||||
// Éviter les URLs avec trop de redirections/proxies
|
||||
if (urlLower.includes('routemeup') || urlLower.includes('route=')) {
|
||||
score -= 200;
|
||||
}
|
||||
|
||||
// Éviter les URLs avec double-encoding ou trop complexes
|
||||
if (url.includes('__token__') && url.includes('%')) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
// === CRITÈRES POSITIFS (à privilégier) ===
|
||||
|
||||
// Privilégier les URLs directes CDN
|
||||
if (urlLower.includes('cdn.net') || urlLower.includes('vod-')) {
|
||||
score += 300;
|
||||
}
|
||||
|
||||
// Privilégier les URLs avec .mpd explicite
|
||||
if (url.endsWith('.mpd') || urlLower.includes('.mpd?')) {
|
||||
score += 200;
|
||||
}
|
||||
|
||||
// Privilégier les URLs courtes (moins de proxies)
|
||||
if (url.length < 300) {
|
||||
score += 100;
|
||||
} else if (url.length > 500) {
|
||||
score -= 50;
|
||||
}
|
||||
|
||||
// Privilégier les tokens JWT simples vs double tokens
|
||||
const tokenCount = (url.match(/token=/g) || []).length;
|
||||
if (tokenCount === 1) {
|
||||
score += 50;
|
||||
} else if (tokenCount > 1) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
// Privilégier les URLs sans encoding excessif
|
||||
const encodingScore = url.length - decodeURIComponent(url).length;
|
||||
if (encodingScore < 50) {
|
||||
score += 50;
|
||||
} else {
|
||||
score -= encodingScore;
|
||||
}
|
||||
|
||||
// === CRITÈRES SPÉCIFIQUES CANAL+ ===
|
||||
|
||||
// Détecter le pattern Canal+/Paramount+
|
||||
if (urlLower.includes('paramountplus') || urlLower.includes('viacom')) {
|
||||
score += 100;
|
||||
|
||||
// Privilégier les edge servers directs
|
||||
if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) {
|
||||
score += 150;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static getSelectionReason(url) {
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
if (urlLower.includes('github.com')) {
|
||||
return 'Excluded: Not a media manifest';
|
||||
}
|
||||
|
||||
if (urlLower.includes('routemeup')) {
|
||||
return 'Router/proxy URL (lower priority)';
|
||||
}
|
||||
|
||||
if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) {
|
||||
return 'Direct CDN edge server (optimal)';
|
||||
}
|
||||
|
||||
if (url.includes('__token__')) {
|
||||
return 'Tokenized CDN URL';
|
||||
}
|
||||
|
||||
return 'Standard MPD URL';
|
||||
}
|
||||
|
||||
static getBestMPD(mpdUrls) {
|
||||
if (!mpdUrls || mpdUrls.length === 0) return null;
|
||||
if (mpdUrls.length === 1) return mpdUrls[0];
|
||||
|
||||
const rankedMPDs = this.scoreAndRankMPDs(mpdUrls);
|
||||
const best = rankedMPDs[0];
|
||||
|
||||
console.log('MPD Selection Results:', rankedMPDs);
|
||||
|
||||
// Ne retourner que si le score est positif
|
||||
return best.score > 0 ? best.url : null;
|
||||
}
|
||||
|
||||
static analyzeAndDisplayMPDs(mpdUrls) {
|
||||
const rankedMPDs = this.scoreAndRankMPDs(mpdUrls);
|
||||
|
||||
// Afficher l'analyse dans la console pour debug
|
||||
console.table(rankedMPDs.map(mpd => ({
|
||||
URL: this.truncateUrl(mpd.url),
|
||||
Score: mpd.score,
|
||||
Reason: mpd.reason
|
||||
})));
|
||||
|
||||
return rankedMPDs;
|
||||
}
|
||||
|
||||
static truncateUrl(url, maxLength = 60) {
|
||||
if (url.length <= maxLength) return url;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const domain = urlObj.hostname;
|
||||
const filename = urlObj.pathname.split('/').pop();
|
||||
return `${domain}/.../${filename}`;
|
||||
} catch {
|
||||
return url.substring(0, maxLength) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour mettre à jour l'UI avec le meilleur MPD
|
||||
static updateUIWithBestMPD(mpdUrls) {
|
||||
const bestMPD = this.getBestMPD(mpdUrls);
|
||||
|
||||
if (bestMPD) {
|
||||
// Auto-sélectionner le meilleur MPD
|
||||
const selects = ['mpdSelect', 'mpdSelectCK'];
|
||||
const inputs = ['mpdUrl', 'mpdUrlCK'];
|
||||
|
||||
selects.forEach((selectId, index) => {
|
||||
const select = document.getElementById(selectId);
|
||||
const input = document.getElementById(inputs[index]);
|
||||
|
||||
if (select && input) {
|
||||
select.value = bestMPD;
|
||||
input.value = bestMPD;
|
||||
|
||||
// Ajouter une classe pour indiquer la sélection auto
|
||||
select.classList.add('border-success');
|
||||
setTimeout(() => select.classList.remove('border-success'), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
const analysis = this.analyzeAndDisplayMPDs(mpdUrls);
|
||||
const bestAnalysis = analysis[0];
|
||||
|
||||
StatusManager.show(
|
||||
`Auto-selected best MPD: ${bestAnalysis.reason}`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MPDDetector {
|
||||
static detectMPDUrls() {
|
||||
const backgroundPage = chrome.extension.getBackgroundPage();
|
||||
const detectedMPDs = backgroundPage.detectedMPDs || [];
|
||||
|
||||
console.log('Raw detected MPDs:', detectedMPDs);
|
||||
|
||||
return detectedMPDs
|
||||
.filter((url, index, arr) => arr.indexOf(url) === index) // Remove duplicates
|
||||
.filter(url => url && url.length > 0) // Remove empty URLs
|
||||
.filter(url => !url.includes('/chunk-') && !url.includes('/segment-')); // Filter segments
|
||||
}
|
||||
|
||||
static updateBadgeFromPopup() {
|
||||
const backgroundPage = chrome.extension.getBackgroundPage();
|
||||
if (backgroundPage && backgroundPage.updateExtensionBadge) {
|
||||
backgroundPage.updateExtensionBadge();
|
||||
}
|
||||
}
|
||||
|
||||
static updateDropdowns() {
|
||||
const mpdUrls = this.detectMPDUrls();
|
||||
const selects = ['mpdSelect', 'mpdSelectCK'];
|
||||
|
||||
selects.forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '<option value="">-- Auto-detected MPDs --</option>';
|
||||
|
||||
// Utiliser le smart selector pour trier les MPDs
|
||||
const rankedMPDs = SmartMPDSelector.scoreAndRankMPDs(mpdUrls);
|
||||
|
||||
rankedMPDs.forEach((mpdInfo, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = mpdInfo.url;
|
||||
|
||||
// Ajouter des indicateurs visuels
|
||||
const prefix = index === 0 ? '⭐ [BEST] ' :
|
||||
mpdInfo.score > 0 ? '✓ ' : '⚠ ';
|
||||
|
||||
option.textContent = prefix + this.formatUrlForDisplay(mpdInfo.url);
|
||||
|
||||
// Ajouter des classes CSS pour styling
|
||||
if (index === 0) {
|
||||
option.style.fontWeight = 'bold';
|
||||
option.style.color = '#28a745';
|
||||
} else if (mpdInfo.score <= 0) {
|
||||
option.style.color = '#dc3545';
|
||||
}
|
||||
|
||||
select.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-sélection intelligente
|
||||
if (mpdUrls.length > 0) {
|
||||
SmartMPDSelector.updateUIWithBestMPD(mpdUrls);
|
||||
}
|
||||
|
||||
// Mettre à jour le status avec l'analyse
|
||||
StatusManager.show(
|
||||
`Found ${mpdUrls.length} MPD(s) - Auto-selected best candidate`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
static formatUrlForDisplay(url) {
|
||||
return SmartMPDSelector.truncateUrl(url, 80);
|
||||
}
|
||||
}
|
||||
|
||||
// === CRAWLFLIX INTEGRATION ===
|
||||
class CrawlFlixIntegration {
|
||||
static async sendToCrawlFlix(isClearchey = false) {
|
||||
const suffix = isClearchey ? 'CK' : '';
|
||||
const crawlFlixUrl = document.getElementById(`crawlFlixUrl${suffix}`).value || 'http://localhost:3000';
|
||||
const mpdUrl = document.getElementById(`mpdUrl${suffix}`).value;
|
||||
const resultTextarea = document.getElementById(isClearchey ? 'ckResult' : 'result');
|
||||
|
||||
// Validation
|
||||
if (!mpdUrl.trim()) {
|
||||
StatusManager.show('Please enter or select an MPD URL', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resultTextarea.value.trim()) {
|
||||
StatusManager.show('No keys available to send', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
StatusManager.show('Processing MPD and sending to CrawlFlix...', 'info', suffix);
|
||||
|
||||
// Parse and validate keys
|
||||
const keys = this.parseKeys(resultTextarea.value);
|
||||
if (keys.length === 0) {
|
||||
StatusManager.show('No valid keys found in the format key:value', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test CrawlFlix connection and process MPD
|
||||
const mpdData = await this.processMPD(crawlFlixUrl, mpdUrl);
|
||||
|
||||
// Create success message
|
||||
const message = `✓ Successfully sent to CrawlFlix!
|
||||
${keys.length} key(s) • ${mpdData.videoTracks.length} video track(s) • ${mpdData.audioTracks.length} audio track(s) • ${mpdData.subtitles.length} subtitle(s)`;
|
||||
|
||||
StatusManager.show(message, 'success', suffix);
|
||||
|
||||
// Open CrawlFlix with pre-filled data
|
||||
this.openCrawlFlixTab(crawlFlixUrl, mpdUrl, resultTextarea.value);
|
||||
|
||||
} catch (error) {
|
||||
console.error('CrawlFlix send error:', error);
|
||||
StatusManager.show(`Failed to send: ${error.message}`, 'error', suffix);
|
||||
}
|
||||
}
|
||||
|
||||
static parseKeys(keysText) {
|
||||
return keysText
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && line.includes(':'))
|
||||
.map(line => {
|
||||
const [key, value] = line.split(':');
|
||||
return {
|
||||
key: key?.trim(),
|
||||
value: value?.trim()
|
||||
};
|
||||
})
|
||||
.filter(k => k.key && k.value && k.key.length > 0 && k.value.length > 0);
|
||||
}
|
||||
|
||||
static async processMPD(crawlFlixUrl, mpdUrl) {
|
||||
const response = await fetch(`${crawlFlixUrl}/processMPD`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mpdUrl })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`MPD processing failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static openCrawlFlixTab(crawlFlixUrl, mpdUrl, keysText) {
|
||||
const params = new URLSearchParams({
|
||||
mpdUrl: mpdUrl,
|
||||
keys: keysText,
|
||||
source: 'widevine-plugin'
|
||||
});
|
||||
|
||||
const crawlFlixTab = `${crawlFlixUrl}?${params.toString()}`;
|
||||
chrome.tabs.create({ url: crawlFlixTab });
|
||||
}
|
||||
|
||||
static copyKeys(resultTextareaId) {
|
||||
const textarea = document.getElementById(resultTextareaId);
|
||||
if (!textarea || !textarea.value.trim()) {
|
||||
StatusManager.show('No keys to copy', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(textarea.value).then(() => {
|
||||
StatusManager.show('Keys copied to clipboard!', 'success');
|
||||
}).catch(err => {
|
||||
console.error('Copy failed:', err);
|
||||
StatusManager.show('Failed to copy keys', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
static updateAfterKeyExtraction() {
|
||||
// Auto-refresh MPD detection after key extraction
|
||||
setTimeout(() => {
|
||||
MPDDetector.updateDropdowns();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
static loadSavedSettings() {
|
||||
chrome.storage.local.get(['crawlFlixUrl'], (result) => {
|
||||
if (result.crawlFlixUrl) {
|
||||
const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
if (input && !input.value) {
|
||||
input.value = result.crawlFlixUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static saveSettings() {
|
||||
const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.addEventListener('change', (e) => {
|
||||
chrome.storage.local.set({ crawlFlixUrl: e.target.value });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === STATUS MANAGER ===
|
||||
class StatusManager {
|
||||
static show(message, type, suffix = '') {
|
||||
const statusDiv = document.getElementById(`crawlFlixStatus${suffix}`);
|
||||
if (!statusDiv) return;
|
||||
|
||||
// Clear any existing content
|
||||
statusDiv.innerHTML = '';
|
||||
|
||||
// Create alert element
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${this.getBootstrapClass(type)} alert-dismissible fade show`;
|
||||
alert.innerHTML = `
|
||||
<i class="fas ${this.getIcon(type)} me-2"></i>
|
||||
${message.replace(/\n/g, '<br>')}
|
||||
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
statusDiv.appendChild(alert);
|
||||
|
||||
// Auto-dismiss after delay
|
||||
if (type === 'success' || type === 'info') {
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
static getBootstrapClass(type) {
|
||||
const mapping = {
|
||||
success: 'success',
|
||||
error: 'danger',
|
||||
info: 'info',
|
||||
warning: 'warning'
|
||||
};
|
||||
return mapping[type] || 'secondary';
|
||||
}
|
||||
|
||||
static getIcon(type) {
|
||||
const mapping = {
|
||||
success: 'fa-check-circle',
|
||||
error: 'fa-exclamation-triangle',
|
||||
info: 'fa-info-circle',
|
||||
warning: 'fa-exclamation-triangle'
|
||||
};
|
||||
return mapping[type] || 'fa-info';
|
||||
}
|
||||
}
|
||||
|
||||
// === UI HELPERS ===
|
||||
class UIHelpers {
|
||||
static setLoadingState(button, isLoading) {
|
||||
if (!button) return;
|
||||
|
||||
if (isLoading) {
|
||||
button.disabled = true;
|
||||
const originalText = button.innerHTML;
|
||||
button.dataset.originalText = originalText;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Extracting Keys...';
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.innerHTML = button.dataset.originalText || button.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
static copyToClipboard(text) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// === EVENT LISTENERS ===
|
||||
class EventManager {
|
||||
static init() {
|
||||
this.setupWidevineListeners();
|
||||
this.setupCrawlFlixListeners();
|
||||
this.setupUIListeners();
|
||||
}
|
||||
|
||||
static setupWidevineListeners() {
|
||||
const guessButton = document.getElementById('guess');
|
||||
const resultTextarea = document.getElementById('result');
|
||||
|
||||
if (guessButton) {
|
||||
guessButton.addEventListener('click', () => WidevineExtractor.extractKeys());
|
||||
}
|
||||
|
||||
if (resultTextarea) {
|
||||
resultTextarea.addEventListener('click', function() {
|
||||
this.select();
|
||||
navigator.clipboard.writeText(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static setupCrawlFlixListeners() {
|
||||
// Dropdown selections
|
||||
['mpdSelect', 'mpdSelectCK'].forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
const urlInput = document.getElementById(selectId.replace('Select', 'Url'));
|
||||
if (select && urlInput) {
|
||||
select.addEventListener('change', (e) => {
|
||||
if (e.target.value) {
|
||||
urlInput.value = e.target.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh buttons
|
||||
['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => {
|
||||
const button = document.getElementById(buttonId);
|
||||
if (button) {
|
||||
button.addEventListener('click', () => MPDDetector.updateDropdowns());
|
||||
}
|
||||
});
|
||||
|
||||
// Send buttons
|
||||
const sendButton = document.getElementById('sendToCrawlFlix');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(false));
|
||||
}
|
||||
|
||||
const sendButtonCK = document.getElementById('sendToCrawlFlixCK');
|
||||
if (sendButtonCK) {
|
||||
sendButtonCK.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(true));
|
||||
}
|
||||
|
||||
// Copy buttons
|
||||
const copyButton = document.getElementById('copyKeys');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', () => CrawlFlixIntegration.copyKeys('result'));
|
||||
}
|
||||
|
||||
const copyButtonCK = document.getElementById('copyKeysCK');
|
||||
if (copyButtonCK) {
|
||||
copyButtonCK.addEventListener('click', () => CrawlFlixIntegration.copyKeys('ckResult'));
|
||||
}
|
||||
}
|
||||
|
||||
static setupUIListeners() {
|
||||
// ClearKey result click handler
|
||||
const ckResult = document.getElementById('ckResult');
|
||||
if (ckResult) {
|
||||
ckResult.addEventListener('click', function() {
|
||||
this.select();
|
||||
navigator.clipboard.writeText(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === CORS FETCH HELPER ===
|
||||
window.corsFetch = (u, m, h, b) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(targetIds[0], {
|
||||
type: "FETCH",
|
||||
u: u,
|
||||
m: m,
|
||||
h: h,
|
||||
b: b
|
||||
}, {frameId: targetIds[1]}, res => {
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// === INITIALIZATION ===
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
EventManager.init();
|
||||
CrawlFlixIntegration.loadSavedSettings();
|
||||
CrawlFlixIntegration.saveSettings();
|
||||
});
|
||||
|
||||
// Main initialization logic
|
||||
if (clearkey) {
|
||||
// ClearKey detected
|
||||
document.getElementById('noEME').style.display = 'none';
|
||||
document.getElementById('ckHome').style.display = 'block';
|
||||
document.getElementById('ckResult').value = clearkey;
|
||||
|
||||
MPDDetector.updateDropdowns();
|
||||
StatusManager.show('ClearKey content detected', 'success');
|
||||
MPDDetector.updateBadgeFromPopup();
|
||||
} else if (psshs.length) {
|
||||
// Widevine detected
|
||||
document.getElementById('noEME').style.display = 'none';
|
||||
document.getElementById('home').style.display = 'block';
|
||||
|
||||
WidevineExtractor.autoSelect();
|
||||
MPDDetector.updateDropdowns();
|
||||
StatusManager.show('Widevine content detected', 'success');
|
||||
MPDDetector.updateBadgeFromPopup();
|
||||
// Auto-refresh MPD detection periodically
|
||||
setInterval(() => {
|
||||
MPDDetector.updateDropdowns();
|
||||
}, 3000);
|
||||
}
|
||||
99
popup/style.css
Normal file
99
popup/style.css
Normal file
@@ -0,0 +1,99 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0; /* Reset default margin */
|
||||
padding: 0; /* Reset default padding */
|
||||
}
|
||||
|
||||
body {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background: linear-gradient(45deg, #0d364c, #062535, #02141e);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
#noEME {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#updateNotice {
|
||||
justify-self: center;
|
||||
align-self: end;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#updateNotice a{
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
#wvForm {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#wvForm label {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#pssh, #license, #schemeSelect {
|
||||
width: 80%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#psshButton, #licenseButton, #editSchemeButton {
|
||||
width: 20%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#toggleHistory {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#toggleHistory button {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#guess {
|
||||
width: 20%;
|
||||
justify-self: center;
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
#result {
|
||||
width: 90%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
resize: none;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#chooserContainer {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#ckHome h3, label {
|
||||
color: white;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#ckHome label {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
#ckResult {
|
||||
width: 90%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
resize: none;
|
||||
justify-self: center;
|
||||
}
|
||||
10
popup/updateNotice.js
Normal file
10
popup/updateNotice.js
Normal file
@@ -0,0 +1,10 @@
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
cManifest=await fetch("/manifest.json").then(r=>r.json());
|
||||
rManifest=await fetch("https://raw.githubusercontent.com/FoxRefire/wvg/next/manifest.json").then(r=>r.json());
|
||||
if(cManifest.version < rManifest.version){
|
||||
let notice = document.getElementById("updateNotice");
|
||||
notice.style.display='block';
|
||||
notice.innerHTML = notice.innerHTML.replace("=VER=", rManifest.version);
|
||||
notice.innerHTML = notice.innerHTML.replace("=HASH=", rManifest.version_name);
|
||||
}
|
||||
});
|
||||
27
private_key.pem
Normal file
27
private_key.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAmMBTdNLOx9MuuuyyoFAykrOQNnz/xcWjGqksOqHa2i6U0eLv
|
||||
te8yfRM6ZB7Xw/z+IFlaNIcszDiSn6r6zQ06AEh1OSCyarFTORKXyWMnnru28Jtv
|
||||
gcS3lOMsggsFVm5xfV3RmpSNj6l4yraa/gZ7fif8lZNmDNxAdyITYx9c0DfPm6fs
|
||||
eXD99vmEUdkkRjFkwGgys4aKUUp7OmXGBRd3qmcMe4fRgOMd7uETPPuP1HlzBhTp
|
||||
NGct7GzOYsMdki//Lwv2S8TTeq3f5e9ZKnrJtMxSU+WGtkfYluzKFesBxwQwNm/F
|
||||
/a0yMzMo+ywVqu00s1/Iv+I21NNIk37LK8qNxQIDAQABAoIBAA+sJVlGVdwFIXV2
|
||||
sAnTIKqSTON3piCLG3syogHutbnkGfXXpG5UzqfW72Cdai2cNBC/9N7l2E/9z/bH
|
||||
lWMGOTi/4R3NlTDC5T3lAqOG47udbp2M59R3NA930uBm3r3UUsWg+M608OStZ+It
|
||||
+S9uHSvqULf3adA7paDzjeys0kTfWdQGSiyMT+8NS2wUSdB0xVsvHmlYb0UvuZa0
|
||||
KEETRoPhrqs3g7ZsAvxyNq6cULwUViVZEo5m1nzpCLLxynVK3E8/BpGMRZdNf8SM
|
||||
I5iygLm1SMdelV/GV1uKeXHcVJFfpYC6Dp0zawbgTIQqn8Sr/X5PnnyjD+984l5f
|
||||
ofyg+pMCgYEAxcQnrwW/f1AfIdpIVc3CzbMUMfYkno/TTY3qytcBrcJmwvQGqG36
|
||||
xCNy++DsosLNK79SGORsuPIWQzZKv9NipYuDM/oZH6LnlKC+FT1mOjcK7cZ2hC0i
|
||||
WbykBSRMIstXaKVVIWv1R6xPBV3YSWB1wYLqcTewnYRstuJJLd5/pwsCgYEAxbrk
|
||||
V4MEh5dRRuhyRVIdsenO6dwulVDZjsc1wx+VxPayEs/ATHBAL34JFrM18VMKC+9L
|
||||
uRO3CUoHCiF3xgBWMDtZ4yJL8NMOfoNPUuG8zu+YeP237UWS5Un0anKPCTQABYOh
|
||||
esaZRCFHPMF/E0OhFnKxP718XmpT9SHBZleHYG8CgYBhW5KJqBPA0imxWas4imX3
|
||||
K8yqV9lpMba9PDs1BimuZ8B9AvLwIcxMmIL3mqAD9FBa1AHPfUxEs/Dfv6+GxcKP
|
||||
aqH5iYqqt2C4G3XYMPTNfeFkcspPgYSilqpMWbUdf+sU+idxn5sNYLc6Fhvu64ys
|
||||
6g0OFXNd6B443idWbG19RwKBgCtUBd9rVV3E+cl4/DlWgi1PV8kFN6v+mH3iB1Tn
|
||||
ofEUQfc/URsu6dG+Y4123dYw5R2yZTx8JnrBnwNITC+2OSy+hLbx+AQgq6drbMFz
|
||||
t+T8ucOhjWFQAp199DIzQka4/1w1+Pend0Rnqm2U1RzKkA0UOuUFx7AEyrsHl5Ku
|
||||
kXq/AoGAIjzPzL1LHohNe3OgobDhbBcwmSunS7pat/zam2encEd/ePdhePKfjODN
|
||||
D7XtAeE/wDLPJNtl09QjgJSpU5QhBG10mkxVpQPzORoS0cDSpk/92x8eUn3Lk43v
|
||||
g09f6rgkSLSnhTEiyL9+XN4MJoh6QwZdfPIFzsCOzO0NoxIsEm0=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
15
python/after.py
Normal file
15
python/after.py
Normal file
@@ -0,0 +1,15 @@
|
||||
try:
|
||||
cdm.parse_license(session_id, licence)
|
||||
except Exception as e:
|
||||
js.document.getElementById('result').value=f"Could not decrypt!\n\nLicense Response:\n{licence}\n\nhttps://github.com/FoxRefire/wvg/wiki/How-to-add-custom-license-scheme-yourself"
|
||||
raise Exception(e)
|
||||
|
||||
# get keys
|
||||
keys=""
|
||||
for key in cdm.get_keys(session_id):
|
||||
if key.type=="CONTENT":
|
||||
keys+=f"{key.kid.hex}:{key.key.hex()}\n"
|
||||
|
||||
# close session, disposes of session data
|
||||
cdm.close(session_id)
|
||||
keys
|
||||
106
python/pre.py
Normal file
106
python/pre.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from pywidevine.cdm import Cdm
|
||||
from pywidevine.remotecdm import RemoteCdm
|
||||
from pywidevine.device import Device, DeviceTypes
|
||||
from pywidevine.pssh import PSSH
|
||||
|
||||
import json
|
||||
import js
|
||||
import base64
|
||||
from pyodide.http import pyfetch
|
||||
|
||||
def blobsToDevice(cID, pKey):
|
||||
return Device(client_id=cID, private_key=pKey, type_=DeviceTypes['ANDROID'], security_level=3, flags=None)
|
||||
|
||||
async def loadCdm():
|
||||
# Looking for device.wvd
|
||||
try:
|
||||
wvd = await (await pyfetch("/device.wvd")).bytes()
|
||||
return Cdm.from_device(Device.loads(wvd))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Looking for device_client_id_blob + device_private_key
|
||||
try:
|
||||
cID=await (await pyfetch("/device_client_id_blob")).bytes()
|
||||
pKey=await (await pyfetch("/device_private_key")).bytes()
|
||||
return Cdm.from_device(blobsToDevice(cID, pKey))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Looking for client_id.bin + private_key.pem
|
||||
try:
|
||||
cID=await (await pyfetch("/client_id.bin")).bytes()
|
||||
pKey=await (await pyfetch("/private_key.pem")).bytes()
|
||||
return Cdm.from_device(blobsToDevice(cID, pKey))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Looking for remote.json
|
||||
try:
|
||||
remote_conf=await (await pyfetch("/remote.json")).json()
|
||||
return RemoteCdm(**remote_conf)
|
||||
except Exception as e:
|
||||
js.document.getElementById('result').value=f"No CDM key pair found! \n\n https://github.com/FoxRefire/wvg/wiki/Getting-started#2-put-cdm-key-pair-files"
|
||||
raise Exception(e)
|
||||
|
||||
# Define corsFetch API for requesting server that require origin header
|
||||
async def corsFetch(url: str, method: str, headers: [dict, str], body: [dict, bytes, str], resType: str="blob"):
|
||||
if type(headers) == dict:
|
||||
headers = json.dumps(headers)
|
||||
|
||||
match body:
|
||||
case bytes(): body = base64.b64encode(body).decode()
|
||||
case str(): body = base64.b64encode(body.encode()).decode()
|
||||
case dict(): body = base64.b64encode(json.dumps(body).encode()).decode()
|
||||
|
||||
res = await js.corsFetch(url, method, headers, body)
|
||||
res = base64.b64decode(res.encode())
|
||||
|
||||
match resType:
|
||||
case "blob": pass
|
||||
case "str": res = res.decode()
|
||||
case "json": res = json.loads(res.decode())
|
||||
|
||||
return res
|
||||
|
||||
# Define loadBody API for loading requestBody to scheme concisely
|
||||
def loadBody(loadAs: str):
|
||||
global licBody
|
||||
licBody = base64.b64decode(licBody.encode())
|
||||
|
||||
match loadAs:
|
||||
case "blob": pass
|
||||
case "str": licBody = licBody.decode()
|
||||
case "json": licBody = json.loads(licBody.decode())
|
||||
|
||||
return licBody
|
||||
|
||||
# Define a function to get challenge if needed to set a service cert
|
||||
def getChallenge(getAs, *cert):
|
||||
global session_id
|
||||
global pssh
|
||||
|
||||
if bool(cert):
|
||||
cdm.set_service_certificate(session_id, cert[0])
|
||||
|
||||
challenge = cdm.get_license_challenge(session_id, pssh)
|
||||
|
||||
match getAs:
|
||||
case "blob": pass
|
||||
case "b64": challenge = base64.b64encode(challenge).decode()
|
||||
case "list": challenge = list(challenge)
|
||||
return challenge
|
||||
|
||||
# prepare pssh
|
||||
pssh = PSSH(pssh)
|
||||
|
||||
# load cdm
|
||||
cdm = await loadCdm()
|
||||
|
||||
# open cdm session
|
||||
session_id = cdm.open()
|
||||
|
||||
# load headers
|
||||
licHeaders=json.loads(licHeaders)
|
||||
|
||||
js.chrome.extension.getBackgroundPage().isBlock=False
|
||||
10
python/schemes/Allente.py
Normal file
10
python/schemes/Allente.py
Normal file
@@ -0,0 +1,10 @@
|
||||
payload = {
|
||||
'playerPayload': base64.b64encode(cdm.service_certificate_challenge).decode()
|
||||
}
|
||||
service_cert = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
service_cert = service_cert['license']
|
||||
payload = {
|
||||
'playerPayload': getChallenge('b64', service_cert)
|
||||
}
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['license']
|
||||
7
python/schemes/Amazon.py
Normal file
7
python/schemes/Amazon.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import urllib.parse
|
||||
payload = f'widevine2Challenge={urllib.parse.quote(base64.b64encode(cdm.service_certificate_challenge).decode())}&includeHdcpTestKeyInLicense=true'
|
||||
service_cert = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
service_cert = service_cert['widevine2License']['license']
|
||||
payload = f'widevine2Challenge={urllib.parse.quote(getChallenge("b64", service_cert))}&includeHdcpTestKeyInLicense=true'
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['widevine2License']['license']
|
||||
4
python/schemes/CanalPlusLive.py
Normal file
4
python/schemes/CanalPlusLive.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody('json')
|
||||
payload['ServiceRequest']['InData']['ChallengeInfo'] = getChallenge('b64', Cdm.common_privacy_cert)
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence["ServiceResponse"]["OutData"]["LicenseInfo"]
|
||||
3
python/schemes/CanalPlusVOD.py
Normal file
3
python/schemes/CanalPlusVOD.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, getChallenge('b64', Cdm.common_privacy_cert), "blob")
|
||||
licence = ET.fromstring(licence).find('.//{http://www.canal-plus.com/DRM/V1}license').text
|
||||
4
python/schemes/Comcast.py
Normal file
4
python/schemes/Comcast.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload['licenseRequest'] = getChallenge("b64")
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['license']
|
||||
1
python/schemes/CommonWV.py
Normal file
1
python/schemes/CommonWV.py
Normal file
@@ -0,0 +1 @@
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, getChallenge('blob'), "blob")
|
||||
2
python/schemes/DRMToday.py
Normal file
2
python/schemes/DRMToday.py
Normal file
@@ -0,0 +1,2 @@
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, getChallenge('blob'), "json")
|
||||
licence = licence['license']
|
||||
3
python/schemes/Fantop.py
Normal file
3
python/schemes/Fantop.py
Normal file
@@ -0,0 +1,3 @@
|
||||
payload = loadBody('json')
|
||||
payload['payload'] = getChallenge('b64')
|
||||
licence = await corsFetch (licUrl, "POST", licHeaders, payload, "blob")
|
||||
3
python/schemes/GlobalTV.py
Normal file
3
python/schemes/GlobalTV.py
Normal file
@@ -0,0 +1,3 @@
|
||||
payload = loadBody("json")
|
||||
payload['license_request_data'] = getChallenge('list')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "blob")
|
||||
61
python/schemes/Heuristic.py
Normal file
61
python/schemes/Heuristic.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import re
|
||||
|
||||
|
||||
def replaceRequest(payload):
|
||||
challenge_blob = getChallenge('blob')
|
||||
challengeB64 = getChallenge('b64')
|
||||
challengeArr = str(getChallenge('list'))
|
||||
|
||||
# Trying decode payload, challenge might be raw bytes if it failed
|
||||
try:
|
||||
decodedPayload = payload.decode()
|
||||
except:
|
||||
return challenge_blob
|
||||
|
||||
# Challenge might be JSON/XML stored B64-encoded string
|
||||
replaced = decodedPayload.replace(r"(?<=(\"|\'|>))CAES.*?(?=(\"|\'|<))", challengeB64).replace(r"(?<=(\"|\'|>))CAQ=(?=(\"|\'|<))", challengeB64)
|
||||
if(decodedPayload != replaced):
|
||||
return replaced
|
||||
|
||||
# Challenge might be raw B64-encoded string
|
||||
replaced = decodedPayload.replace(r"^CAES.*?=$", challengeB64).replace(r"^CAQ=$", challengeB64)
|
||||
if(decodedPayload != replaced):
|
||||
return replaced
|
||||
|
||||
# Challenge might be Uint8Array
|
||||
replaced = decodedPayload.replace(r"\[0?8 ?, ?0?1 ?, ?[0-9 ,]*?\]", challengeArr).replace(r"\[0?8 ?, ?0?4]", challengeArr)
|
||||
if(decodedPayload != replaced):
|
||||
return replaced
|
||||
|
||||
|
||||
def findLicense(response):
|
||||
# Trying decode response, license might be raw bytes if it failed
|
||||
try:
|
||||
decodedResponse = response.decode()
|
||||
except:
|
||||
return response
|
||||
|
||||
# License might be JSON/XML stored B64-encoded string
|
||||
try:
|
||||
return re.search(r"(?<=(\"|\'|>))CAIS.*?(?=(\"|\'|<))", decodedResponse).group()
|
||||
except:
|
||||
pass
|
||||
|
||||
# License might be raw B64-encoded string
|
||||
try:
|
||||
return re.search(r"^CAIS.*?=$", decodedResponse).group()
|
||||
except:
|
||||
pass
|
||||
|
||||
# License might be Uint8Array
|
||||
try:
|
||||
foundStr = re.search(r"\[0?8 ?, ?0?2 ?, ?[0-9 ,]*?\]", decodedResponse).group()
|
||||
return bytes(json.loads(foundStr))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
payload = loadBody("blob")
|
||||
payload = replaceRequest(payload)
|
||||
response = await corsFetch(licUrl, "POST", licHeaders, payload, "blob")
|
||||
licence = findLicense(response)
|
||||
5
python/schemes/NosTV.py
Normal file
5
python/schemes/NosTV.py
Normal file
@@ -0,0 +1,5 @@
|
||||
payload = {
|
||||
'challenge': getChallenge('b64')
|
||||
}
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence["license"][0]
|
||||
4
python/schemes/PolSatBoxGo.py
Normal file
4
python/schemes/PolSatBoxGo.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody('json')
|
||||
payload['params']['object'] = getChallenge('b64', Cdm.common_privacy_cert)
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['result']['object']['license']
|
||||
4
python/schemes/RedBee.py
Normal file
4
python/schemes/RedBee.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload['message'] = getChallenge('b64')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['license']
|
||||
5
python/schemes/Sling.py
Normal file
5
python/schemes/Sling.py
Normal file
@@ -0,0 +1,5 @@
|
||||
payload = loadBody('json')
|
||||
payload['message'] = list(cdm.service_certificate_challenge)
|
||||
service_cert = await corsFetch(licUrl, "POST", licHeaders, payload, "blob")
|
||||
payload['message'] = list(getChallenge('list', service_cert))
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "blob")
|
||||
3
python/schemes/VUDRM.py
Normal file
3
python/schemes/VUDRM.py
Normal file
@@ -0,0 +1,3 @@
|
||||
payload = loadBody("json")
|
||||
payload['drm_info'] = getChallenge('list')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "blob")
|
||||
10
python/schemes/VdoCipher.py
Normal file
10
python/schemes/VdoCipher.py
Normal file
@@ -0,0 +1,10 @@
|
||||
payload = loadBody("json")
|
||||
decoded_token = json.loads(base64.b64decode(payload['token']).decode())
|
||||
decoded_token['licenseRequest'] = base64.b64encode(cdm.service_certificate_challenge).decode('utf-8')
|
||||
payload = {"token": base64.b64encode(json.dumps(decoded_token).encode()).decode()}
|
||||
service_cert = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
service_cert = service_cert["license"]
|
||||
decoded_token['licenseRequest'] = getChallenge('b64', service_cert)
|
||||
payload = {"token": base64.b64encode(json.dumps(decoded_token).encode()).decode()}
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence["license"]
|
||||
4
python/schemes/Vodafone.py
Normal file
4
python/schemes/Vodafone.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload['requests'][3]['params']['challenge'] = getChallenge('b64')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['license']
|
||||
4
python/schemes/YouTube.py
Normal file
4
python/schemes/YouTube.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody('json')
|
||||
payload['licenseRequest'] = getChallenge('b64', Cdm.common_privacy_cert)
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['license'].replace("-", "+").replace("_", "/")
|
||||
7
python/schemes/Youku.py
Normal file
7
python/schemes/Youku.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import urllib.parse
|
||||
payload = urllib.parse.parse_qs(loadBody("str"))
|
||||
payload['licenseRequest'] = [getChallenge('b64')]
|
||||
payload = {k: v[0] for k, v in payload.items()}
|
||||
payload = urllib.parse.urlencode(payload)
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['data']
|
||||
4
python/schemes/moTV.py
Normal file
4
python/schemes/moTV.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload['rawLicense'] = getChallenge('b64')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence['rawLicense']
|
||||
4
python/schemes/oqee.py
Normal file
4
python/schemes/oqee.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload['licenseRequest'] = getChallenge('b64')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence["result"]["license"]
|
||||
4
python/schemes/thePlatform.py
Normal file
4
python/schemes/thePlatform.py
Normal file
@@ -0,0 +1,4 @@
|
||||
payload = loadBody("json")
|
||||
payload["getWidevineLicense"]["widevineChallenge"] = getChallenge('b64')
|
||||
licence = await corsFetch(licUrl, "POST", licHeaders, payload, "json")
|
||||
licence = licence["getWidevineLicenseResponse"]["license"]
|
||||
42
selectRules.conf
Normal file
42
selectRules.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
cwip-shaka-proxy
|
||||
zulu.api-canaldigital.com/v1/drm/$$Allente
|
||||
c4.eme.lp.aws.redbee$$RedBee
|
||||
comcast.net/license$$Comcast
|
||||
lic.staging.drmtoday$$DRMToday
|
||||
lic.drmtoday.com$$DRMToday
|
||||
corusappservices.com/authorization/widevine/getresourcekey$$GlobalTV
|
||||
b2c-www.redefine.pl/rpc/drm/$$PolSatBoxGo
|
||||
widevine.entitlement.eu.theplatform.com$$thePlatform
|
||||
cdp/catalog/GetPlaybackResources$$Amazon
|
||||
drm-license.youku.tv$$Youku
|
||||
NOS71ZV1/wvls$$NosTV
|
||||
license.vdocipher.com/auth$$VdoCipher
|
||||
secure-gen-hapi.canal-plus.com/conso/view$$CanalPlusVOD
|
||||
canalplustech.pro/api/V4/zones/cppol$$CanalPlusLive
|
||||
api.fantop.jp/api/v1/drm/widevine-license$$Fantop
|
||||
widevine-proxy.drm.technology/proxy$$VUDRM
|
||||
widevine-license.vudrm.tech/proxy$$VUDRM
|
||||
motv.eu/widevine_proxy$$moTV
|
||||
mw.tvnsul.com.br/widevine_proxy$$moTV
|
||||
vodafone.com/vtv/ccursession/v1/start$$Vodafone
|
||||
api.oqee.net/api/v1/avod/license$$oqee
|
||||
youtubei/v1/player/get_drm_license$$YouTube
|
||||
drmwv.movetv.com/widevine/proxy$$Sling
|
||||
contentlicenseservice/v1/licenses
|
||||
media-license-server/validate-auth-token
|
||||
wv-keyos.licensekeyserver
|
||||
vcas-wv-ew.waoo.tv/
|
||||
uplynk.com/wv
|
||||
widevine.com/proxy
|
||||
expressplay.com/hms/wv
|
||||
pluto.tv/v1/wv
|
||||
/licensing/
|
||||
license-ap
|
||||
getRawWidevineLicense
|
||||
AcquireLicense
|
||||
acquire-license
|
||||
widevine-proxy
|
||||
licenseManager.do
|
||||
widevine
|
||||
license
|
||||
cenc
|
||||
Reference in New Issue
Block a user