0.0.002
ddat interop with dat js cable interop with cabal js ddat's frontend = dat-thang cable fe = cable-thang dillo support based on cel's dillo-dat bootstrap node xplatform support Signed-off-by: divo <418@cryspace.space>
This commit is contained in:
1 parent
f0f91540be
commit
b735900f2e
323 files changed
+134347
-343
No files matched your search
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Yь ⌠≥Q#ёф⌠ШX┤╥╩║XoAВОXrb*з│
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
/build/
|
||||
/dist/
|
||||
/local-data/
|
||||
/test-deps/
|
||||
/config.h
|
||||
/config.mk
|
||||
/config.log
|
||||
/conftest-*
|
||||
@@ -1,22 +1,65 @@
|
||||
## x86_64 ** GCC **
|
||||
deps
|
||||
NaCl/libsodium
|
||||
libutp
|
||||
//webrtc
|
||||
//sdl2
|
||||
dat-thang requires a C99 compiler, make, libsodium development headers, and the
|
||||
system DNS resolver development library (`libresolv` on glibc systems).
|
||||
|
||||
./configure
|
||||
make
|
||||
open a new tty console and log in as root and then cd to the dir then make install
|
||||
Build and test:
|
||||
|
||||
CONFIG FLAGS y'all can pass
|
||||
--
|
||||
./configure CPPFLAGS="-DSLEEP_SALT"
|
||||
make SLEEP_SALT=1
|
||||
--
|
||||
./configure
|
||||
make
|
||||
make test
|
||||
|
||||
** x86_64 MUSL **
|
||||
Install under /usr/local by default:
|
||||
|
||||
## aarch64
|
||||
make install
|
||||
|
||||
## rv64
|
||||
Choose another prefix during configuration:
|
||||
|
||||
./configure --prefix="$HOME/.local"
|
||||
make
|
||||
make test
|
||||
make install
|
||||
|
||||
Packagers can stage an install without writing to the live prefix:
|
||||
|
||||
make DESTDIR=/tmp/dat-thang-package install
|
||||
|
||||
The installed C command is named `ddat`, so the historical JavaScript `dat`
|
||||
command can remain installed beside it. To build the old Hypercore V6 session
|
||||
mode for side-by-side compatibility work, use:
|
||||
|
||||
make NETWORK_PROTOCOL=hypercore build/linux/ddat
|
||||
|
||||
The SDL2 mockup is optional. Install SDL2 development headers and pkg-config,
|
||||
then build and run it with:
|
||||
|
||||
make ENABLE_SDL2=1
|
||||
build/linux/dat-thang
|
||||
|
||||
The GNU/Linux default output path is `build/linux`; the native Windows
|
||||
development build is `build/windows`. Release files belong under `dist/`, so
|
||||
the source tree and platform build outputs stay separate. Create a staged
|
||||
GNU/Linux install-tree archive with:
|
||||
|
||||
make package
|
||||
|
||||
This creates `dist/linux-<architecture>/dat-thang-<version>-linux-<architecture>.tar.gz`.
|
||||
The tarball contains the installed programs, headers, library, Dillo DPI, and
|
||||
manuals; the target system supplies the linked system libraries. On Windows,
|
||||
`build-windows.ps1` creates a portable ZIP and `build-windows-installer.ps1`
|
||||
creates the install wizard package.
|
||||
|
||||
To make a source-only ZIP from PowerShell without including ignored build or
|
||||
release outputs, run `./package-source.ps1`. See
|
||||
`docs/source-distribution.md` for its archive contents.
|
||||
|
||||
To include the SDL2 clients in the Linux install and release archive, install
|
||||
SDL2 and SDL2_ttf development files, then run `make ENABLE_SDL2=1 package`.
|
||||
|
||||
`NETWORK_PROTOCOL` remains the repository-root setting; the make override above
|
||||
is temporary and does not change that file.
|
||||
|
||||
S7 is bundled and required. The default build includes the embedded Scheme
|
||||
runtime, `ddat-scheme`, and the shared JSON reader used for JavaScript Dat
|
||||
metadata. `--disable-s7` is rejected because Dat metadata parsing depends on it.
|
||||
|
||||
Use `make uninstall` to remove installed dat-thang files and `make distclean` to
|
||||
remove all generated build and configuration files.
|
||||
@@ -1,3 +1,667 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
~~~~ ADDENDUM
|
||||
|
||||
Creative Communism ☭
|
||||
This is My Software
|
||||
Freedom is a spook
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
-include config.mk
|
||||
|
||||
BUILD ?= build/linux
|
||||
VERSION ?= $(shell tr -d '\r\n' < VERSION)
|
||||
PACKAGE_ARCH ?= $(shell uname -m)
|
||||
PACKAGE_STAGE ?= $(CURDIR)/$(BUILD)/package-root
|
||||
PACKAGE_DIR ?= $(CURDIR)/dist/linux-$(PACKAGE_ARCH)
|
||||
|
||||
PREFIX ?= /usr/local
|
||||
DPIDIR ?= $(PREFIX)/lib/dillo/dpi/dat
|
||||
DAT_SCHEME_DIR ?= $(PREFIX)/share/datgpt/scheme
|
||||
DESTDIR ?=
|
||||
CC ?= cc
|
||||
AR ?= ar
|
||||
RANLIB ?= ranlib
|
||||
OBJCOPY ?= objcopy
|
||||
INSTALL ?= install
|
||||
CFLAGS ?= -O2 -std=c99 -Wall -Wextra -Wpedantic
|
||||
HOST_OS := $(shell uname -s 2>/dev/null)
|
||||
ifneq (,$(findstring CYGWIN,$(HOST_OS)))
|
||||
TEST_LDFLAGS += -Wl,--stack,8388608
|
||||
endif
|
||||
CPPFLAGS ?=
|
||||
LDFLAGS ?=
|
||||
SODIUM_LIBS ?= -lsodium
|
||||
CURL_CFLAGS ?= $(shell pkg-config --cflags libcurl 2>/dev/null)
|
||||
CURL_LIBS ?= $(shell pkg-config --libs libcurl 2>/dev/null || echo -lcurl)
|
||||
ENABLE_SDL2 ?= 0
|
||||
FFMPEG_MEDIA_AVAILABLE := $(shell pkg-config --exists 'libavcodec >= 59' 'libavformat >= 59' 'libavutil >= 57' 'libswresample >= 4' 'libswscale >= 6' 2>/dev/null && echo 1 || echo 0)
|
||||
ENABLE_CABLE_MEDIA ?= 0
|
||||
ifeq ($(ENABLE_CABLE_MEDIA),1)
|
||||
ifneq ($(FFMPEG_MEDIA_AVAILABLE),1)
|
||||
$(error Cable media needs FFmpeg 5.0+ development files: libavcodec>=59, libavformat>=59, libavutil>=57, libswresample>=4, libswscale>=6)
|
||||
endif
|
||||
CABLE_MEDIA_CFLAGS = -DCABLE_HAVE_FFMPEG $(shell pkg-config --cflags libavcodec libavformat libavutil libswresample libswscale)
|
||||
CABLE_MEDIA_LIBS = $(shell pkg-config --libs libavformat libavcodec libavutil libswresample libswscale)
|
||||
endif
|
||||
# Node and Cabal modules are used only by optional interoperability tests.
|
||||
NODE ?= node
|
||||
CABAL_JS_MODULE ?= cabal-core
|
||||
CABAL_CLIENT_MODULE ?= cabal-client
|
||||
|
||||
CPPFLAGS += -I. -Iinc -Ilibcess/include -DSLEEP_SALT -DCESSPOOL_HAVE_SODIUM $(CURL_CFLAGS)
|
||||
CPPFLAGS += -DDAT_BUILD_DIR=\"$(BUILD)\"
|
||||
# Root-level NETWORK_PROTOCOL is the Cesspool peer identity and discovery
|
||||
# namespace. Hypercore-compatible protocol identifiers belong in test builds.
|
||||
NETWORK_PROTOCOL := $(shell tr -d '\r\n' < NETWORK_PROTOCOL)
|
||||
CPPFLAGS += -DDAT_NETWORK_PROTOCOL=\"$(NETWORK_PROTOCOL)\"
|
||||
CPPFLAGS += -DCESSPOOL_NETWORK_PROTOCOL=\"$(NETWORK_PROTOCOL)\"
|
||||
CPPFLAGS += -DCESSPOOL_CONFIG_DIRECTORY=\"dat-gpt\"
|
||||
ifeq ($(NETWORK_PROTOCOL),hypercore)
|
||||
CPPFLAGS += -DDAT_LEGACY_HYPERCORE
|
||||
endif
|
||||
LIBS = $(SODIUM_LIBS) $(CURL_LIBS) -lm -ldl
|
||||
LIB_SOURCES = src/base32768.c src/cable.c src/cable_feed.c src/dat.c src/dat_browse.c src/dat_dns.c src/dat_fetch.c src/dat_walk.c src/dat_wire.c src/dat_session.c \
|
||||
src/sleep.c src/sleep_tree.c src/sleep_bitfield.c \
|
||||
src/sleep_proto.c src/sleep_crypto.c src/sleep_writer.c \
|
||||
src/sleep_feed.c src/sleep_proof.c src/sleep_replication.c src/sleep_utp.c
|
||||
LIB_SOURCES += src/dat_s7.c src/dat_scheme.c src/s7.c
|
||||
S7_INSTALL = $(INSTALL) -m 755 $(BUILD)/ddat-scheme "$(DESTDIR)$(PREFIX)/bin/ddat-scheme"
|
||||
S7_SCHEME_INSTALL = $(INSTALL) -d "$(DESTDIR)$(DAT_SCHEME_DIR)" && \
|
||||
$(INSTALL) -m 644 scheme/dat-init.scm scheme/dat-record.scm scheme/dat-wire.scm scheme/dat-json.scm scheme/cable-room.scm "$(DESTDIR)$(DAT_SCHEME_DIR)/"
|
||||
S7_UNINSTALL = rm -f "$(DESTDIR)$(PREFIX)/bin/ddat-scheme" "$(DESTDIR)$(DAT_SCHEME_DIR)/dat-init.scm" "$(DESTDIR)$(DAT_SCHEME_DIR)/dat-record.scm" "$(DESTDIR)$(DAT_SCHEME_DIR)/dat-wire.scm" "$(DESTDIR)$(DAT_SCHEME_DIR)/dat-json.scm" "$(DESTDIR)$(DAT_SCHEME_DIR)/cable-room.scm"
|
||||
LIB_OBJECTS = $(LIB_SOURCES:src/%.c=$(BUILD)/%.o) $(BUILD)/sleep_verify_lib.o
|
||||
LIBCESS_OBJECTS = $(BUILD)/libcess/cess_net.o $(BUILD)/libcess/cesspool.o $(BUILD)/libcess/cess_identity.o
|
||||
PUBLIC_HEADERS = $(wildcard inc/*.h)
|
||||
TESTS = $(BUILD)/test_tree $(BUILD)/test_bitfield $(BUILD)/test_base32768 $(BUILD)/test_cable $(BUILD)/test_proto $(BUILD)/test_wire \
|
||||
$(BUILD)/test_archive $(BUILD)/test_session $(BUILD)/test_dns $(BUILD)/test_utp \
|
||||
$(BUILD)/test_discovery $(BUILD)/test_replication $(BUILD)/test_writer $(BUILD)/test_paths
|
||||
ifeq ($(ENABLE_SDL2),1)
|
||||
GUI_TESTS = $(BUILD)/test_version_bar $(BUILD)/test_gui_p2p
|
||||
CABLE_LIVE_TEST = $(BUILD)/test_cable_thang
|
||||
endif
|
||||
|
||||
.PHONY: all test install uninstall clean distclean package package-linux
|
||||
.PHONY: test-cabal-interop
|
||||
.PHONY: test-cable-live
|
||||
.PHONY: FORCE
|
||||
all: $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(BUILD)/sleep-verify $(BUILD)/dat $(BUILD)/ddat $(BUILD)/cable $(BUILD)/cesspool-bootstrap $(BUILD)/dat.dpi
|
||||
all: $(BUILD)/ddat-scheme
|
||||
ifeq ($(ENABLE_SDL2),1)
|
||||
all: $(BUILD)/dat-thang $(BUILD)/cable-thang
|
||||
endif
|
||||
|
||||
$(BUILD):
|
||||
mkdir -p $(BUILD)
|
||||
|
||||
# A protocol identity changes wire bytes and must never reuse objects built for
|
||||
# another value (including make command-line overrides used for compatibility).
|
||||
$(BUILD)/.network-protocol: FORCE NETWORK_PROTOCOL | $(BUILD)
|
||||
@protocol="$(NETWORK_PROTOCOL)"; \
|
||||
if test ! -f $@ || test "$$(cat $@)" != "$$protocol"; then \
|
||||
printf '%s\n' "$$protocol" > $@.tmp; mv $@.tmp $@; \
|
||||
fi
|
||||
|
||||
$(BUILD)/%.o: src/%.c NETWORK_PROTOCOL $(BUILD)/.network-protocol | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/libcess:
|
||||
mkdir -p $@
|
||||
|
||||
$(BUILD)/libcess/%.o: libcess/src/%.c libcess/include/cesspool.h libcess/include/cess_net.h libcess/include/cess_compat.h NETWORK_PROTOCOL $(BUILD)/.network-protocol | $(BUILD)/libcess
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/libcess.a: $(LIBCESS_OBJECTS)
|
||||
$(AR) rcs $@ $(LIBCESS_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
# S7 is an upstream single-file interpreter with noisy legacy diagnostics.
|
||||
# Suppression applies only to that vendored translation unit; our host stays
|
||||
# under the normal project warning flags.
|
||||
$(BUILD)/s7.o: src/s7.c NETWORK_PROTOCOL $(BUILD)/.network-protocol | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/sleep_verify_lib.o: src/sleep_verify.c $(BUILD)/.network-protocol | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -DSLEEP_VERIFY_NO_MAIN -c $< -o $@
|
||||
|
||||
-include $(LIB_OBJECTS:$(BUILD)/%.o=$(BUILD)/%.d)
|
||||
|
||||
$(BUILD)/libdatgpt.a: $(LIB_OBJECTS) $(BUILD)/.network-protocol
|
||||
$(AR) rcs $@ $(LIB_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
# The DPI uses S7's reader for dat.scm, treating archive metadata as data and
|
||||
# never evaluating archive-supplied Scheme forms.
|
||||
DPI_LIB_OBJECTS = $(patsubst $(BUILD)/%.o,$(BUILD)/dpi/%.o,$(filter-out $(BUILD)/dat_s7.o $(BUILD)/dat_scheme.o $(BUILD)/s7.o,$(LIB_OBJECTS)))
|
||||
$(BUILD)/dpi:
|
||||
mkdir -p $@
|
||||
|
||||
$(BUILD)/dpi/%.o: src/%.c NETWORK_PROTOCOL $(BUILD)/.network-protocol | $(BUILD)/dpi
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/dpi/sleep_verify_lib.o: src/sleep_verify.c NETWORK_PROTOCOL $(BUILD)/.network-protocol | $(BUILD)/dpi
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -DSLEEP_VERIFY_NO_MAIN -c $< -o $@
|
||||
|
||||
$(BUILD)/dpi_s7.o: $(BUILD)/s7.o | $(BUILD)
|
||||
$(OBJCOPY) --redefine-sym main=dat_dpi_s7_standalone_main $< $@
|
||||
|
||||
-include $(DPI_LIB_OBJECTS:$(BUILD)/dpi/%.o=$(BUILD)/dpi/%.d) $(LIBCESS_OBJECTS:$(BUILD)/libcess/%.o=$(BUILD)/libcess/%.d)
|
||||
|
||||
$(BUILD)/libdatgpt-dpi.a: $(DPI_LIB_OBJECTS) $(BUILD)/.network-protocol
|
||||
$(AR) rcs $@ $(DPI_LIB_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(BUILD)/sleep-verify: src/sleep_verify.c $(BUILD)/libdatgpt.a $(BUILD)/.network-protocol
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) src/sleep_verify.c $(BUILD)/libdatgpt.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/dat: src/dat_cli.c NETWORK_PROTOCOL $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(BUILD)/.network-protocol
|
||||
$(CC) $(CPPFLAGS) -DDAT_CLI_NAME=\"dat\" $(CFLAGS) $(LDFLAGS) src/dat_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/ddat: src/dat_cli.c NETWORK_PROTOCOL $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(BUILD)/.network-protocol
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) src/dat_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/cable: src/cable_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/cesspool-bootstrap: libcess/tools/cesspool-bootstrap.c $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $< $(BUILD)/libcess.a $(SODIUM_LIBS) -o $@
|
||||
|
||||
$(BUILD)/ddat-scheme: src/dat_scheme_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) -DDAT_SCHEME_DIR=\"$(DAT_SCHEME_DIR)\" $(CFLAGS) $(LDFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
SDL2_CFLAGS ?= $(shell pkg-config --cflags sdl2)
|
||||
SDL2_LIBS ?= $(shell pkg-config --libs sdl2)
|
||||
SDL2_TTF_CFLAGS ?= $(shell pkg-config --cflags SDL2_ttf)
|
||||
SDL2_TTF_LIBS ?= $(shell pkg-config --libs SDL2_ttf)
|
||||
DAT_FONT_PATH ?= $(PREFIX)/share/datgpt/fonts/MorePerfectDOSVGA.ttf
|
||||
DAT_UNIFONT_PATH ?= $(PREFIX)/share/datgpt/fonts/Unifont-18.0.01.otf
|
||||
DAT_SPEAKER_PATH ?= $(PREFIX)/share/datgpt/icons/datthang-speaker.bmp
|
||||
ifeq ($(ENABLE_SDL2),1)
|
||||
GUI_CPPFLAGS = -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DDAT_FONT_PATH=\"$(DAT_FONT_PATH)\" -DDAT_UNIFONT_PATH=\"$(DAT_UNIFONT_PATH)\" -DDAT_SPEAKER_PATH=\"$(DAT_SPEAKER_PATH)\"
|
||||
GUI_INSTALL = rm -f "$(DESTDIR)$(PREFIX)/bin/ddat-gui" && \
|
||||
$(INSTALL) -d "$(DESTDIR)$(PREFIX)/share/datgpt/fonts" "$(DESTDIR)$(PREFIX)/share/datgpt/icons" && \
|
||||
$(INSTALL) -m 644 assets/fonts/MorePerfectDOSVGA.ttf "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/MorePerfectDOSVGA.ttf" && \
|
||||
$(INSTALL) -m 644 assets/fonts/Unifont-18.0.01.otf "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/Unifont-18.0.01.otf" && \
|
||||
$(INSTALL) -m 644 assets/fonts/Unifont-OFL-1.1.txt "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/Unifont-OFL-1.1.txt" && \
|
||||
$(INSTALL) -m 644 assets/datthang-speaker.bmp "$(DESTDIR)$(PREFIX)/share/datgpt/icons/datthang-speaker.bmp" && \
|
||||
$(INSTALL) -m 755 $(BUILD)/dat-thang "$(DESTDIR)$(PREFIX)/bin/dat-thang" && \
|
||||
$(INSTALL) -m 644 docs/dat-thang.1 "$(DESTDIR)$(PREFIX)/share/man/man1/dat-thang.1" && \
|
||||
$(INSTALL) -m 755 $(BUILD)/cable-thang "$(DESTDIR)$(PREFIX)/bin/cable-thang" && \
|
||||
$(INSTALL) -m 644 docs/cable-thang.1 "$(DESTDIR)$(PREFIX)/share/man/man1/cable-thang.1" && \
|
||||
$(INSTALL) -m 644 docs/ddat-gui.md "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/ddat-gui.md"
|
||||
else
|
||||
GUI_INSTALL = :
|
||||
endif
|
||||
$(BUILD)/dat-thang: src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a assets/fonts/MorePerfectDOSVGA.ttf assets/datthang-speaker.bmp | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) -DDAT_THANG_VERSION=\"v$(VERSION)\" $(GUI_CPPFLAGS) $(CFLAGS) $(SDL2_CFLAGS) $(SDL2_TTF_CFLAGS) $(LDFLAGS) src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL2_TTF_LIBS) $(SDL2_LIBS) $(CABLE_MEDIA_LIBS) -lm -o $@
|
||||
|
||||
$(BUILD)/cable_media.o: src/cable_media.c inc/cable_media.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(CABLE_MEDIA_CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/cable_video.o: src/cable_video.c inc/cable_video.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(SDL2_CFLAGS) $(CFLAGS) $(CABLE_MEDIA_CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/cable-thang: src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a assets/fonts/MorePerfectDOSVGA.ttf assets/datthang-speaker.bmp | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) -DDAT_THANG_VERSION=\"v$(VERSION)\" $(GUI_CPPFLAGS) $(CFLAGS) $(SDL2_CFLAGS) $(SDL2_TTF_CFLAGS) $(LDFLAGS) src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL2_TTF_LIBS) $(SDL2_LIBS) $(CABLE_MEDIA_LIBS) -lm -o $@
|
||||
|
||||
.PHONY: gui
|
||||
gui: $(BUILD)/dat-thang
|
||||
|
||||
# Native Dillo DPI plugin; dpid supplies an authenticated socket on stdin/stdout.
|
||||
$(BUILD)/dat.dpi: src/dat_dpi.c src/dat_dpi_icons.h NETWORK_PROTOCOL $(BUILD)/libdatgpt-dpi.a $(BUILD)/libcess.a $(BUILD)/dpi_s7.o $(BUILD)/.network-protocol
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) src/dat_dpi.c $(BUILD)/libdatgpt-dpi.a $(BUILD)/libcess.a $(BUILD)/dpi_s7.o $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/test_%: tests/test_%.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/test_dpi_socket: tests/test_dpi_socket.c | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $< -o $@
|
||||
|
||||
$(BUILD)/test_gui_p2p: tests/test_gui_p2p.c src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a assets/fonts/MorePerfectDOSVGA.ttf assets/datthang-speaker.bmp
|
||||
$(CC) $(CPPFLAGS) $(GUI_CPPFLAGS) $(CFLAGS) $(SDL2_CFLAGS) $(SDL2_TTF_CFLAGS) $(LDFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL2_TTF_LIBS) $(SDL2_LIBS) $(CABLE_MEDIA_LIBS) -lm -o $@
|
||||
|
||||
$(BUILD)/test_version_bar: tests/test_version_bar.c src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a assets/fonts/MorePerfectDOSVGA.ttf assets/datthang-speaker.bmp
|
||||
$(CC) $(CPPFLAGS) $(GUI_CPPFLAGS) $(CFLAGS) $(SDL2_CFLAGS) $(SDL2_TTF_CFLAGS) $(LDFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL2_TTF_LIBS) $(SDL2_LIBS) $(CABLE_MEDIA_LIBS) -lm -o $@
|
||||
|
||||
$(BUILD)/test_cable_thang: tests/test_cable_thang.c src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a assets/fonts/MorePerfectDOSVGA.ttf assets/datthang-speaker.bmp
|
||||
$(CC) $(CPPFLAGS) $(GUI_CPPFLAGS) $(CFLAGS) $(SDL2_CFLAGS) $(SDL2_TTF_CFLAGS) $(LDFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL2_TTF_LIBS) $(SDL2_LIBS) $(CABLE_MEDIA_LIBS) -lm -o $@
|
||||
|
||||
$(TESTS): $(PUBLIC_HEADERS)
|
||||
|
||||
test: all $(TESTS) $(GUI_TESTS) $(BUILD)/test_dpi_socket
|
||||
@echo "TEST Cable address CLI"
|
||||
@$(BUILD)/cable address cabal://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | grep -q '^cable://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$$'
|
||||
@$(BUILD)/cable address hyper://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | grep -q '^cable://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$$'
|
||||
@echo "TEST Cabal JS/Cable side-by-side room and user identity"
|
||||
@if command -v "$(NODE)" >/dev/null 2>&1 && CABAL_JS_MODULE="$(CABAL_JS_MODULE)" "$(NODE)" -e 'require(process.env.CABAL_JS_MODULE)' >/dev/null 2>&1; then \
|
||||
CABAL_JS_MODULE="$(CABAL_JS_MODULE)" CABAL_BIN="$(CURDIR)/$(BUILD)/cable" "$(NODE)" tests/interop/cabal/cabal_cable_side_by_side.js; \
|
||||
else echo "SKIP Cabal JS/Cable interop (run make test-cabal-interop on a host with Node and cabal-core installed)"; fi
|
||||
@if command -v "$(NODE)" >/dev/null 2>&1 && CABAL_CLIENT_MODULE="$(CABAL_CLIENT_MODULE)" "$(NODE)" -e 'require(process.env.CABAL_CLIENT_MODULE)' >/dev/null 2>&1; then \
|
||||
CABAL_CLIENT_MODULE="$(CABAL_CLIENT_MODULE)" "$(NODE)" tests/interop/cabal/cabal_cli_features.js; \
|
||||
else echo "SKIP cabal-cli multi-peer workflows (run make test-cabal-interop on a host with Node and cabal-client installed)"; fi
|
||||
@if test "$(ENABLE_SDL2)" = 1 && command -v "$(NODE)" >/dev/null 2>&1 && CABAL_JS_MODULE="$(CABAL_JS_MODULE)" "$(NODE)" -e 'require(process.env.CABAL_JS_MODULE)' >/dev/null 2>&1; then \
|
||||
CABAL_JS_MODULE="$(CABAL_JS_MODULE)" CABLE_CABAL_CORE="$(CABAL_JS_MODULE)" "$(NODE)" tests/interop/cabal/cable_js_c_live.js $(BUILD)/test_cable_thang; \
|
||||
else echo "SKIP C SDL/Cabal JS live test (use make test-cable-live ENABLE_SDL2=1 with Node and cabal-core installed)"; fi
|
||||
@echo "TEST bundled S7 JSON/S-expression adapter"
|
||||
@json_output=$$($(BUILD)/ddat-scheme tests/test-json.scm 2>&1); \
|
||||
printf '%s\n' "$$json_output"; \
|
||||
printf '%s\n' "$$json_output" | grep -q '^S7 JSON/S-expression adapter: PASS$$'; \
|
||||
! printf '%s\n' "$$json_output" | grep -q '^;'
|
||||
@if command -v "$(NODE)" >/dev/null 2>&1; then "$(NODE)" tests/interop/cabal/cable_sexpr_boundary.js; fi
|
||||
@set -e; for test_program in $(TESTS); do echo "TEST $$test_program"; $$test_program; done
|
||||
@set -e; for test_program in $(GUI_TESTS); do echo "TEST $$test_program"; SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy $$test_program; done
|
||||
@echo "TEST current Dillo DPIP helper protocol"
|
||||
@sh tests/test_dpi.sh $(BUILD)/dat.dpi $(BUILD)/dat $(BUILD)/test_dpi_socket
|
||||
@set -e; cli_root=$$(mktemp -d /tmp/datgpt-cli.XXXXXX); mkdir -p "$$cli_root/home"; \
|
||||
echo "TEST dat doctor local network primitives"; \
|
||||
$(BUILD)/ddat doctor | grep -q '^cesspool: UDP socket bound on 127.0.0.1:'; \
|
||||
$(BUILD)/ddat doctor 0000000000000000000000000000000000000000000000000000000000000000 | grep -q '^address: dat://'; \
|
||||
$(BUILD)/ddat --help | grep -q '^ddat '; \
|
||||
echo "TEST dat create/sync/log and dat:// address"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat create "$$cli_root/archive" --title "hello" --description "archive test" > "$$cli_root/url"; \
|
||||
grep -q '^dat://' "$$cli_root/url"; \
|
||||
cp "$$cli_root/url" "$$cli_root/original-url"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat dns set archive.example.test "$$cli_root/archive" > "$$cli_root/dns-set"; \
|
||||
grep -q '^DAT-DNS /.well-known/dat:$$' "$$cli_root/dns-set"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat resolve archive.example.test | grep -q '^dat://'; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat dns list | grep -q '^archive.example.test[[:space:]]'; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat dns remove archive.example.test; \
|
||||
mkdir -p "$$cli_root/empty-home"; \
|
||||
cp "$$cli_root/archive/.dat/metadata.key" "$$cli_root/metadata-key-before-readonly-sync"; \
|
||||
if HOME="$$cli_root/empty-home" $(BUILD)/dat sync "$$cli_root/archive" >/dev/null 2>&1; then exit 1; fi; \
|
||||
cmp "$$cli_root/metadata-key-before-readonly-sync" "$$cli_root/archive/.dat/metadata.key"; \
|
||||
grep -q '(title "hello")' "$$cli_root/archive/dat.scm"; \
|
||||
grep -q '(description "archive test")' "$$cli_root/archive/dat.scm"; \
|
||||
grep -q '(url "dat://' "$$cli_root/archive/dat.scm"; \
|
||||
$(BUILD)/dat log "$$cli_root/archive" | grep -q dat.scm; \
|
||||
test ! -e "$$cli_root/archive/dat.json"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat create "$$cli_root/empty" --yes > /dev/null; \
|
||||
test -f "$$cli_root/empty/dat.scm"; \
|
||||
test ! -e "$$cli_root/empty/dat.json"; \
|
||||
mkdir -p "$$cli_root/scheme-created"; \
|
||||
$(BUILD)/ddat-scheme -e "(dat-close (dat-create \"$$cli_root/scheme-created\"))" >/dev/null; \
|
||||
grep -q '(title "scheme-created")' "$$cli_root/scheme-created/dat.scm"; \
|
||||
$(BUILD)/dat log "$$cli_root/scheme-created" | grep -q dat.scm; \
|
||||
echo hello > "$$cli_root/archive/hello.txt"; \
|
||||
echo hidden > "$$cli_root/archive/.hidden"; \
|
||||
echo '*.skip' > "$$cli_root/archive/.datignore"; \
|
||||
echo ignored > "$$cli_root/archive/ignored.skip"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat sync "$$cli_root/archive" > /dev/null; \
|
||||
$(BUILD)/dat status "$$cli_root/archive" | sed -n 's/^Archive: //p' > "$$cli_root/updated-url"; \
|
||||
cmp "$$cli_root/original-url" "$$cli_root/updated-url"; \
|
||||
$(BUILD)/dat status "$$cli_root/archive" | grep -q 'Metadata entries:'; \
|
||||
$(BUILD)/dat url --compact '' "$$cli_root/archive" | grep -q '^dat://'; \
|
||||
$(BUILD)/dat files "$$cli_root/archive" | grep -q hello.txt; \
|
||||
$(BUILD)/dat verify "$$cli_root/archive" >/dev/null; \
|
||||
$(BUILD)/dat log "$$cli_root/archive" | grep -q hello.txt; \
|
||||
! $(BUILD)/dat log "$$cli_root/archive" | grep -qE '\.(hidden|datignore)|ignored\.skip'; \
|
||||
old_version=$$($(BUILD)/dat log "$$cli_root/archive" | awk '/hello.txt/{print $$1; exit}'); \
|
||||
echo newer > "$$cli_root/archive/hello.txt"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat sync "$$cli_root/archive" > /dev/null; \
|
||||
test "$$($(BUILD)/dat cat "$$old_version" /hello.txt "$$cli_root/archive")" = hello; \
|
||||
$(BUILD)/dat url "$$old_version" /hello.txt "$$cli_root/archive" | \
|
||||
grep -Eq "^dat://[0-9a-f]{64}\\+$$old_version/hello.txt$$"; \
|
||||
$(BUILD)/dat checkout "$$old_version" "$$cli_root/snapshot" "$$cli_root/archive"; \
|
||||
test "$$(cat "$$cli_root/snapshot/hello.txt")" = hello; \
|
||||
versions_before=$$($(BUILD)/dat log "$$cli_root/archive" | wc -l); \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat sync "$$cli_root/archive" > /dev/null; \
|
||||
test "$$($(BUILD)/dat log "$$cli_root/archive" | wc -l)" = "$$versions_before"; \
|
||||
$(BUILD)/dat files "$$old_version" "$$cli_root/archive" | grep -q hello.txt; \
|
||||
rm "$$cli_root/archive/hello.txt"; \
|
||||
HOME="$$cli_root/home" $(BUILD)/dat sync "$$cli_root/archive" > /dev/null; \
|
||||
$(BUILD)/dat log "$$cli_root/archive" | grep -q 'del .*hello.txt'
|
||||
@echo "TEST historical .dat fixture"
|
||||
@$(BUILD)/sleep-verify .dat
|
||||
@echo "TEST dat/ddat side-by-side archive compatibility"
|
||||
@sh tests/test_cli_parity.sh $(BUILD)/dat $(BUILD)/ddat
|
||||
|
||||
test-cabal-interop: $(BUILD)/cable
|
||||
CABAL_JS_MODULE="$(CABAL_JS_MODULE)" CABAL_BIN="$(CURDIR)/$(BUILD)/cable" "$(NODE)" tests/interop/cabal/cabal_cable_side_by_side.js
|
||||
CABAL_CLIENT_MODULE="$(CABAL_CLIENT_MODULE)" "$(NODE)" tests/interop/cabal/cabal_cli_features.js
|
||||
CABAL_JS_MODULE="$(CABAL_JS_MODULE)" CABLE_CABAL_CORE="$(CABAL_JS_MODULE)" "$(NODE)" tests/interop/cabal/cabal_core_hyperswarm_live.js
|
||||
|
||||
ifeq ($(ENABLE_SDL2),1)
|
||||
test-cable-live: $(BUILD)/test_cable_thang
|
||||
CABAL_JS_MODULE="$(CABAL_JS_MODULE)" CABLE_CABAL_CORE="$(CABAL_JS_MODULE)" "$(NODE)" tests/interop/cabal/cable_js_c_live.js $(BUILD)/test_cable_thang
|
||||
else
|
||||
test-cable-live:
|
||||
@echo "test-cable-live requires ENABLE_SDL2=1, SDL2, Node.js, and Cabal Core" >&2
|
||||
@exit 2
|
||||
endif
|
||||
|
||||
install: all
|
||||
$(INSTALL) -d "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include/datgpt" "$(DESTDIR)$(PREFIX)/include/libcess" "$(DESTDIR)$(PREFIX)/bin" "$(DESTDIR)$(PREFIX)/share/man/man1" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt" "$(DESTDIR)$(DPIDIR)"
|
||||
$(INSTALL) -m 644 $(BUILD)/libdatgpt.a "$(DESTDIR)$(PREFIX)/lib/libdatgpt.a"
|
||||
$(INSTALL) -m 644 $(BUILD)/libcess.a "$(DESTDIR)$(PREFIX)/lib/libcess.a"
|
||||
$(INSTALL) -m 644 $(PUBLIC_HEADERS) "$(DESTDIR)$(PREFIX)/include/datgpt/"
|
||||
$(INSTALL) -m 644 libcess/include/cesspool.h libcess/include/cess_net.h libcess/include/cess_compat.h "$(DESTDIR)$(PREFIX)/include/libcess/"
|
||||
$(INSTALL) -m 755 $(BUILD)/sleep-verify "$(DESTDIR)$(PREFIX)/bin/sleep-verify"
|
||||
$(INSTALL) -m 755 $(BUILD)/ddat "$(DESTDIR)$(PREFIX)/bin/ddat"
|
||||
$(INSTALL) -m 755 $(BUILD)/cable "$(DESTDIR)$(PREFIX)/bin/cable"
|
||||
$(INSTALL) -m 755 $(BUILD)/cesspool-bootstrap "$(DESTDIR)$(PREFIX)/bin/cesspool-bootstrap"
|
||||
$(S7_INSTALL)
|
||||
$(GUI_INSTALL)
|
||||
$(S7_SCHEME_INSTALL)
|
||||
$(INSTALL) -m 755 $(BUILD)/dat.dpi "$(DESTDIR)$(DPIDIR)/dat.dpi"
|
||||
$(INSTALL) -m 644 docs/dat.1 "$(DESTDIR)$(PREFIX)/share/man/man1/dat.1"
|
||||
$(INSTALL) -m 644 docs/cable.1 "$(DESTDIR)$(PREFIX)/share/man/man1/cable.1"
|
||||
$(INSTALL) -m 644 docs/cesspool-bootstrap.1 "$(DESTDIR)$(PREFIX)/share/man/man1/cesspool-bootstrap.1"
|
||||
$(INSTALL) -m 644 docs/libcess.md "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/libcess.md"
|
||||
$(INSTALL) -m 644 docs/key-management.md docs/cable-interop.md docs/ddat-gui.md docs/dillo.md "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/"
|
||||
|
||||
# Build an install-tree archive suitable for Linux distribution and testing.
|
||||
# System libraries remain explicit runtime dependencies; this is not an AppImage.
|
||||
package: package-linux
|
||||
|
||||
package-linux: all
|
||||
rm -rf "$(PACKAGE_STAGE)"
|
||||
$(MAKE) BUILD="$(BUILD)" DESTDIR="$(PACKAGE_STAGE)" PREFIX=/usr install
|
||||
mkdir -p "$(PACKAGE_DIR)"
|
||||
tar -czf "$(PACKAGE_DIR)/dat-gpt-$(VERSION)-linux-$(PACKAGE_ARCH).tar.gz" -C "$(PACKAGE_STAGE)" .
|
||||
|
||||
uninstall:
|
||||
rm -f "$(DESTDIR)$(PREFIX)/lib/libdatgpt.a" "$(DESTDIR)$(PREFIX)/lib/libcess.a" "$(DESTDIR)$(PREFIX)/include/libcess/cesspool.h" "$(DESTDIR)$(PREFIX)/include/libcess/cess_net.h" "$(DESTDIR)$(PREFIX)/include/libcess/cess_compat.h" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/libcess.md" "$(DESTDIR)$(PREFIX)/bin/sleep-verify" "$(DESTDIR)$(PREFIX)/bin/ddat" "$(DESTDIR)$(PREFIX)/bin/cable" "$(DESTDIR)$(PREFIX)/bin/cesspool-bootstrap" "$(DESTDIR)$(PREFIX)/bin/ddat-scheme" "$(DESTDIR)$(PREFIX)/bin/ddat-gui" "$(DESTDIR)$(PREFIX)/bin/dat-thang" "$(DESTDIR)$(PREFIX)/bin/cable-thang" "$(DESTDIR)$(DPIDIR)/dat.dpi" "$(DESTDIR)$(PREFIX)/share/datgpt/cable_hyperswarm.js" "$(DESTDIR)$(PREFIX)/share/datgpt/cable_bootstrap.js" "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/MorePerfectDOSVGA.ttf" "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/Unifont-18.0.01.otf" "$(DESTDIR)$(PREFIX)/share/datgpt/fonts/Unifont-OFL-1.1.txt" "$(DESTDIR)$(PREFIX)/share/datgpt/icons/datthang-speaker.bmp" "$(DESTDIR)$(PREFIX)/share/man/man1/dat.1" "$(DESTDIR)$(PREFIX)/share/man/man1/cable.1" "$(DESTDIR)$(PREFIX)/share/man/man1/cesspool-bootstrap.1" "$(DESTDIR)$(PREFIX)/share/man/man1/dat-thang.1" "$(DESTDIR)$(PREFIX)/share/man/man1/cable-thang.1" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/key-management.md" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/cable-interop.md" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/ddat-gui.md" "$(DESTDIR)$(PREFIX)/share/doc/dat-gpt/dillo.md"
|
||||
$(S7_UNINSTALL)
|
||||
@for header in $(notdir $(PUBLIC_HEADERS)); do rm -f "$(DESTDIR)$(PREFIX)/include/datgpt/$$header"; done
|
||||
|
||||
clean:
|
||||
@if test -d "$(BUILD)"; then find "$(BUILD)" -type f \( -name '*.o' -o -name '*.d' -o -name '*.a' -o -name '*.so' -o -name '*.so.*' -o -name '*.dll' -o -name '*.exe' \) -delete; fi
|
||||
rm -f "$(BUILD)/.network-protocol" "$(BUILD)/.network-protocol.tmp" "$(BUILD)/libdatgpt.a" "$(BUILD)/libdatgpt-dpi.a" "$(BUILD)/libcess.a" "$(BUILD)/dat" "$(BUILD)/ddat" "$(BUILD)/cable" "$(BUILD)/cesspool-bootstrap" "$(BUILD)/sleep-verify" "$(BUILD)/ddat-scheme" "$(BUILD)/dat.dpi" "$(BUILD)/dat-thang" "$(BUILD)/cable-thang" "$(BUILD)/test_"* "$(BUILD)/test-*"
|
||||
rm -rf "$(BUILD)/dpi" "$(BUILD)/libcess"
|
||||
$(MAKE) -C libcess clean
|
||||
|
||||
distclean: clean
|
||||
rm -f config.mk config.h config.log
|
||||
@@ -0,0 +1,150 @@
|
||||
CC := gcc
|
||||
AR := ar
|
||||
RANLIB := ranlib
|
||||
WINDRES := windres
|
||||
BUILD := build/windows
|
||||
VERSION ?= $(shell tr -d '\r\n' < VERSION)
|
||||
NETWORK_PROTOCOL := $(strip $(file < NETWORK_PROTOCOL))
|
||||
CPPFLAGS := -I. -Iinc -Ilibcess/include -DSLEEP_SALT -DCESSPOOL_HAVE_SODIUM -DCESSPOOL_CONFIG_DIRECTORY=\"dat-gpt\" -DCESSPOOL_NETWORK_PROTOCOL=\"$(NETWORK_PROTOCOL)\" -include inc/win_compat.h -DDAT_BUILD_DIR=\"$(BUILD)\" -DDAT_NETWORK_PROTOCOL=\"$(NETWORK_PROTOCOL)\" $(shell pkg-config --cflags libcurl)
|
||||
CFLAGS := -O2 -std=c99 -Wall -Wextra -Wpedantic
|
||||
TEST_LDFLAGS := -Wl,--stack,8388608
|
||||
SODIUM_LIBS := $(shell pkg-config --libs libsodium)
|
||||
CURL_LIBS := $(shell pkg-config --libs libcurl)
|
||||
SDL_CFLAGS := $(shell pkg-config --cflags sdl2 SDL2_ttf)
|
||||
SDL_LIBS := $(shell pkg-config --libs sdl2 SDL2_ttf)
|
||||
ENABLE_CABLE_MEDIA ?= 0
|
||||
FFMPEG_MEDIA_AVAILABLE := $(shell pkg-config --exists 'libavcodec >= 59' 'libavformat >= 59' 'libavutil >= 57' 'libswresample >= 4' 'libswscale >= 6' 2>/dev/null && echo 1 || echo 0)
|
||||
ifeq ($(ENABLE_CABLE_MEDIA),1)
|
||||
ifneq ($(FFMPEG_MEDIA_AVAILABLE),1)
|
||||
$(error Cable media needs FFmpeg 5.0+ development packages: libavcodec, libavformat, libavutil, libswresample, libswscale)
|
||||
endif
|
||||
FFMPEG_MEDIA_CFLAGS := -DCABLE_HAVE_FFMPEG $(shell pkg-config --cflags libavformat libavcodec libavutil libswresample libswscale)
|
||||
FFMPEG_MEDIA_LIBS := $(shell pkg-config --libs libavformat libavcodec libavutil libswresample libswscale)
|
||||
endif
|
||||
LIBS := $(SODIUM_LIBS) $(CURL_LIBS) -lws2_32 -lm
|
||||
|
||||
LIB_SOURCES := base32768 cable cable_feed dat dat_browse dat_dns dat_walk dat_wire dat_session sleep sleep_tree sleep_bitfield sleep_proto sleep_crypto sleep_writer sleep_feed sleep_proof sleep_replication sleep_utp dat_s7 dat_scheme s7
|
||||
LIB_OBJECTS := $(addprefix $(BUILD)/,$(addsuffix .o,$(LIB_SOURCES))) $(BUILD)/sleep_verify_lib.o
|
||||
LIBCESS_OBJECTS := $(BUILD)/libcess/cess_net.o $(BUILD)/libcess/cesspool.o $(BUILD)/libcess/cess_identity.o
|
||||
|
||||
.PHONY: all clean test test-paths test-discovery test-version-bar test-gui-p2p test-gui-media package-assets
|
||||
.PHONY: FORCE
|
||||
WINDOWS_PROGRAMS := $(addprefix $(BUILD)/,dat.exe ddat.exe cable.exe cesspool-bootstrap.exe sleep-verify.exe ddat-scheme.exe dat-thang.exe cable-thang.exe)
|
||||
all: $(BUILD)/libcess.a $(WINDOWS_PROGRAMS) package-assets
|
||||
|
||||
$(BUILD):
|
||||
mkdir -p $(BUILD)
|
||||
|
||||
$(BUILD)/.network-protocol-$(NETWORK_PROTOCOL): NETWORK_PROTOCOL | $(BUILD)
|
||||
@echo $(NETWORK_PROTOCOL) > "$@"
|
||||
|
||||
$(BUILD)/%.o: src/%.c inc/win_compat.h NETWORK_PROTOCOL $(BUILD)/.network-protocol-$(NETWORK_PROTOCOL) | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/libcess:
|
||||
mkdir -p $@
|
||||
|
||||
$(BUILD)/libcess/%.o: libcess/src/%.c libcess/include/cesspool.h libcess/include/cess_net.h NETWORK_PROTOCOL $(BUILD)/.network-protocol-$(NETWORK_PROTOCOL) | $(BUILD)/libcess
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/libcess.a: $(LIBCESS_OBJECTS)
|
||||
$(AR) rcs $@ $(LIBCESS_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(BUILD)/s7.o: src/s7.c inc/win_compat.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/dat_thang_icon.o: assets/dat-thang.rc assets/dat-thang.ico | $(BUILD)
|
||||
$(WINDRES) -i $< -O coff -o $@
|
||||
|
||||
$(BUILD)/cable_media.o: src/cable_media.c inc/cable_media.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(FFMPEG_MEDIA_CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/cable_video.o: src/cable_video.c inc/cable_video.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(SDL_CFLAGS) $(FFMPEG_MEDIA_CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/sleep_verify_lib.o: src/sleep_verify.c | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -DSLEEP_VERIFY_NO_MAIN -MMD -MP -c $< -o $@
|
||||
|
||||
-include $(LIB_OBJECTS:.o=.d) $(LIBCESS_OBJECTS:.o=.d)
|
||||
|
||||
$(BUILD)/libdatgpt.a: $(LIB_OBJECTS) $(BUILD)/libcess.a
|
||||
$(AR) rcs $@ $(LIB_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(BUILD)/dat.exe: src/dat_cli.c $(BUILD)/libdatgpt.a
|
||||
$(CC) $(CPPFLAGS) -DDAT_CLI_NAME=\"dat\" $(CFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/ddat.exe: src/dat_cli.c $(BUILD)/libdatgpt.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/cable.exe: src/cable_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/cesspool-bootstrap.exe: libcess/tools/cesspool-bootstrap.c $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< $(BUILD)/libcess.a $(SODIUM_LIBS) -lws2_32 -o $@
|
||||
|
||||
$(BUILD)/sleep-verify.exe: src/sleep_verify.c $(BUILD)/libdatgpt.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/ddat-scheme.exe: src/dat_scheme_cli.c $(BUILD)/libdatgpt.a $(BUILD)/libcess.a
|
||||
$(CC) $(CPPFLAGS) -DDAT_SCHEME_DIR=\"scheme\" $(CFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
$(BUILD)/test-paths.exe: tests/test_paths.c inc/dat_paths.h inc/win_compat.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@
|
||||
|
||||
CORE_TESTS := tree bitfield base32768 proto wire dns replication writer
|
||||
|
||||
$(BUILD)/test-%.exe: tests/test_%.c $(BUILD)/libdatgpt.a | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
test: test-paths test-discovery test-version-bar test-gui-p2p $(addprefix $(BUILD)/test-,$(addsuffix .exe,$(CORE_TESTS)))
|
||||
$(BUILD)/test-tree.exe
|
||||
$(BUILD)/test-bitfield.exe
|
||||
$(BUILD)/test-base32768.exe
|
||||
$(BUILD)/test-proto.exe
|
||||
$(BUILD)/test-wire.exe
|
||||
$(BUILD)/test-dns.exe
|
||||
$(BUILD)/test-replication.exe
|
||||
$(BUILD)/test-writer.exe
|
||||
|
||||
test-paths: $(BUILD)/test-paths.exe
|
||||
$(BUILD)/test-paths.exe
|
||||
|
||||
$(BUILD)/test-discovery.exe: tests/test_discovery.c $(BUILD)/libdatgpt.a | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_LDFLAGS) $< $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) -o $@
|
||||
|
||||
test-discovery: $(BUILD)/test-discovery.exe
|
||||
$(BUILD)/test-discovery.exe
|
||||
|
||||
$(BUILD)/test-version-bar.exe: tests/test_version_bar.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_LDFLAGS) $(SDL_CFLAGS) $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL_LIBS) $(FFMPEG_MEDIA_LIBS) -o $@
|
||||
|
||||
test-version-bar: $(BUILD)/test-version-bar.exe
|
||||
$(BUILD)/test-version-bar.exe
|
||||
|
||||
$(BUILD)/test-gui-p2p.exe: tests/test_gui_p2p.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_LDFLAGS) $(SDL_CFLAGS) -DSDL_MAIN_HANDLED -Umain $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL_LIBS) $(FFMPEG_MEDIA_LIBS) -mconsole -o $@
|
||||
|
||||
test-gui-p2p: $(BUILD)/test-gui-p2p.exe
|
||||
$(BUILD)/test-gui-p2p.exe
|
||||
|
||||
$(BUILD)/test-gui-media.exe: tests/test_gui_p2p.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_LDFLAGS) $(SDL_CFLAGS) -DDAT_GUI_TEST_MEDIA -DSDL_MAIN_HANDLED -Umain $< $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL_LIBS) $(FFMPEG_MEDIA_LIBS) -mconsole -o $@
|
||||
|
||||
test-gui-media: $(BUILD)/test-gui-media.exe
|
||||
$(BUILD)/test-gui-media.exe
|
||||
|
||||
$(BUILD)/dat-thang.exe: src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/dat_thang_icon.o
|
||||
$(CC) $(CPPFLAGS) -DDAT_THANG_VERSION=\"v$(VERSION)\" $(CFLAGS) $(SDL_CFLAGS) src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/dat_thang_icon.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL_LIBS) $(FFMPEG_MEDIA_LIBS) -o $@
|
||||
|
||||
$(BUILD)/cable-thang.exe: src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/libdatgpt.a $(BUILD)/dat_thang_icon.o
|
||||
$(CC) $(CPPFLAGS) -DDAT_THANG_VERSION=\"v$(VERSION)\" $(CFLAGS) $(SDL_CFLAGS) src/ddat_gui.c $(BUILD)/cable_media.o $(BUILD)/cable_video.o $(BUILD)/dat_thang_icon.o $(BUILD)/libdatgpt.a $(BUILD)/libcess.a $(LIBS) $(SDL_LIBS) $(FFMPEG_MEDIA_LIBS) -o $@
|
||||
|
||||
package-assets: $(WINDOWS_PROGRAMS) | $(BUILD)
|
||||
powershell.exe -NoProfile -Command "if (Test-Path -LiteralPath '$(BUILD)/cable_hyperswarm.js') { Remove-Item -LiteralPath '$(BUILD)/cable_hyperswarm.js' -Force }; if (Test-Path -LiteralPath '$(BUILD)/cable_bootstrap.js') { Remove-Item -LiteralPath '$(BUILD)/cable_bootstrap.js' -Force }"
|
||||
powershell.exe -NoProfile -Command "Copy-Item -Path 'assets','scheme' -Destination '$(BUILD)' -Recurse -Force"
|
||||
powershell.exe -NoProfile -Command "$$stage='dist/windows-x64'; if (Test-Path -LiteralPath $$stage) { Remove-Item -LiteralPath $$stage -Recurse -Force }; New-Item -ItemType Directory -Path $$stage | Out-Null; $$apps=@('dat.exe','ddat.exe','cable.exe','cesspool-bootstrap.exe','sleep-verify.exe','ddat-scheme.exe','dat-thang.exe','cable-thang.exe'); foreach ($$app in $$apps) { Copy-Item -LiteralPath (Join-Path '$(BUILD)' $$app) -Destination $$stage -Force }; Get-ChildItem -LiteralPath '$(BUILD)' -Filter '*.dll' -File | Copy-Item -Destination $$stage -Force; Copy-Item -Path 'assets','scheme' -Destination $$stage -Recurse -Force; New-Item -ItemType Directory -Path (Join-Path $$stage 'docs') -Force | Out-Null; Copy-Item -Path 'docs/windows-build.md','docs/dat-thang.1','docs/cable-thang.1','docs/cesspool-bootstrap.1','docs/dillo.md' -Destination (Join-Path $$stage 'docs') -Force; New-Item -ItemType Directory -Path 'dist/installers' -Force | Out-Null; $$zip='dist/installers/dat-gpt-v$(VERSION)-windows-x64.zip'; if (Test-Path -LiteralPath $$zip) { Remove-Item -LiteralPath $$zip -Force }; Compress-Archive -Path ($$stage + '/*') -DestinationPath $$zip -CompressionLevel Optimal"
|
||||
|
||||
clean:
|
||||
powershell.exe -NoProfile -Command "if (Test-Path -LiteralPath '$(BUILD)') { Get-ChildItem -LiteralPath '$(BUILD)' -Recurse -File | Where-Object { $$_.Extension -in '.o','.d','.exe','.a','.dll','.so' -or $$_.Name -in '.network-protocol-cesspool','.network-protocol.tmp' } | Remove-Item -Force }"
|
||||
@@ -0,0 +1 @@
|
||||
cesspool
|
||||
+18
-3
@@ -4,12 +4,16 @@ breaking out of Node-JS jail and enabling both C and Scheme env
|
||||
|
||||
dis
|
||||
dat://59d8099399510e23a3c693fb5887b7bba1586f4117f7ef1c5872622ada0c1181
|
||||
Warning: this may publicly expose your IP and the old tools are unmaintained
|
||||
|
||||
be aware: seeding this may publicly expose your IP and the old tools are unmaintained
|
||||
|
||||
you might discuss here
|
||||
|
||||
cabal://cf2caf3a1c4fd299471d315416b6cc3d9c5f7c678046652fb70ede4ff16698db
|
||||
|
||||
hosted on the web at
|
||||
https://git.worlio.com/divorce/dat-thang/
|
||||
(prev) https://git.worlio.com/divorce/dat-thang/
|
||||
https://git.canithesis.org/divorce/dat-thang
|
||||
|
||||
BUILD
|
||||
x86_64 GNU/Linux
|
||||
@@ -20,11 +24,18 @@ i implement the SLEEP spec as a C header (similar to REST with HTML)
|
||||
then i provide FFI bindings so you can call easily from scheme
|
||||
i also have a novel solution for hyperswarm-dht (i think)
|
||||
|
||||
this is pre-alpha prototype-level software
|
||||
|
||||
what this means and what this does:
|
||||
here be a p2p program that does away with the client-server model,
|
||||
and enables you to basically share any directory in your system as a torrent (basically esentially)
|
||||
|
||||
the 'thang' part is the client, eventually i'm targetting wasm
|
||||
the 'thang' part is the client, a mashup between AIM, Kazaa, and Impulsetracker
|
||||
right now trying SDL2
|
||||
|
||||
dat-thang currently supports serving html, addressing different versions of archives, integration with the dillo browser
|
||||
|
||||
'cable' is a C port of cabal-cli, there is an experimental feature of sharing voice and video, which can be turned off with compile flags.
|
||||
|
||||
"why? when hyper:// is here now with so cool much better justwerks dApps(Decentralized Apps)???"
|
||||
|
||||
@@ -34,6 +45,10 @@ I like the sound of dat:// better, this is cooler no offense, and the scope of t
|
||||
|
||||
"hyperswarm" sounds badass, its just what p2p need(s(ded since 201X)). i wanna be developing hyperswarm. in this implementation its called cesspool. so when people include that particular module in their distros they will package libcess. i've always wanted to write libcess and now here it is, baby.
|
||||
|
||||
cesspool is essentially (old) hyperswarm under the hood, in fact the only difference are the literal bytes 'cesspool' which you can change back to 'hypercore' in the NETWORK_PROTOCOL file and it (should) find peers on that network. similarly, theoretically, you can set your own to whatever and have your own network.
|
||||
|
||||
you're typically going to be the only people on the network, i tried to make the bootstrapping node process as easy as possible and crossplatform.
|
||||
|
||||
i'm using C because ez libraries, assert is the ultimate hack, and scheme because macros *shablamo* and s7 lisp-in-C because hell yeah i specialize in beautiful elegant solutions to your networking problem.
|
||||
|
||||
LICENSE
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
set "PATH=%~dp0;%PATH%"
|
||||
echo dat-gpt command shell. Try: dat --help, ddat --help, cable --help
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
1 ICON "assets/dat-thang.ico"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
# More Perfect DOS VGA
|
||||
|
||||
`MorePerfectDOSVGA.ttf` is the exact font file loaded by both SDL clients through SDL_ttf. It is by LÆMEUR, based on Zeh Fernando's Perfect DOS VGA 437. The author states that the font is free for commercial and non-commercial use; see the [font page](https://laemeur.sdf.org/fonts/) for details.
|
||||
|
||||
At runtime the clients use the real TrueType outlines and metrics, with monochrome hinting for crisp DOS-style pixels. SDL2_ttf is required when building with `ENABLE_SDL2=1`. The default local path is `assets/fonts/MorePerfectDOSVGA.ttf`; installed clients load it from `$(PREFIX)/share/datgpt/fonts/MorePerfectDOSVGA.ttf`. Set `DAT_THANG_FONT_FILE` to override the runtime font path.
|
||||
|
||||
`Unifont-18.0.01.otf` is bundled as the Unicode fallback for compact Base32768 addresses. The address alphabet is restricted to the Unicode Basic Multilingual Plane; startup checks every one of its 32,768 main glyphs and 128 padding glyphs against the bundled font and refuses to continue if any glyph is absent. Unifont's official release describes coverage for every printable BMP code point. It is dual-licensed under SIL OFL 1.1 and GPL-2-or-later with the font embedding exception; the OFL text is included beside the font. Source: [GNU Unifont](https://unifoundry.com/unifont/index.html).
|
||||
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
The SIL Open Font License version 1.1 is copied below, and is also
|
||||
available with a FAQ at http://scripts.sil.org/OFL.
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,37 @@
|
||||
param(
|
||||
[string]$MsysRoot = 'C:\msys64',
|
||||
[string]$InnoSetupCompiler = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$iss = Join-Path $repo 'dat-thang.iss'
|
||||
|
||||
if (!$InnoSetupCompiler) {
|
||||
$candidate = Get-Command ISCC.exe -ErrorAction SilentlyContinue
|
||||
if ($candidate) { $InnoSetupCompiler = $candidate.Source }
|
||||
else {
|
||||
foreach ($path in @(
|
||||
'C:\Program Files (x86)\Inno Setup 6\ISCC.exe',
|
||||
'C:\Program Files\Inno Setup 6\ISCC.exe'
|
||||
)) {
|
||||
if (Test-Path -LiteralPath $path) { $InnoSetupCompiler = $path; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$InnoSetupCompiler -or !(Test-Path -LiteralPath $InnoSetupCompiler)) {
|
||||
throw 'Inno Setup 6 is required to create the installer. Install Inno Setup 6, then rerun .\build-windows-installer.ps1.'
|
||||
}
|
||||
|
||||
Push-Location $repo
|
||||
try {
|
||||
& (Join-Path $repo 'build-windows.ps1') -MsysRoot $MsysRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw "Windows app build failed with exit code $LASTEXITCODE" }
|
||||
$version = (Get-Content -LiteralPath (Join-Path $repo 'VERSION') -Raw).Trim()
|
||||
& $InnoSetupCompiler "/DAppVersion=$version" $iss
|
||||
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Write-Host "Windows installer and portable ZIP are in $(Join-Path $repo 'dist\installers')"
|
||||
@@ -0,0 +1,51 @@
|
||||
param(
|
||||
[string]$MsysRoot = 'C:\msys64',
|
||||
[switch]$EnableCableMedia
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = (Get-Location).Path
|
||||
$iconGenerator = Join-Path $repo 'tools\make_windows_icon.ps1'
|
||||
$makeIcon = Join-Path $repo 'assets\dat-thang.ico'
|
||||
$iconResourceCompiler = Join-Path $MsysRoot 'ucrt64\bin\windres.exe'
|
||||
$bash = Join-Path $MsysRoot 'usr\bin\bash.exe'
|
||||
$ucrtBin = Join-Path $MsysRoot 'ucrt64\bin'
|
||||
if (!(Test-Path -LiteralPath $bash)) { throw "MSYS2 bash not found at $bash. Install MSYS2 with the UCRT64 environment." }
|
||||
foreach ($tool in @('gcc.exe', 'mingw32-make.exe', 'pkg-config.exe', 'windres.exe')) {
|
||||
if (!(Test-Path -LiteralPath (Join-Path $ucrtBin $tool))) { throw "Required UCRT64 tool missing: $tool" }
|
||||
}
|
||||
if (!(Test-Path -LiteralPath $makeIcon) -or (Get-Item -LiteralPath $iconGenerator).LastWriteTimeUtc -gt (Get-Item -LiteralPath $makeIcon).LastWriteTimeUtc) {
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $iconGenerator
|
||||
if ($LASTEXITCODE -ne 0) { throw "Windows icon generation failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
|
||||
$repoUnix = $repo -replace '^([A-Za-z]):', '/$1' -replace '\\', '/'
|
||||
$repoUnix = $repoUnix -replace '^/([A-Za-z])', '/$1'
|
||||
$mediaFlag = if ($EnableCableMedia) { '1' } else { '0' }
|
||||
$mediaCheck = if ($EnableCableMedia) { ' && pkg-config --exists libavformat libavcodec libavutil libswresample libswscale' } else { '' }
|
||||
$command = "export PATH=/ucrt64/bin:`$PATH; cd '$repoUnix' && pkg-config --exists libsodium libcurl sdl2 SDL2_ttf$mediaCheck && mingw32-make -f Makefile.windows all ENABLE_CABLE_MEDIA=$mediaFlag"
|
||||
& $bash -lc $command
|
||||
if ($LASTEXITCODE -ne 0) { throw "Windows build failed with exit code $LASTEXITCODE" }
|
||||
|
||||
$objdump = Join-Path $ucrtBin 'objdump.exe'
|
||||
$queue = [System.Collections.Generic.Queue[string]]::new()
|
||||
Get-ChildItem -LiteralPath (Join-Path $repo 'build\windows') -Filter '*.exe' | ForEach-Object { $queue.Enqueue($_.FullName) }
|
||||
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
while ($queue.Count -gt 0) {
|
||||
$binary = $queue.Dequeue()
|
||||
$deps = & $objdump -p $binary 2>$null | Select-String 'DLL Name:'
|
||||
foreach ($depLine in $deps) {
|
||||
$dll = ($depLine.Line -replace '^.*DLL Name:\s*', '').Trim()
|
||||
if (!$dll -or $dll -match '^(api-ms-win-|ext-ms-win-)' -or !$seen.Add($dll)) { continue }
|
||||
$sourceDll = Join-Path $ucrtBin $dll
|
||||
if (Test-Path -LiteralPath $sourceDll) {
|
||||
$targetDll = Join-Path (Split-Path -Parent $binary) $dll
|
||||
Copy-Item -LiteralPath $sourceDll -Destination $targetDll -Force
|
||||
$queue.Enqueue($targetDll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Windows development build: $repo\build\windows"
|
||||
Write-Host "Windows portable package: $repo\dist\windows-x64"
|
||||
Write-Host "Windows distributions: $repo\dist\installers"
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
prefix=/usr/local
|
||||
enable_s7=1
|
||||
cc=${CC:-cc}
|
||||
|
||||
usage() {
|
||||
echo "usage: ./configure [--prefix=PATH] [--enable-s7] [CC=compiler]"
|
||||
echo "requires a C99 compiler, make, libsodium, libcurl; S7 is bundled and required"
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--prefix=*) prefix=${arg#*=} ;;
|
||||
--enable-s7) enable_s7=1 ;;
|
||||
--disable-s7) echo "configure: error: S7 is required for JSON and Scheme data handling" >&2; exit 1 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
CC=*) cc=${arg#CC=} ;;
|
||||
*) echo "configure: unknown option: $arg" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
log=config.log
|
||||
: > "$log"
|
||||
echo "configure: checking build tools"
|
||||
for tool in "$cc" ar ranlib install make; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "configure: error: required tool '$tool' was not found" | tee -a "$log" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
tmp_c=conftest-$$.c
|
||||
tmp_exe=conftest-$$
|
||||
trap 'rm -f "$tmp_c" "$tmp_exe"' EXIT HUP INT TERM
|
||||
cat > "$tmp_c" <<'EOF'
|
||||
#include <sodium.h>
|
||||
int main(void) { return sodium_init() < 0; }
|
||||
EOF
|
||||
|
||||
sodium_cflags=
|
||||
sodium_libs=-lsodium
|
||||
curl_cflags=
|
||||
curl_libs=-lcurl
|
||||
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libsodium; then
|
||||
sodium_cflags=$(pkg-config --cflags libsodium)
|
||||
sodium_libs=$(pkg-config --libs libsodium)
|
||||
fi
|
||||
if ! "$cc" $sodium_cflags "$tmp_c" $sodium_libs -o "$tmp_exe" >>"$log" 2>&1; then
|
||||
echo "configure: error: libsodium headers and library are required" | tee -a "$log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libcurl; then
|
||||
curl_cflags=$(pkg-config --cflags libcurl)
|
||||
curl_libs=$(pkg-config --libs libcurl)
|
||||
fi
|
||||
cat > "$tmp_c" <<'EOF'
|
||||
#include <curl/curl.h>
|
||||
int main(void) { CURL *c = curl_easy_init(); if (c) curl_easy_cleanup(c); return 0; }
|
||||
EOF
|
||||
if ! "$cc" $curl_cflags "$tmp_c" $curl_libs -o "$tmp_exe" >>"$log" 2>&1; then
|
||||
echo "configure: error: libcurl headers and library are required for Dat name resolution" | tee -a "$log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > config.mk <<EOF
|
||||
# Generated by ./configure. Edit configure arguments, not this file.
|
||||
PREFIX = $prefix
|
||||
CC = $cc
|
||||
AR = ar
|
||||
RANLIB = ranlib
|
||||
INSTALL = install
|
||||
SODIUM_CFLAGS = $sodium_cflags
|
||||
SODIUM_LIBS = $sodium_libs
|
||||
CURL_CFLAGS = $curl_cflags
|
||||
CURL_LIBS = $curl_libs
|
||||
ENABLE_S7 = $enable_s7
|
||||
CPPFLAGS += \$(SODIUM_CFLAGS)
|
||||
EOF
|
||||
|
||||
cat > config.h <<EOF
|
||||
#ifndef DATGPT_CONFIG_H
|
||||
#define DATGPT_CONFIG_H
|
||||
#define DATGPT_HAVE_SODIUM 1
|
||||
#define DATGPT_ENABLE_S7 $enable_s7
|
||||
#define DATGPT_PREFIX "$prefix"
|
||||
#endif
|
||||
EOF
|
||||
|
||||
echo "configure: prefix: $prefix"
|
||||
echo "configure: compiler: $cc"
|
||||
echo "configure: libsodium: $sodium_libs"
|
||||
echo "configure: Dat name transport: $curl_libs"
|
||||
echo "configure: S7 bindings: required"
|
||||
echo "configure: created config.mk and config.h"
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
#define AppName "dat-thang"
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.0.002"
|
||||
#endif
|
||||
#define AppPublisher "dat-gpt contributors"
|
||||
#define AppExeName "dat-thang.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{D1E521E5-19F8-4D7C-9C44-23B19D070A73}
|
||||
AppName={#AppName}
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher={#AppPublisher}
|
||||
DefaultDirName={autopf}\dat-thang
|
||||
DefaultGroupName=dat-thang
|
||||
DisableProgramGroupPage=no
|
||||
PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
WizardStyle=modern
|
||||
WizardResizable=yes
|
||||
SetupIconFile=assets\dat-thang.ico
|
||||
UninstallDisplayIcon={app}\dat-thang.exe
|
||||
OutputDir=dist\installers
|
||||
OutputBaseFilename=dat-thang-setup-{#AppVersion}-x64
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
ChangesEnvironment=yes
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
Uninstallable=yes
|
||||
VersionInfoVersion={#AppVersion}.0
|
||||
VersionInfoProductName=dat-thang
|
||||
VersionInfoDescription=dat-thang v{#AppVersion} (prototype, pre-alpha)
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "Create a desktop shortcut for dat-thang"; GroupDescription: "Additional shortcuts:"; Flags: unchecked
|
||||
Name: "addtopath"; Description: "Add dat-gpt terminal commands to PATH"; GroupDescription: "Terminal integration:"; Flags: unchecked
|
||||
Name: "launch"; Description: "Launch dat-thang when setup finishes"; GroupDescription: "Finishing up:"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "dist\windows-x64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "assets\dat-shell.cmd"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "assets\dat-thang.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\dat-thang"; Filename: "{app}\dat-thang.exe"; WorkingDir: "{userdocs}"; IconFilename: "{app}\dat-thang.exe"
|
||||
Name: "{group}\dat-gpt command prompt"; Filename: "{cmd}"; Parameters: "/K ""{app}\dat-shell.cmd"""; WorkingDir: "{userdocs}"; IconFilename: "{app}\dat-thang.exe"
|
||||
Name: "{group}\Windows build and bridge notes"; Filename: "{app}\docs\windows-build.md"
|
||||
Name: "{autodesktop}\dat-thang"; Filename: "{app}\dat-thang.exe"; WorkingDir: "{userdocs}"; IconFilename: "{app}\dat-thang.exe"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\dat-thang.exe"; Description: "Launch dat-thang"; Flags: postinstall nowait skipifsilent; Tasks: launch
|
||||
|
||||
[Code]
|
||||
const
|
||||
EnvironmentKey = 'Environment';
|
||||
PathValueName = 'Path';
|
||||
|
||||
function PathRegistryRoot(): Integer;
|
||||
begin
|
||||
if IsAdminInstallMode then
|
||||
Result := HKEY_LOCAL_MACHINE
|
||||
else
|
||||
Result := HKEY_CURRENT_USER;
|
||||
end;
|
||||
|
||||
function PathContainsEntry(const PathValue, Entry: String): Boolean;
|
||||
var
|
||||
Parts: TArrayOfString;
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Parts := SplitString(PathValue, ';');
|
||||
for I := 0 to GetArrayLength(Parts) - 1 do
|
||||
if CompareText(Trim(Parts[I]), Entry) = 0 then
|
||||
begin
|
||||
Result := True;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure AddInstallPath;
|
||||
var
|
||||
Root: Integer;
|
||||
PathValue, Entry: String;
|
||||
begin
|
||||
Root := PathRegistryRoot();
|
||||
Entry := ExpandConstant('{app}');
|
||||
if not RegQueryStringValue(Root, EnvironmentKey, PathValueName, PathValue) then
|
||||
PathValue := '';
|
||||
if not PathContainsEntry(PathValue, Entry) then
|
||||
begin
|
||||
if PathValue <> '' then
|
||||
PathValue := PathValue + ';';
|
||||
PathValue := PathValue + Entry;
|
||||
RegWriteExpandStringValue(Root, EnvironmentKey, PathValueName, PathValue);
|
||||
RegWriteStringValue(Root, 'Software\dat-gpt\Installer', 'AddedDatPath', Entry);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure RemoveInstallPath;
|
||||
var
|
||||
Root: Integer;
|
||||
PathValue, Entry, NewPath, AddedEntry: String;
|
||||
Parts: TArrayOfString;
|
||||
I: Integer;
|
||||
begin
|
||||
Root := PathRegistryRoot();
|
||||
Entry := ExpandConstant('{app}');
|
||||
if not RegQueryStringValue(Root, 'Software\dat-gpt\Installer', 'AddedDatPath', AddedEntry) then
|
||||
Exit;
|
||||
if CompareText(AddedEntry, Entry) <> 0 then
|
||||
Exit;
|
||||
if not RegQueryStringValue(Root, EnvironmentKey, PathValueName, PathValue) then
|
||||
Exit;
|
||||
Parts := SplitString(PathValue, ';');
|
||||
NewPath := '';
|
||||
for I := 0 to GetArrayLength(Parts) - 1 do
|
||||
if (Trim(Parts[I]) <> '') and (CompareText(Trim(Parts[I]), Entry) <> 0) then
|
||||
begin
|
||||
if NewPath <> '' then
|
||||
NewPath := NewPath + ';';
|
||||
NewPath := NewPath + Trim(Parts[I]);
|
||||
end;
|
||||
if NewPath <> PathValue then
|
||||
begin
|
||||
if NewPath = '' then
|
||||
RegDeleteValue(Root, EnvironmentKey, PathValueName)
|
||||
else
|
||||
RegWriteExpandStringValue(Root, EnvironmentKey, PathValueName, NewPath);
|
||||
end;
|
||||
RegDeleteValue(Root, 'Software\dat-gpt\Installer', 'AddedDatPath');
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if (CurStep = ssPostInstall) and WizardIsTaskSelected('addtopath') then
|
||||
AddInstallPath();
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
begin
|
||||
if CurUninstallStep = usUninstall then
|
||||
RemoveInstallPath();
|
||||
end;
|
||||
@@ -0,0 +1,109 @@
|
||||
# Beaker networking notes for `dat-gpt`
|
||||
|
||||
These notes use the archived `beakerbrowser/beaker` 1.1.0 source and the
|
||||
included old `dillo-dat` plugin as references. Beaker was archived in 2022.
|
||||
Its later Hyperdrive network stack is useful for subsystem boundaries, but it
|
||||
is not proof of compatibility with the old Dat protocol or a replacement for
|
||||
the Dillo plugin's `hyperdrive` + `hyperdiscovery` path.
|
||||
|
||||
## What Beaker separates
|
||||
|
||||
1. **Name resolution.** URLs are parsed first. A 64-character key is used
|
||||
directly; a domain requires a resolver. Beaker has a `dat-dns` based
|
||||
resolver and local name/key maps. Its v1.1 HyperDNS implementation leaves
|
||||
the remote DNS logic disabled, and its `dat://` handler calls a separate
|
||||
`dat-dns` resolver. Name resolution therefore belongs above transport and
|
||||
should return the archive key before opening feeds.
|
||||
2. **Drive sessions and persistence.** Beaker's later stack talks to a
|
||||
long-lived Hyperspace process through `hyperdrive-daemon-client`. Sessions
|
||||
are keyed by archive key plus version and read/write mode. The daemon owns
|
||||
the storage and network handles; Beaker expires idle sessions and reconnects
|
||||
them if the daemon restarts.
|
||||
3. **Announce versus lookup.** Beaker explicitly configures a drive with
|
||||
`announce: true, lookup: true` when it should be hosted and discover peers.
|
||||
Merely opening a drive and resolving its key are separate operations.
|
||||
4. **Archive reads.** The newer `hyper://` handler resolves the host to a key,
|
||||
opens/loads the drive (which starts network lookup), checks out an optional
|
||||
version, reads its manifest, resolves the path, and streams the selected
|
||||
file from the drive. It supports `GET`/`HEAD`, byte ranges, redirects for
|
||||
directory URLs without a trailing slash, manifest MIME overrides, and
|
||||
content sniffing. A drive with version zero becomes a clear “no peers found
|
||||
yet” response, so lookup-in-progress is distinct from a missing path.
|
||||
The separate legacy `dat://` handler resolves through `dat-dns`, runs the
|
||||
legacy conversion/download tool into temporary storage, and then serves
|
||||
that filesystem through the same path resolver. That conversion route is
|
||||
materially different from the native Hyperdrive read path.
|
||||
5. **Operational visibility.** Beaker has separate debug categories for the
|
||||
application, Dat, DHT discovery, DNS discovery, and the Hypercore protocol.
|
||||
That separation makes it possible to tell whether a failure is lookup,
|
||||
connection, replication, or file resolution.
|
||||
|
||||
## What applies to this C implementation
|
||||
|
||||
- Preserve the existing `NETWORK_PROTOCOL` switch and `cesspool` spelling.
|
||||
The default Cesspool build uses the V7 message format. `hypercore` selects
|
||||
the old Dillo dependency's V6 channels, nonce-bearing feed handshake, and
|
||||
XSalsa20 stream encryption. The `CSP0` UDP discovery packet format is still
|
||||
separate from the old JS `hyperdiscovery` format.
|
||||
- Keep hostname resolution, discovery, authenticated peer sessions, feed
|
||||
replication, and browser path handling as separate layers. The Dillo DPI
|
||||
helper already consumes the URL/path and local archive layers; its cache
|
||||
lookup is not yet a remote archive loader.
|
||||
- The archive session joins a key's topic, consumes topic-tagged Cesspool
|
||||
announcements, carries peer feed channels, exchanges HAVE/WANT state,
|
||||
requests blocks, and verifies/forwards signed proofs. The UDP discovery
|
||||
packet format still needs old JS `hyperdiscovery` interoperability work;
|
||||
Noise authentication and socket-to-session transport integration are
|
||||
separate networking layers.
|
||||
- Beaker's daemon restart/session handling suggests a lifecycle boundary for
|
||||
the C version: one archive session owns the discovery membership, peer set,
|
||||
replication state, pending block requests, and close/retry behavior. Keep
|
||||
this below both the CLI and Dillo DPI callers so they can share it.
|
||||
- `dat-dns` names should be resolved as a mapping to a key, not treated as a
|
||||
second archive identity. Cache the mapping separately from archive content;
|
||||
keep the resolved key visible for debugging and canonical `dat://<key>`
|
||||
links.
|
||||
- `dat_dns_resolve` follows the old `dat-dns` URL-to-key contract. The bundled
|
||||
Dillo plugin disables DNS-over-HTTPS and resolves through HTTPS
|
||||
`/.well-known/dat`; the C resolver follows that same path after checking its
|
||||
local alias map. It does not call the operating system's DNS TXT resolver.
|
||||
The record contains a `dat://<64-hex-key>` line and optional `TTL=<seconds>`.
|
||||
This lookup returns a key only; archive discovery and transfer remain
|
||||
separate Cesspool/session operations.
|
||||
|
||||
## Suggested implementation order
|
||||
|
||||
1. Define an archive network-session API with explicit `open`, `announce`,
|
||||
`lookup`, `peer-up`, `peer-down`, `request-block`, `block-arrived`, and
|
||||
`close` transitions. Keep transport details behind the existing `cesspool`
|
||||
boundary.
|
||||
2. Make one peer connection carry a real authenticated protocol handshake and
|
||||
identify the metadata and content feeds. Do not start with browser DNS or
|
||||
directory listing behavior.
|
||||
3. Connect remote metadata block requests to feed verification and archive
|
||||
reads. A cold `dat://<key>/index.html` should be able to wait for and serve
|
||||
the requested bytes, while earlier verified blocks remain cached.
|
||||
4. Add retry, peer removal, and session cleanup. Expose peer and missing-block
|
||||
state through a diagnostic command before adding a dashboard.
|
||||
5. Add name resolution and any Beaker-style convenience behaviors only after
|
||||
key-addressed remote reads work.
|
||||
|
||||
## Source references
|
||||
|
||||
- Beaker 1.1.0 `app/package.json`: `dat-dns`, `discovery-swarm`, `hyperspace`,
|
||||
`hyperdrive-daemon-client`, and `pauls-dat-api2` dependencies.
|
||||
- `app/bg/hyper/daemon.js`: daemon setup/reconnect, per-drive sessions, peer
|
||||
addresses, and session cleanup.
|
||||
- `app/bg/hyper/drives.js`: key/domain resolution, network configuration,
|
||||
drive loading, manifest reads, and version checkouts.
|
||||
- `app/bg/hyper/dns.js`: local name/key map; remote DNS implementation is
|
||||
commented out in this archived revision.
|
||||
- `app/bg/protocols/dat.js`: legacy `dat://` URL host resolution, local
|
||||
legacy download/conversion to temporary storage, path resolution, MIME
|
||||
selection, and response handling.
|
||||
- `app/bg/protocols/hyper.js`: native Hyperdrive network open, historic
|
||||
checkout, path resolution, no-peer state, range reads, and browser response
|
||||
behavior.
|
||||
- Beaker's repository README identifies version 1.1.0 as the last release and
|
||||
the repository as archived; its debug namespaces include `dat`,
|
||||
`bittorrent-dht`, `dns-discovery`, and `hypercore-protocol`.
|
||||
@@ -0,0 +1,154 @@
|
||||
# Cable and Cabal interoperability notes
|
||||
|
||||
This document describes the **test-only Cabal/Hypercore interoperability
|
||||
harness** under `tests/interop/cabal/`. It is kept to compare historical
|
||||
behavior and is excluded from production builds and runtime paths.
|
||||
|
||||
The production `cable` and `cable-thang` clients are C/S7 applications:
|
||||
Cesspool handles discovery and transport, Sleep handles persistent local
|
||||
feeds, and S7 represents room events as S-expressions. Cabal CLI and
|
||||
Hypercore are test targets, not production dependencies.
|
||||
|
||||
## Which client to run
|
||||
|
||||
The working historical Cabal terminal client is `cabal-cli`. To join a room,
|
||||
run:
|
||||
|
||||
```sh
|
||||
cabal-cli cabal://<room-key>
|
||||
```
|
||||
|
||||
The `cable` executable also supports local Sleep feed and Cesspool bootstrap
|
||||
commands. The native Cable chat client is `cable-thang`, using a terminal-style
|
||||
SDL2 layout inspired by Cabal's TUI. Build with `make ENABLE_SDL2=1`; create an
|
||||
address in the terminal with `build/linux/cable --new`, then launch the GUI with
|
||||
`build/linux/cable-thang <cable://address>`. Use `/join NAME`, `/nick NAME`,
|
||||
`Ctrl+N`, and `Ctrl+P` in the client.
|
||||
|
||||
## Cabal behaviors being ported
|
||||
|
||||
The Cabal CLI's `/whoami` (also `/key`) displays the local user key, while
|
||||
`/nick` changes that user's display name. Those are different things: the
|
||||
user's public key is the stable identity within that room; the nickname is a
|
||||
readable label published as an `about` record in that user's room feed.
|
||||
The CLI's usage and commands are listed in the
|
||||
[cabal-cli README](https://github.com/cabal-club/cabal-cli#commands). The
|
||||
[cabal-core API](https://github.com/cabal-club/cabal-core#cabalgetlocalkeycb)
|
||||
documents `getLocalKey()` as the local user's key and documents messages as
|
||||
records published to user feeds. Cabal creates its local writer feed inside
|
||||
each room's storage, so a user's key is room-scoped, stable for that room, and
|
||||
can differ in another room. A room key and its optional `?admin=<key>`
|
||||
moderation key are separate from each user's feed key. Cabal text records use
|
||||
`chat/text` with channel and text fields.
|
||||
|
||||
## Dat-era Cabal reference
|
||||
|
||||
Cabal Core 5.0.1 is a concrete older reference candidate: it was published about
|
||||
seven years ago, and a 2019 Cabal walkthrough describes the chat as running
|
||||
over Dat (the walkthrough was later updated for Cabal Core 5.0). The bundled
|
||||
Dillo Dat tree supplies a nearby protocol reference with Hypercore 7.7.1,
|
||||
hypercore-protocol 6.12.0, Hyperdrive 9.16.0, and hyperdiscovery 7.1.1. Use
|
||||
those versions to guide backwards-adaptation; do not treat current Cabal Core
|
||||
16 or its Hyper:// networking as the target.
|
||||
|
||||
The repeatable JS behavior tests remain useful for room identity, nicknames,
|
||||
channels, and record shapes. To run them against the historical package, point
|
||||
CABAL_JS_MODULE and CABAL_CLIENT_MODULE at a Cabal Core 5.0.x install, then run
|
||||
make test-cabal-interop.
|
||||
|
||||
## Current C groundwork
|
||||
|
||||
- `cable address` accepts room keys with `cabal://`, `cable://`, or
|
||||
`hyper://`, including a `?admin=<64 hex>` parameter, and prints canonical
|
||||
`cable://` room addresses while retaining the admin key separately.
|
||||
- `cable whoami <room-address>` and dat-thang expose a persistent local user
|
||||
key as a `cable://<64 hex>` user address. Each room gets its own 64-byte
|
||||
Ed25519 secret at `~/.cable/rooms/<room-key>/user-feed.key`, with owner-only
|
||||
permissions. The key is stable in that room across restarts and distinct
|
||||
from the room and admin keys.
|
||||
- `hyper://` parsing only aliases the key representation; production Cable
|
||||
does not implement the newer Hypercore stack.
|
||||
- The library defines a bounded Cable text-message envelope and libsodium
|
||||
sealed-box encrypt/decrypt helpers.
|
||||
- dat-thang's left panel is a peer list again. Double-clicking a discovered
|
||||
peer opens a direct-message pane; messages use the C-only sealed Cesspool
|
||||
record and can be exchanged between Cable-enabled dat-thang clients.
|
||||
- `cable-thang` is a native C/S7 room client. Cesspool handles discovery and
|
||||
authenticated transport, Sleep persists local feeds, and S7 builds signed
|
||||
room-event S-expressions. The room UI supports room creation, named
|
||||
channels, nickname changes, and channel cycling. This path is Cable's own
|
||||
protocol and does not claim Cabal wire interoperability.
|
||||
|
||||
### Hypercore feed storage check
|
||||
|
||||
The test-only Cable compatibility writer occupies the `room/0` slot used by Cabal Core 5's
|
||||
`multifeed`, exposes the expected `localname`, and shares the Hypercore tree,
|
||||
signature, bitfield, key, and data files. Run
|
||||
`tests/interop/dat/cable_hypercore_compat.ps1` on Windows with WSL and Node installed. It
|
||||
writes Cabal-shaped nick and chat JSON blocks from C, opens and verifies them
|
||||
with the bundled Hypercore 7.7.1, appends a JavaScript block, then confirms C
|
||||
reads that appended block. This verifies the standalone C feed format against
|
||||
the legacy Hypercore fixture. It is separate from the native Sleep feeds used
|
||||
by cable-thang.
|
||||
|
||||
## Side-by-side regression harness
|
||||
|
||||
`tests/interop/cabal/cabal_cable_side_by_side.js` creates a Cabal JS room, publishes a nick
|
||||
and `chat/text` entry, and verifies that a second Cabal JS instance receives
|
||||
both through Cabal's live replication stream. It then gives Cable that exact
|
||||
room address, including `?admin=`, checks address normalization, and checks
|
||||
that Cable's local user address persists in the room and changes for a second
|
||||
room. This covers address normalization and the CLI's local identity utility;
|
||||
it does not exercise cable-thang's network session.
|
||||
|
||||
`tests/interop/cabal/cabal_cli_features.js` uses cabal-cli's own `cabal-client` API to create
|
||||
a room, execute `/nick` and `/join`, traverse joined channels using the same
|
||||
channel ordering as the CLI's `Ctrl+N` handler, and exchange chat/text entries
|
||||
between two independently opened Cabal clients over live Hypercore replication.
|
||||
The C SDL live harness is `tests/interop/cabal/cable_js_c_live.js`; it launches one Cabal
|
||||
Core JavaScript peer and one headless SDL/C client, then checks peer discovery
|
||||
and chat in both directions through the Cabal Hypercore feeds. Run it with
|
||||
`make test-cable-live ENABLE_SDL2=1` after building SDL2 support and installing
|
||||
Node.js and Cabal Core. The test uses a local Hyperswarm DHT bootstrap node and
|
||||
separate client UDP ports.
|
||||
|
||||
Run it once the historical JS `cabal-core` package and Node are available. The
|
||||
test invokes Cabal JS directly; the UI command for people is `cabal-cli`:
|
||||
|
||||
```sh
|
||||
make test-cabal-interop CABAL_JS_MODULE=/path/to/cabal-cli/node_modules/cabal-core
|
||||
make test-cabal-interop CABAL_CLIENT_MODULE=/path/to/cabal-cli/node_modules/cabal-client
|
||||
```
|
||||
|
||||
On Windows with Cable built in WSL, run the same harness with Windows Node and
|
||||
the installed Cabal package, routing each Cable CLI call through WSL:
|
||||
|
||||
```powershell
|
||||
$env:CABAL_JS_MODULE = "$env:APPDATA\npm\node_modules\cabal-cli\node_modules\cabal-core"
|
||||
$env:CABLE_BIN = "$PWD\build\cable"
|
||||
$env:CABLE_RUNNER = 'wsl'
|
||||
node tests/interop/cabal/cabal_cable_side_by_side.js
|
||||
```
|
||||
|
||||
The harness maps its temporary Cable home directory into WSL for this mode.
|
||||
This is useful when the WSL distribution has no Node installation; no second
|
||||
Node runtime is needed.
|
||||
|
||||
The normal `make test` runs this harness when it can resolve `cabal-core`; it
|
||||
prints `SKIP` when the configured Node runtime cannot resolve the optional JS
|
||||
package. A standalone `make test-cabal-interop` reports the resolution error
|
||||
directly.
|
||||
|
||||
## Compatibility boundary
|
||||
|
||||
The upstream Cabal components documented here use Hypercore feed replication
|
||||
and their own Cabal database/index conventions. Cable's current GUI messages
|
||||
use a Cesspool transport record and do not enter Cabal feeds, so they are not
|
||||
yet readable by Cabal JS clients. `/nick` in Cabal appends a `{ type: "about",
|
||||
content: { name: nick } }` record to the user's room feed; the public user-feed
|
||||
key remains its unique identity. Cable has not yet implemented that nickname
|
||||
record in the Cabal message schema.
|
||||
The next compatibility layer needs the Hypercore transport/session selected
|
||||
for the deployed Cabal version, feed discovery, Cabal's multiwriter user-feed
|
||||
model, and its JSON message/index schema. Dat archive wire identifiers and
|
||||
`dat://` handling remain independent.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Cable live media
|
||||
|
||||
Cable live media travels beside Cable room chat and Dat archives. A live
|
||||
session is not an archive, does not append to a user's permanent chat feed, and does not
|
||||
advance a Dat version. It is sent as volatile Cable records over authenticated
|
||||
Cesspool/uTP peer sessions. The current audio path fans packets out from the
|
||||
broadcaster to authenticated room peers; multi-hop relay is not yet wired.
|
||||
|
||||
## Audio, first slice
|
||||
|
||||
`cable-thang`'s `BROADCAST` button opens the default SDL microphone as 48 kHz,
|
||||
mono, signed 16-bit PCM. FFmpeg's AAC-LC encoder converts 1,024-sample frames
|
||||
to 96 kbps AAC and wraps each access unit in an ADTS header. The `cable-live`
|
||||
packet header carries a random session id, sequence number, GUI timestamp,
|
||||
media kind, and bounded payload. The C viewer validates packets, decodes AAC,
|
||||
places PCM in a half-second maximum SDL playback queue, and plots those decoded
|
||||
samples in the waveform panel. `Ctrl+Shift+A` toggles the local broadcast.
|
||||
Received room audio begins playing as soon as its start packet is seen.
|
||||
|
||||
Cable audio and video are experimental and disabled by default. A normal SDL
|
||||
build has no FFmpeg dependency. Enable both together with
|
||||
`make ENABLE_SDL2=1 ENABLE_CABLE_MEDIA=1`; the makefile requires compatible
|
||||
FFmpeg 5.0+ development files (`libavcodec`, `libavformat`, `libavutil`,
|
||||
`libswresample`, and `libswscale`). To build the SDL clients without compiling
|
||||
either media feature, use `make ENABLE_SDL2=1 ENABLE_CABLE_MEDIA=0`.
|
||||
|
||||
On Debian or Ubuntu, install the development headers and enable the feature:
|
||||
|
||||
```sh
|
||||
sudo apt install libavcodec-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libswscale-dev
|
||||
make ENABLE_SDL2=1 ENABLE_CABLE_MEDIA=1
|
||||
```
|
||||
|
||||
With media enabled, the Cable video pane can listen to a local OBS MPEG-TS
|
||||
output. Click `OBS INPUT` or press `Ctrl+Shift+V`; Cable listens on
|
||||
`udp://127.0.0.1:5000`, decodes the video on a
|
||||
worker thread, scales the local preview to at most 640x360, and displays the
|
||||
latest frame. New frames are reduced to 160x90 JPEG and sent in 4,000-byte
|
||||
fragments over authenticated Cesspool/uTP sessions; receivers reassemble and
|
||||
decode them in the video pane. Transmission is capped at five frames per
|
||||
second. In OBS, add a Custom FFmpeg Output with container `mpegts`, video
|
||||
encoder `libx264`, and output URL `udp://127.0.0.1:5000?pkt_size=1316`. OBS
|
||||
input provides local preview and direct broadcast to authenticated peers.
|
||||
Camera capture without OBS and multi-hop forwarding are not implemented.
|
||||
|
||||
## Storage boundary
|
||||
|
||||
No media packets are written to `.dat`, test-only Cabal JSON chat feeds, or the
|
||||
versioned archive metadata/content feeds. Closing the room discards the
|
||||
decoder and playback queue. This avoids turning a live broadcast into
|
||||
permanent archive history. Hypervision's
|
||||
historic approach chunked WebM media and appended the chunks to a dedicated
|
||||
Hypercore feed. That is a useful precedent for chunking and peer redistribution,
|
||||
but an append-only archive is not the right retention policy for this live
|
||||
session path.
|
||||
|
||||
The packet kinds cover audio and video start/frame/stop. Direct camera capture,
|
||||
multi-hop relay, remote stream selection, and adaptive bitrate are follow-on
|
||||
work.
|
||||
@@ -0,0 +1,124 @@
|
||||
.TH CABLE-THANG 1 "October 2026" "cable-thang" "User Commands"
|
||||
.SH NAME
|
||||
cable-thang \- SDL2 Cable room chat client
|
||||
.SH SYNOPSIS
|
||||
.B cable-thang
|
||||
.RB [ cable-address ]
|
||||
.RB [ --offline ]
|
||||
.br
|
||||
.B cable-thang --help
|
||||
.SH DESCRIPTION
|
||||
.B cable-thang
|
||||
is the C and SDL2 room chat client. Create a new room address in the terminal
|
||||
with
|
||||
.BR cable --new ,
|
||||
then launch the client with the printed address as its positional argument:
|
||||
.B cable-thang cable://address .
|
||||
Launching
|
||||
.B cable-thang
|
||||
without an address opens its SDL room window; choose CREATE ROOM to create and
|
||||
enter a new room, or paste a
|
||||
.B cable://
|
||||
or
|
||||
.B cabal://
|
||||
room address there and press Enter to join it.
|
||||
Cesspool handles discovery and authenticated peer transport, Sleep stores
|
||||
persistent local feeds, and S7 constructs room records as S-expressions. Its
|
||||
fixed-cell, terminal-style layout
|
||||
shows joined channels, room messages, local nickname, room address, and
|
||||
authenticated peers. It joins a room by address.
|
||||
.PP
|
||||
Click PLAY AUDIO to start sending live room audio; the button changes to STOP
|
||||
AUDIO while active. Ctrl+Shift+A toggles the same control. The first media
|
||||
path captures the default microphone and sends 96 kbps AAC-LC. Incoming audio
|
||||
is decoded and played in the live waveform panel. Media packets are relayed
|
||||
through Cable peers without being appended to chat history or Dat archives.
|
||||
See the cable-live-streaming document for the packet and storage boundary.
|
||||
.PP
|
||||
Click PLAY VIDEO to start sending the local OBS MPEG-TS input at
|
||||
udp://127.0.0.1:5000; the button changes to STOP VIDEO while active. Ctrl+Shift+V
|
||||
toggles the same control. Configure OBS Custom
|
||||
FFmpeg Output with MPEG-TS, libx264, and that UDP URL. This previews the local
|
||||
source and sends 160x90 JPEG frames to authenticated room peers over Cesspool.
|
||||
.PP
|
||||
Room creation stores the room signing key and local user's persistent key in
|
||||
.IR ~/.cable/rooms/<room-key>/ .
|
||||
The room address is the same 32-byte key rendered as
|
||||
.BR cable:// .
|
||||
Joined channels and the local nickname are stored in the room's profile.
|
||||
.PP
|
||||
Without a configured Cesspool rendezvous, the room broadcasts discovery on
|
||||
the local network automatically and shows LAN DISCOVERY. Messages are kept
|
||||
in the local chat until a peer connects. Local network broadcasts do not cross
|
||||
routers. For discovery over the Internet, a reachable Cesspool rendezvous node
|
||||
is required; configure one with
|
||||
.B cable bootstrap add host:port
|
||||
or set
|
||||
.BR CABLE_BOOTSTRAP .
|
||||
If the local UDP network setup itself fails, the SDL2 client still opens and
|
||||
reports the error in its status line. Use
|
||||
.B --offline
|
||||
to deliberately skip network startup.
|
||||
.PP
|
||||
The production client is C and S7. Dat archives and Cable rooms both use
|
||||
Cesspool, with separate topics and record formats. Cabal CLI and Hypercore
|
||||
interoperability helpers remain under the test tree and are not launched or
|
||||
packaged by the production client.
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B /join \fIname\fR
|
||||
Join or create a channel and announce the channel to connected Cable peers.
|
||||
.TP
|
||||
.B /nick \fIname\fR
|
||||
Change the local nickname and announce it to connected Cable peers.
|
||||
.TP
|
||||
.B /channels
|
||||
Show the number of joined channels in the status line.
|
||||
.TP
|
||||
.B /help
|
||||
Show the available commands and key bindings in the status line.
|
||||
.SH KEY BINDINGS
|
||||
.TP
|
||||
.B Ctrl+N
|
||||
Select the next joined channel.
|
||||
.TP
|
||||
.B Ctrl+P
|
||||
Select the previous joined channel.
|
||||
.TP
|
||||
.B Ctrl+C
|
||||
Copy the room address to the clipboard.
|
||||
.TP
|
||||
.B Enter
|
||||
Send a message to connected room peers, or execute a slash command.
|
||||
.TP
|
||||
.B Escape
|
||||
Close the client.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B DDAT_BOOTSTRAP
|
||||
Comma- or semicolon-separated Cesspool rendezvous endpoints shared by Dat and
|
||||
Cable clients.
|
||||
.TP
|
||||
.B CABLE_BOOTSTRAP
|
||||
Comma- or semicolon-separated Cesspool rendezvous endpoints in host:port form.
|
||||
.TP
|
||||
.B DDAT_PORT
|
||||
Local UDP listening port for Dat archive Cesspool discovery.
|
||||
.TP
|
||||
.B cable bootstrap add host:port
|
||||
Persist a Cesspool rendezvous endpoint in
|
||||
.IR ~/.config/dat-gpt/bootstrap .
|
||||
Use cable bootstrap list to inspect the shared Dat/Cable list.
|
||||
.TP
|
||||
.B cable bootstrap serve host port
|
||||
Run a Cesspool rendezvous node in the foreground. It must be reachable over
|
||||
UDP at the advertised IPv4 address and port.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
cable --new
|
||||
cable-thang cabal://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
cable-thang cable://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef --offline
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR cabal-cli (1),
|
||||
.BR cable (1)
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
.TH CABLE 1 "October 2026" "cable" "User Commands"
|
||||
.SH NAME
|
||||
cable \- Cabal/Cable address, feed, and bootstrap utility
|
||||
.SH SYNOPSIS
|
||||
.B cable --new
|
||||
.br
|
||||
.B cable address
|
||||
.I address
|
||||
.br
|
||||
.B cable whoami
|
||||
.I room-address
|
||||
.br
|
||||
.B cable bootstrap add
|
||||
.I host:port
|
||||
.br
|
||||
.B cable bootstrap list
|
||||
.br
|
||||
.B cable bootstrap serve
|
||||
.I public-ipv4-host udp-port
|
||||
.br
|
||||
.B cable nick
|
||||
.I room-address name
|
||||
.br
|
||||
.B cable publish
|
||||
.I room-address channel text
|
||||
.br
|
||||
.B cable feed
|
||||
.I room-address
|
||||
.br
|
||||
.B cable --version
|
||||
.SH DESCRIPTION
|
||||
.B cable
|
||||
provides address, local feed, and bootstrap utilities. Its SDL2 chat client is
|
||||
.BR cable-thang (1).
|
||||
The CLI normalizes legacy
|
||||
.B cabal://
|
||||
and
|
||||
.B cable://
|
||||
room-address forms to canonical
|
||||
.B cable://
|
||||
addresses, preserving an optional
|
||||
.B ?admin=\fI64-hex-key\fR
|
||||
room moderation key. Raw 64-digit hexadecimal keys are accepted.
|
||||
.PP
|
||||
The room UI uses embedded S7 and S-expressions for its C-facing data model.
|
||||
Cesspool handles peer discovery and transport, Sleep stores persistent local
|
||||
feeds, and S7 represents room records as S-expressions. Hypercore and Cabal
|
||||
CLI interoperability tools are test-only and are not production dependencies.
|
||||
.PP
|
||||
The historical JavaScript terminal client remains available as
|
||||
.BR cabal-cli .
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B --new
|
||||
Create a new Cabal room, save its Ed25519 room signing key with mode 0600
|
||||
under
|
||||
.IR ~/.cable/rooms/<room-key>/room-secret.key ,
|
||||
and print its reusable
|
||||
.B cable://
|
||||
address. Pass that address to
|
||||
.BR cable-thang (1)
|
||||
on this or another client.
|
||||
.TP
|
||||
.BI address " address"
|
||||
Validate a Cabal/Cable/Hyper room address and print the key as
|
||||
.B cable://
|
||||
plus 64 lowercase hexadecimal digits, preserving any admin key.
|
||||
.TP
|
||||
.BI whoami " room-address"
|
||||
Print the persistent local C-side user's
|
||||
.B cable://
|
||||
address in the selected room, creating that room's local keypair on first use.
|
||||
.TP
|
||||
.BI bootstrap " add host:port"
|
||||
Persist a Cesspool rendezvous endpoint in
|
||||
.IR ~/.config/dat-gpt/bootstrap .
|
||||
.TP
|
||||
.B bootstrap list
|
||||
List configured Cesspool rendezvous endpoints.
|
||||
.TP
|
||||
.BI bootstrap " serve public-ipv4-host udp-port"
|
||||
Run the legacy Cesspool rendezvous server in the foreground. For a persistent
|
||||
node with a stable identity, service unit, and operator guidance, use
|
||||
.BR cesspool-bootstrap (1).
|
||||
The UDP port still must be reachable through the host firewall and router.
|
||||
Clients can configure its endpoint with
|
||||
.B cable bootstrap add host:port
|
||||
or CABLE_BOOTSTRAP.
|
||||
.TP
|
||||
.BI nick " room-address name"
|
||||
Append a local S-expression `about` record to the room's Sleep feed. This feed
|
||||
is experimental and is not a Cabal Hypercore feed used by
|
||||
.BR cable-thang .
|
||||
.TP
|
||||
.BI publish " room-address channel text"
|
||||
Append a local C-side feed chat record.
|
||||
.TP
|
||||
.BI feed " room-address"
|
||||
Print local C-side feed records.
|
||||
.TP
|
||||
.B --version
|
||||
Print the Cable version.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
cable --new
|
||||
cable address cabal://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
cable-thang cable://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
cable bootstrap add bootstrap.example.net:49737
|
||||
cable bootstrap list
|
||||
cable-thang cable://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR cabal-cli (1),
|
||||
.BR cable-thang (1)
|
||||
@@ -0,0 +1,82 @@
|
||||
.TH CESSPOOL-BOOTSTRAP 1 "October 2026" "dat-gpt" "User Commands"
|
||||
.SH NAME
|
||||
cesspool-bootstrap \- dedicated Cesspool rendezvous node
|
||||
.SH SYNOPSIS
|
||||
.B cesspool-bootstrap
|
||||
.RI [ options ]
|
||||
.br
|
||||
.B cesspool-bootstrap --help
|
||||
.SH DESCRIPTION
|
||||
.B cesspool-bootstrap
|
||||
runs a Cesspool discovery node in the foreground. It answers discovery
|
||||
lookups and tracks peer announcements; it does not relay chat, archive blocks,
|
||||
or other peer payloads. Leave it running on a machine that should provide
|
||||
discovery to clients on other networks.
|
||||
.PP
|
||||
At startup it reports its bound UDP port and prints the Windows Firewall rule
|
||||
for that port. Use
|
||||
.B --advertise HOST:PORT
|
||||
to print the externally reachable endpoint for client setup. It does not
|
||||
query a third-party service to discover the host's public address.
|
||||
.PP
|
||||
The node creates and reuses a persistent Cesspool identity at
|
||||
.IR state-dir/identity.key .
|
||||
Keep the state directory across restarts and protect it from other users.
|
||||
Losing it creates a new node identity, but does not change archive or room
|
||||
addresses.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BI --bind " IPv4"
|
||||
Local IPv4 interface to bind. Default: 0.0.0.0.
|
||||
.TP
|
||||
.BI --port " UDP-PORT"
|
||||
UDP listen port. Default: 3282.
|
||||
.TP
|
||||
.BI --advertise " HOST:PORT"
|
||||
Print the externally reachable endpoint in client setup commands. The
|
||||
endpoint must resolve to this node and accept inbound UDP on the selected port.
|
||||
.TP
|
||||
.BI --state-dir " DIR"
|
||||
Directory for the persistent node identity. Default on Windows:
|
||||
.IR %LOCALAPPDATA%/dat-gpt/cesspool-bootstrap-state ;
|
||||
on GNU/Linux:
|
||||
.IR $XDG_STATE_HOME/dat-gpt/cesspool-bootstrap ,
|
||||
or
|
||||
.IR $HOME/.local/state/dat-gpt/cesspool-bootstrap
|
||||
when XDG_STATE_HOME is unset.
|
||||
The directory is created if needed.
|
||||
.TP
|
||||
.B --help
|
||||
Show usage.
|
||||
.SH RUNNING A NODE
|
||||
Start the executable directly:
|
||||
.PP
|
||||
.nf
|
||||
cesspool-bootstrap --bind 0.0.0.0 --port 3282
|
||||
.fi
|
||||
.PP
|
||||
Keep that process running. Allow inbound UDP on port 3282 in the host
|
||||
firewall. If the machine is behind a router or cloud firewall, forward the
|
||||
same UDP port to it there. The program prints a Windows Firewall command;
|
||||
other firewalls need an equivalent UDP allow rule. A public hostname can be
|
||||
used with
|
||||
.B --advertise
|
||||
when one is available. Clients set the printed endpoint in Settings, or run
|
||||
.B dat bootstrap set HOST:PORT
|
||||
or
|
||||
.BR "cable bootstrap set HOST:PORT" .
|
||||
.PP
|
||||
The periodic status line's peer count is the number of distinct Cesspool
|
||||
identities with at least one current announcement. The topic count is the
|
||||
number of distinct announced discovery keys. One identity can announce many
|
||||
topics; a Cesspool identity is a network key, not necessarily a human user.
|
||||
.SH EXAMPLE
|
||||
.nf
|
||||
cesspool-bootstrap --bind 0.0.0.0 --port 3282 \\
|
||||
--state-dir cesspool-bootstrap-state \\
|
||||
--advertise bootstrap.example.net:3282
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR dat (1),
|
||||
.BR cable (1),
|
||||
.BR cable-thang (1)
|
||||
Binary file not shown.
@@ -0,0 +1,188 @@
|
||||
.TH DAT-THANG 1 "October 2026" "dat-thang v0.0.002" "User Commands"
|
||||
.SH NAME
|
||||
dat-thang \- SDL2 local archive and peer discovery client
|
||||
.SH SYNOPSIS
|
||||
.B dat-thang
|
||||
.RI [ --archive DIRECTORY ]
|
||||
.RI [ --site HOSTNAME ]
|
||||
.RI [ --offline ]
|
||||
.br
|
||||
.B dat-thang --version
|
||||
.SH DESCRIPTION
|
||||
dat-thang is the SDL2 graphical client for browsing a local filesystem directory,
|
||||
explicitly loading or creating Dat archives, and controlling the client's online
|
||||
discovery state. The top-left switch applies to the whole client. OFF leaves all
|
||||
loaded archive topics, unannounces when possible, closes the network socket, and
|
||||
clears peer state. ON connects the client and announces archives in its loaded
|
||||
archive list. Selecting an archive or browsing a directory does not toggle the
|
||||
client's network state or create an archive.
|
||||
.PP
|
||||
Version v0.0.002 is a prototype in pre-alpha.
|
||||
.PP
|
||||
The pane to the right of the Merkle graph navigates the local filesystem.
|
||||
Click a directory to enter it; navigation alone never creates or shares an
|
||||
archive. The bottom bar displays the selected filename and directory. Use
|
||||
F9 or IMPORT DAT to load an existing archive from the selected directory, or
|
||||
CREATE NEW DAT to deliberately create one. Loaded archives are remembered in
|
||||
the archive switcher and shared whenever the client is online. Secret signing keys are stored under
|
||||
.B $HOME/.dat/secret_keys/
|
||||
using Dat Desktop's sharded discovery-key paths, with one raw secret file per
|
||||
feed. See the installed key-management documentation.
|
||||
.PP
|
||||
Click the local SITE field while an archive is loaded to assign that archive a
|
||||
dat-dns hostname. Type the hostname and press Return to save a local alias; this
|
||||
client resolves it immediately. The confirmation dialog copies the corresponding
|
||||
dat-dns value to the clipboard. Public Dillo-compatible resolution uses the
|
||||
hostname's HTTPS /.well-known/dat response, whose first line is dat:// followed
|
||||
by the 64-digit archive key and whose optional second line is TTL=<seconds>.
|
||||
Escape cancels hostname editing. The site field can also display the
|
||||
hostname supplied with --site. Archive displays the selected archive key in
|
||||
hexadecimal or compact Base32768 form; click the format control to switch and
|
||||
COPY to copy its dat:// address. Version shows the active
|
||||
selected metadata-feed version and latest version; click the arrows to check out
|
||||
another recorded version. The matching Merkle view shows the feed's block count
|
||||
and a tree view of its active version.
|
||||
The peer pane distinguishes discovered peer announcements from authenticated
|
||||
connected peers with a marker on each buddy-list row. Double-click a peer to
|
||||
open Cable-thang's encrypted direct-message pane. Copy the desired dat:// link
|
||||
from the local ARCHIVE field, paste it into chat with Ctrl+V, and press Enter
|
||||
to send it. Click a received dat:// message to copy it, then click the
|
||||
right-hand ARCHIVE field and press Ctrl+V followed by Enter to begin importing
|
||||
into the currently selected local directory. The
|
||||
incoming archive appears in the lower transfer table while peers are found and
|
||||
metadata downloads. The current Cesspool message record is Cable-specific and
|
||||
is not Cabal feed interoperability.
|
||||
.PP
|
||||
By default dat-thang listens on UDP 3282 and cable-thang listens on UDP 3000,
|
||||
the stable ports used by Cesspool LAN discovery. If a preferred port is busy,
|
||||
the client chooses another known Cesspool port. Set DDAT_PORT to pin the local
|
||||
port. With no configured rendezvous, discovery is limited to the local network;
|
||||
across networks, both clients need a reachable Cesspool node.
|
||||
The YOU panel shows the selected room's local Cable feed address. Like Cabal,
|
||||
the key is scoped to that room's local feed and remains stable across restarts;
|
||||
room and optional admin keys are separate. The CLI prints the full address with
|
||||
.BR "cable whoami <room-address>" .
|
||||
The panel also shows connected peers, discovered peers, and the total number of
|
||||
loaded local directories available for sharing.
|
||||
The lower table lists loaded archives as well as active transfers. Its progress
|
||||
column is a bar: local archives are complete, while transfer bars count received
|
||||
blocks against the peer's advertised feed lengths. Received blocks are checked
|
||||
against SLEEP proofs and saved in per-feed .remote caches for later sessions.
|
||||
For each local archive, TITLE and DESCRIPTION come from its root `dat.scm`
|
||||
S-expression. Missing fields fall back to the directory name and archive key.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B --version, -v
|
||||
Print \fBdat-thang v0.0.002 (prototype, pre-alpha)\fR and exit.
|
||||
.TP
|
||||
.B --archive DIRECTORY
|
||||
Start in DIRECTORY. If it contains an existing .dat archive, load that archive;
|
||||
otherwise browse the directory without creating or sharing an archive. Defaults
|
||||
to the current directory.
|
||||
.TP
|
||||
.B --site HOSTNAME
|
||||
Display HOSTNAME in the site field.
|
||||
.TP
|
||||
.B --offline
|
||||
Start with peer discovery disabled, even when the directory already has an
|
||||
archive.
|
||||
.TP
|
||||
.B --smoke
|
||||
Render one frame and save build/linux/dat-thang-smoke.bmp on GNU/Linux or
|
||||
build/windows/dat-thang-smoke.bmp on Windows. Intended for headless
|
||||
render checks.
|
||||
.SH CONTROLS
|
||||
.TP
|
||||
.B F1
|
||||
Show this manual page in the client. Use Up/Down or the mouse wheel to scroll;
|
||||
press Escape to close it.
|
||||
.TP
|
||||
.B F9
|
||||
Load an existing Dat archive from the selected directory. This does not create
|
||||
an archive or start discovery; use ON to share it.
|
||||
.TP
|
||||
.B CREATE NEW DAT
|
||||
Create a Dat archive for the selected local directory and import its current
|
||||
files. Use ON to start discovery after creation.
|
||||
.TP
|
||||
.B IMPORT DAT
|
||||
With no remote address entered, load an existing archive in the selected local
|
||||
directory. To import a remote archive, enter a dat:// URL in the
|
||||
right-hand SITE or ARCHIVE field, select an empty destination directory, then
|
||||
click IMPORT DAT. Verified metadata and content blocks are cached below that
|
||||
directory's .dat folder.
|
||||
.TP
|
||||
.B Escape
|
||||
Toggle the client menu.
|
||||
.TP
|
||||
.B Mouse
|
||||
Click OFF to disconnect all peers and go offline. Click ON to bring the client
|
||||
online and announce loaded archives. With no loaded archives it still connects
|
||||
to Cesspool without announcing a topic. Archive selection changes the local view.
|
||||
Click entries in the right-hand pane to navigate
|
||||
directories or select a filename. Click ARCHIVES above the pane to switch
|
||||
between known local archives. The directory pane has its own scrollbar and
|
||||
mouse-wheel scrolling. The bottom archive/transfer table includes known shared
|
||||
archives and active transfers; its horizontal scrollbar can be dragged, and
|
||||
the mouse wheel scrolls rows (Shift+wheel scrolls columns). Enter a remote dat:// address in the mirrored
|
||||
SITE or ARCHIVE field before selecting IMPORT DAT. Drag transfer-table header dividers to resize
|
||||
columns; use Shift+wheel over the table to scroll horizontally. The bottom
|
||||
track indicates horizontal position. Right-click opens load, create, and
|
||||
copy-address actions. COPY includes the selected archive version in its dat://
|
||||
link.
|
||||
.SH NETWORK
|
||||
The client reads endpoints saved by \fBdat bootstrap add host:port\fR or
|
||||
\fBdat bootstrap set host:port\fR from
|
||||
$HOME/.config/dat-gpt/bootstrap and also accepts DDAT_BOOTSTRAP as a comma- or
|
||||
semicolon-separated list. The Settings menu also edits this endpoint and
|
||||
restarts discovery immediately. Run \fBcesspool-bootstrap\fR on the rendezvous
|
||||
host with \fB--advertise HOST:PORT\fR to print the address clients should use.
|
||||
It does not query an outside service for a public IP. Allow the chosen UDP
|
||||
port through the host firewall and router.
|
||||
This is Cesspool rendezvous for these C clients; it is not a BitTorrent DHT
|
||||
node and cannot be substituted for the historical JavaScript Dat bootstrap
|
||||
servers. OFF leaves every archive topic and closes the Cesspool socket. ON
|
||||
starts archive discovery; the indicator turns green only after a Cesspool
|
||||
response or connected peer.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B DDAT_BOOTSTRAP
|
||||
Comma- or semicolon-separated Cesspool rendezvous endpoints, for example
|
||||
127.0.0.1:3282.
|
||||
Saved endpoints from dat bootstrap add are also loaded automatically.
|
||||
.TP
|
||||
.B DDAT_PORT
|
||||
UDP listening port. If unset, Cesspool selects an available default port.
|
||||
.TP
|
||||
.B DAT_THANG_MANPAGE
|
||||
Optional path to the dat-thang manual source displayed by F1.
|
||||
.SH MENU
|
||||
Escape opens the vertical Settings, Return, Exit menu. Settings contains
|
||||
persistent controls for the default copied-address format, sonar audio, and
|
||||
connecting on startup. Use Up/Down and Enter, or click an item. Return resumes
|
||||
the client; Exit closes it. Escape from Settings returns to the menu.
|
||||
.SH AUDIO
|
||||
Click the speaker icon to play and send a two-second low square-wave cue.
|
||||
Clicking the dolphin plays and sends a one-second arpeggiated chiptune cue.
|
||||
The dolphin cue goes to connected authenticated peers. The speaker cue refreshes
|
||||
discovery for the active archive and is sent to discovered peers as their secure
|
||||
sessions become ready. Peers play received cues locally; the speaker response
|
||||
causes the peer to return one dolphin chirp, which plays at the sender and marks
|
||||
the peer found. The waveform viewer draws samples captured from the actual SDL
|
||||
audio output.
|
||||
.SH BUILD DATE
|
||||
The header shows the program build date in Discordian format (ddate). The
|
||||
calendar conversion is compiled directly into dat-thang; no external ddate
|
||||
program or runtime lookup is needed on Windows or GNU/Linux.
|
||||
.SH DILLO
|
||||
To install the Dillo DPI helper for the current Dillo source, build with make, run make install using the
|
||||
same PREFIX as Dillo, add proto.dat=dat/dat.dpi to ~/.dillo/dpidrc, and restart
|
||||
Dillo. The helper first reads its local archive cache at
|
||||
~/.dillo/dat/archives/XX/<key-tail>. On a cache miss, it can use the configured
|
||||
Cesspool bootstrap to find a peer, authenticate the SLEEP session, and fetch
|
||||
the archive metadata and content feeds into that cache. Start the bootstrap
|
||||
before the clients so the first archive announcement is recorded. The Linux
|
||||
side-by-side runbook uses the default GNU/Linux output path, build/linux, to
|
||||
keep its objects separate from Windows artifacts in a shared checkout.
|
||||
.SH SEE ALSO
|
||||
ddat(1), dat(1)
|
||||
+70
-2
@@ -25,14 +25,82 @@
|
||||
|
||||
.Oc
|
||||
.Sh DESCRIPTION
|
||||
[WIP] dis dat mf thang
|
||||
The C dat command creates and maintains local Dat archives. Archive metadata
|
||||
is append-only, so each metadata entry advances the archive version and older
|
||||
file contents remain addressable by their earlier version number.
|
||||
.Pp
|
||||
The status subcommand reports the archive key, current version, metadata entry
|
||||
count, and latest live path counts. The files subcommand lists the paths at
|
||||
the current version, or at a selected historical version.
|
||||
.Pp
|
||||
Canonical archive addresses use the historical 64-character hexadecimal key.
|
||||
Base32768 is available for compact test addresses with Cm dat url --compact;
|
||||
the parser accepts both forms. Secret key filenames and dat-dns records
|
||||
continue to use hexadecimal keys.
|
||||
.Pp
|
||||
The library provides an archive session, Cesspool discovery, and a Hypercore
|
||||
feed codec. The `hypercore` build selects the legacy V6 wire session. Discovery
|
||||
and transport establishment are separate layers.
|
||||
.Sh COMMANDS
|
||||
.Bl -tag -width "checkout"
|
||||
.It Cm create , init
|
||||
Create an archive, write its title, description, and dat:// address as a
|
||||
`dat.scm` S-expression, and print the address. New archives do not get a
|
||||
`dat.json` sidecar. `--title` and `--description` set the presentation fields.
|
||||
.It Cm share , sync
|
||||
Import changed local paths into the archive.
|
||||
.It Cm status
|
||||
Show the archive key and metadata/version counts.
|
||||
.It Cm files Op version
|
||||
List the live files and directories at a version.
|
||||
.It Cm log
|
||||
Show appended metadata entries, including history and tombstones.
|
||||
.It Cm cat Ar version path
|
||||
Write a historical file's bytes to standard output.
|
||||
.It Cm checkout Ar version destination
|
||||
Export a historical snapshot to a directory.
|
||||
.It Cm url Op version Op path
|
||||
Print an archive URL, optionally pinned to a version and path.
|
||||
.It Cm resolve Ar hostname
|
||||
Resolve a dat-dns hostname through the legacy HTTPS /.well-known/dat response
|
||||
and print its canonical dat:// key and TTL. This does not use the operating
|
||||
system DNS TXT resolver.
|
||||
.It Cm dns set Ar hostname archive-dir|dat://key
|
||||
Create or replace this machine's local dat-dns name mapping. Names are stored
|
||||
in Pa ~/.dat/dat-dns and used by resolve and the dat-thang SITE field. For
|
||||
public Dillo-compatible resolution, publish an HTTPS /.well-known/dat file
|
||||
whose first line is dat:// followed by the archive's 64-digit key and whose
|
||||
optional second line is TTL=<seconds>.
|
||||
.It Cm dns list
|
||||
List local dat-dns name mappings.
|
||||
.It Cm dns remove Ar hostname
|
||||
Remove a local dat-dns name mapping.
|
||||
.It Cm verify Op dir
|
||||
Verify metadata and content feeds with the archive's public keys.
|
||||
.It Cm bootstrap Op udp-port
|
||||
Run a Cesspool rendezvous node on all local interfaces. The node prints the
|
||||
DDAT_BOOTSTRAP setting participating C clients should use. The UDP port must
|
||||
be reachable from those clients through the host firewall and router. Peer
|
||||
records expire after ten minutes without an announce or lookup. This is the
|
||||
Cesspool protocol used by dat-thang and cable-thang, not the historical
|
||||
JavaScript Dat BitTorrent DHT bootstrap protocol.
|
||||
.It Cm bootstrap add Ar host:port
|
||||
Save a Cesspool rendezvous endpoint for dat-thang and cable-thang in the
|
||||
per-user configuration file. It is used in addition to DDAT_BOOTSTRAP.
|
||||
.It Cm bootstrap set Ar host:port
|
||||
Replace the active rendezvous list with one Cesspool endpoint. The desktop
|
||||
client exposes the same setting under Settings, so this command is only needed
|
||||
for terminal-only setup.
|
||||
.El
|
||||
.Sh OPTIONS
|
||||
.It Fl d
|
||||
Debug
|
||||
.Sh FILES
|
||||
.Bl -tag -width -indent
|
||||
.It Pa ~/.dat/secret_keys/
|
||||
its there
|
||||
Contains private feed secrets as raw Ed25519 secret bytes, indexed by
|
||||
discovery key. See the key format and Dat Desktop compatibility notes in
|
||||
the installed key-management.md documentation.
|
||||
.Sh AUTHORS
|
||||
"Dat" was first written about by Mathias Buus Madsen & Maxwell Ogden (c) 2015
|
||||
.Nm
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# `dat-thang` v0.0.002
|
||||
|
||||
This release is a prototype in pre-alpha.
|
||||
|
||||
Build with `make ENABLE_SDL2=1` (requires SDL2 and SDL2_ttf development files); the Dat GUI is `build/linux/dat-thang` and the
|
||||
separate Cable room chat client is `build/linux/cable-thang`. `make
|
||||
ENABLE_SDL2=1 install` installs both clients and their manuals. Run
|
||||
`build/linux/dat-thang --archive DIRECTORY --site HOSTNAME` to begin in a local
|
||||
directory. The initial directory defaults to `.`.
|
||||
|
||||
The GNU/Linux default build path is `build/linux`; the native Windows target
|
||||
uses `build/windows`. Set `BUILD=...` only when you intentionally need another
|
||||
isolated build tree.
|
||||
|
||||
Both interfaces are painted with SDL2 and intentionally resemble terminal
|
||||
clients: fixed-width labels, high-contrast panes, keyboard shortcuts, and
|
||||
terminal-style status text. They do not use Notcurses. For the existing
|
||||
JavaScript Cabal terminal client, run `cabal-cli <room-address>`; `cable` is
|
||||
only the C address and identity utility.
|
||||
|
||||
Both clients load the bundled `assets/fonts/MorePerfectDOSVGA.ttf`, verify its
|
||||
family name is `More Perfect DOS VGA`, and render hard edged glyphs at 14 px
|
||||
with SDL2_ttf. Installed clients load that same font from
|
||||
`$(PREFIX)/share/datgpt/fonts/`; there is no alternate font setting.
|
||||
|
||||
The dat-thang speaker graphic is the original 84 by 84 pixel crop from the
|
||||
provided mockup, stored as `assets/datthang-speaker.bmp`. Green pixels from the
|
||||
mockup background are transparent in the GUI. Installed clients load it from
|
||||
`$(PREFIX)/share/datgpt/icons/`.
|
||||
|
||||
The top-left OFF/ON switch controls the client network. OFF leaves every joined
|
||||
topic, unannounces when possible, closes the Cesspool socket, and clears peer
|
||||
state. ON connects and announces only archives already loaded in the archive
|
||||
list; navigating the filesystem never creates or loads an archive. The status
|
||||
indicator turns green after a Cesspool response or connected peer. F9 loads an
|
||||
existing archive or imports the entered remote link. `--offline` starts with
|
||||
network discovery disabled.
|
||||
|
||||
Run `build/linux/cesspool-bootstrap --port 3282 --advertise HOST:3282` on the
|
||||
rendezvous host. It prints the endpoint you supplied and a Windows firewall
|
||||
rule; it does not query an outside IP lookup service. Forward UDP 3282 through
|
||||
the edge router or cloud firewall. In each GUI, open Escape > Settings
|
||||
> Cesspool Node and enter that reachable `host:port`; saving it restarts
|
||||
discovery. Terminal users can use `dat bootstrap set host:port` or `cable
|
||||
bootstrap set host:port`. `CESSPOOL_BOOTSTRAP` and `DDAT_BOOTSTRAP` accept comma- or
|
||||
semicolon-separated endpoints. `DDAT_PORT` selects each client's local UDP
|
||||
port. With no explicit `DDAT_PORT`, dat-thang listens on UDP 3282 and
|
||||
cable-thang on UDP 3000 so same-LAN discovery can reach both clients on their
|
||||
stable ports. If the preferred port is busy, a client chooses another known
|
||||
Cesspool port; set `DDAT_PORT` to pin a specific local port. The node keeps short-lived peer announcements and returns endpoints
|
||||
announcing the same topic. This is Cesspool rendezvous for the C clients, not
|
||||
the historical JavaScript Dat/Bittorrent DHT.
|
||||
|
||||
The pane to the right of the Merkle graph is a local filesystem browser. Click
|
||||
a directory to navigate into and select it as the local directory. Click a file
|
||||
to select its name. The bottom bar reports that filename and selected local
|
||||
directory. `CREATE NEW DAT` opens or creates that directory's archive and begins
|
||||
discovery when online. With no remote address entered, `IMPORT DAT` opens and
|
||||
syncs the archive already present in the selected directory. To share an archive,
|
||||
copy its address with the `COPY` control beside the local ARCHIVE field.
|
||||
Double-click a peer in the buddy list to open private chat, paste the address
|
||||
with Ctrl+V, and press Enter to send it. The recipient clicks the received
|
||||
`dat://` message to copy its address, selects an empty destination directory,
|
||||
clicks the right-hand ARCHIVE field, presses Ctrl+V, then Enter. Enter starts the remote import; an incoming
|
||||
archive row appears in the transfer table as peer discovery and metadata download
|
||||
begin. The mirrored SITE, ARCHIVE, and VERSION fields show its hostname, key,
|
||||
path, and selected version. Received metadata and content blocks are
|
||||
proof-verified and cached under that directory's `.dat` folder. The client
|
||||
learns the content-feed key from the authenticated, verified metadata header.
|
||||
The status beside the remote fields reports address, discovery, and feed state.
|
||||
Escape opens the vertical Settings, Return, Exit menu. Settings persists the
|
||||
default address format, sonar audio, and online-at-startup preferences; use
|
||||
Up/Down and Enter or click a row.
|
||||
The table columns can be resized by dragging their header dividers;
|
||||
the wheel scrolls horizontally over the transfer table. Its bottom track shows
|
||||
the current horizontal position. The ARCHIVES control above the filesystem pane
|
||||
switches among remembered shared directories; the list persists in
|
||||
`$HOME/.dat/dat-thang-shares`. Right-click a transfer for pause/resume, cancel,
|
||||
reveal destination, or copy address. Right-click elsewhere for load/share,
|
||||
create, and copy-address actions.
|
||||
For each locally shared archive, the table's TITLE and DESCRIPTION come from
|
||||
the archive root's `dat.scm` S-expression, for example
|
||||
`((title "dat-thang") (description "dat:// implemented over c & scheme"))`.
|
||||
The client reads this file as Scheme data; missing fields fall back to the
|
||||
directory name and archive address.
|
||||
|
||||
The client passes discovered Cesspool peers to the archive session, opens uTP,
|
||||
authenticates each connection with a signed nonce exchange bound to the peer
|
||||
key, then passes session frames through the feed handshake and replication
|
||||
code. The peer pane distinguishes discovered announcements from authenticated
|
||||
connections. The lower table lists authenticated archive sessions and their
|
||||
replication state. Upload/download readouts count session frames, and the top
|
||||
diamonds pulse on payload activity. The center hexagon is red offline, amber
|
||||
online without a connected peer, and green when connected. Awkward Moments,
|
||||
Share Guy, and Rank remain planned placeholders. Received blocks are verified
|
||||
against their SLEEP proofs and stored in per-feed `.remote` caches, then
|
||||
reloaded when the next session starts.
|
||||
|
||||
The original dolphin-in-a-porthole and radar/speaker art from the mockup are
|
||||
embedded in the client. The dolphin sits at the upper right and sends a
|
||||
one-second arpeggiated chiptune sonar cue to authenticated connected peers. The
|
||||
speaker sits at the lower right of the YOU panel and plays a two-second low
|
||||
square-wave cue; while online on an archive topic, it refreshes discovery and
|
||||
sends the cue to discovered peers as their authenticated sessions come up. A
|
||||
receiving peer plays the speaker cue and returns one dolphin chirp; the sender
|
||||
plays that echo and marks the peer found. The waveform panel plots samples
|
||||
captured from SDL's audio output ring, so it shows the actual waveform being
|
||||
played. Cues travel as a small Dat-thang UI record over the authenticated
|
||||
Cesspool/uTP connection and never enter archive feeds.
|
||||
|
||||
F1 opens the client manual from the source tree or installed manpage. Up/Down
|
||||
and the mouse wheel scroll it; Escape closes it. Escape toggles the menu when
|
||||
the manual is closed. The source can be selected with `DAT_THANG_MANPAGE`.
|
||||
|
||||
The site field shows the explicit hostname supplied at launch or a local
|
||||
`dat dns set` alias associated with the selected archive. Beaker's legacy
|
||||
`dat:` handler resolves hostnames to keys before opening an archive, and its
|
||||
`dat-dns` supports DNS-over-HTTPS TXT and `/.well-known/dat`; the bundled
|
||||
Dillo plugin disables the first option and uses HTTPS `/.well-known/dat`.
|
||||
The C resolver follows that Dillo behavior after checking local aliases; it
|
||||
does not call the operating system DNS TXT resolver. The archive field defaults
|
||||
to the historical 64-digit hexadecimal address; its format control switches to
|
||||
compact Base32768, and COPY produces the selected format.
|
||||
The build date uses the Discordian `ddate` calendar.
|
||||
The Merkle panel is a shape preview driven by metadata entry count, not hashes
|
||||
or proofs.
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
# Dillo integration
|
||||
|
||||
Use the Dillo source maintained at <https://git.dillo-browser.org/dillo>.
|
||||
The project also lists <https://codeberg.org/dillo/dillo> as an up-to-date
|
||||
mirror; that mirror was used for the Linux run documented below.
|
||||
The latest published release is 3.2.0; the current source tree identifies its
|
||||
development version as 3.3.0. The old `dillo.org` host is not controlled by the
|
||||
Dillo developers.
|
||||
|
||||
`make` builds `$(BUILD)/dat.dpi` (normally `build/linux/dat.dpi`), a native C Dillo
|
||||
Plugin Interface (DPI) helper.
|
||||
It uses Dillo's external DPIP protocol and does not link against Dillo internals
|
||||
or depend on a particular browser ABI. The helper authenticates each accepted
|
||||
DPIP connection with the `~/.dillo/dpid_comm_keys` value and handles `open_url`
|
||||
and `DpiBye`. It follows the historical plugin lifecycle: `dpid` passes its
|
||||
listening socket on stdin, the helper accepts a request connection, and closes
|
||||
that connection after the response body. DPIP responses do not use HTTP status
|
||||
lines or `Content-Length`; closing the request socket marks the end of the
|
||||
page. Current Dillo
|
||||
continues to load `.dpi` plugins and associate them with protocols in
|
||||
`~/.dillo/dpidrc`.
|
||||
|
||||
Build and install the DPI with the same prefix used by Dillo. For the usual
|
||||
`/usr/local` installation:
|
||||
|
||||
```sh
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
If Dillo uses a different plugin directory, set `PREFIX` and `DPIDIR` on both
|
||||
commands. The default plugin path is `/usr/local/lib/dillo/dpi/dat/dat.dpi`.
|
||||
The per-user `~/.dillo/dpidrc` must contain both the DPI root and this mapping
|
||||
(Dillo does not merge `dpi_dir` from the system config into the per-user file):
|
||||
|
||||
```ini
|
||||
dpi_dir=/usr/local/lib/dillo/dpi
|
||||
proto.dat=dat/dat.dpi
|
||||
```
|
||||
|
||||
Then restart Dillo (which restarts `dpid`). The plugin first checks Dillo's
|
||||
local archive cache. On a cache miss it can join the archive's Cesspool topic,
|
||||
authenticate a peer session, fetch the metadata and content feeds, and then
|
||||
serve the requested path. Both Dillo's DPI and the seeding Dat-thang client
|
||||
must use the same Cesspool bootstrap node. Configure that endpoint for the
|
||||
environment that launches each program:
|
||||
|
||||
```sh
|
||||
build/linux/dat bootstrap set 127.0.0.1:3282
|
||||
```
|
||||
|
||||
The command stores the node in `~/.config/dat-gpt/bootstrap`. For a public
|
||||
test, use the reachable `HOST:PORT` of the Cesspool node instead of the local
|
||||
loopback address. The Cygwin Dillo and native Windows Dat-thang configurations
|
||||
are separate when their `HOME` directories differ, so configure each using its
|
||||
own `dat` executable/environment. The helper does not use Hyperswarm or
|
||||
Hypercore; its remote feed transfer uses this project's Cesspool and SLEEP
|
||||
session protocol.
|
||||
|
||||
Use the full key shown by Dat-thang, then enter `dat://<key>/` in Dillo. The
|
||||
helper caches fetched data at `~/.dillo/dat/archives/XX/<remaining-key>` and
|
||||
serves historical versions, `index.html`, `.html` fallback, explicit `//`
|
||||
directory listings, parent-directory navigation, stat queries, and the MIME
|
||||
rules used by the old `dillo-dat` plugin. Directory listings retain its
|
||||
up/directory/file icons and sort directories first, then names in descending
|
||||
order. The internal `dpi:/dat/` page lists archives opened by this DPI process,
|
||||
shows title, description, and version from `dat.scm` (read as an S-expression
|
||||
without evaluating archive contents), falls back to `CNAME`, and provides the
|
||||
unswarm/reload and quit controls. Status messages identify name resolution,
|
||||
archive retrieval, and file serving. If a fetch cannot complete, it returns a
|
||||
text error page that distinguishes an unconfigured or unreachable node, no
|
||||
peer announcing the archive key, and a peer transfer timeout.
|
||||
|
||||
Version pins use the metadata-entry count shown by `dat status`:
|
||||
`dat://<key>+2/` reads the snapshot at version 2, while `dat://<key>/` follows
|
||||
the latest version. Root navigation looks for `index.html`. `make test` checks
|
||||
the archive behaviors and starts the helper with a real TCP listening socket,
|
||||
then verifies response bytes and EOF framing over an accepted connection.
|
||||
|
||||
## Linux bootstrap, Dat-thang, and Dillo smoke run
|
||||
|
||||
Run these steps from the repository root after installing the build
|
||||
dependencies in `INSTALL`. GNU/Linux objects and executables default to
|
||||
`build/linux`, separate from the Windows build in `build/windows`. The bundled `site/`
|
||||
archive already has an address and three recorded versions; this flow leaves it
|
||||
unchanged.
|
||||
|
||||
Build and install the GUI plus the C Dillo helper:
|
||||
|
||||
```sh
|
||||
./configure
|
||||
make ENABLE_SDL2=1
|
||||
sudo make ENABLE_SDL2=1 install
|
||||
```
|
||||
|
||||
Merge these entries into `~/.dillo/dpidrc`, preserving any other DPI entries,
|
||||
then restart Dillo so `dpid` reloads the mapping:
|
||||
|
||||
```ini
|
||||
dpi_dir=/usr/local/lib/dillo/dpi
|
||||
proto.dat=dat/dat.dpi
|
||||
```
|
||||
|
||||
Use three terminals. Terminal 1 runs a local discovery node for this same-host
|
||||
smoke run. Start it before either client so it receives the initial announce:
|
||||
|
||||
```sh
|
||||
build/linux/cesspool-bootstrap --bind 127.0.0.1 --port 3282 \
|
||||
--state-dir "$HOME/.local/state/dat-gpt/cesspool-bootstrap" \
|
||||
--advertise 127.0.0.1:3282
|
||||
```
|
||||
|
||||
Terminal 2 configures the client and starts the SDL client with the bundled
|
||||
archive explicitly loaded. Leave its window open and online:
|
||||
|
||||
```sh
|
||||
build/linux/dat bootstrap set 127.0.0.1:3282
|
||||
DDAT_BOOTSTRAP=127.0.0.1:3282 DDAT_PORT=3000 \
|
||||
build/linux/dat-thang --archive "$PWD/site"
|
||||
```
|
||||
|
||||
Terminal 3 starts Dillo with an empty per-run cache, so the page must arrive
|
||||
from the archive being announced by Dat-thang. The temporary home gives Dillo
|
||||
its own fresh cache and DPID keys without disturbing the normal profile:
|
||||
keep this shell open for the two `dilloc` version checks below.
|
||||
|
||||
```sh
|
||||
dillo_home=$(mktemp -d)
|
||||
mkdir -p "$dillo_home/.dillo"
|
||||
cat > "$dillo_home/.dillo/dpidrc" <<'EOF'
|
||||
dpi_dir=/usr/local/lib/dillo/dpi
|
||||
proto.dat=dat/dat.dpi
|
||||
EOF
|
||||
archive=$(build/linux/dat url / "$PWD/site")
|
||||
HOME="$dillo_home" DDAT_BOOTSTRAP=127.0.0.1:3282 dillo "$archive/" &
|
||||
sleep 5
|
||||
HOME="$dillo_home" dilloc title
|
||||
HOME="$dillo_home" dilloc dump | grep -F 'The site is online'
|
||||
```
|
||||
|
||||
The latest `site/` snapshot is version 3 and displays “Site is online.” Open
|
||||
the version-1 pin in the existing Dillo window to verify historical navigation;
|
||||
it predates `dat.scm` and `index.html` and shows the earlier archive listing:
|
||||
|
||||
```sh
|
||||
historical=$(build/linux/dat url 1 / "$PWD/site")
|
||||
HOME="$dillo_home" dilloc open "$historical/"
|
||||
sleep 2
|
||||
HOME="$dillo_home" dilloc url
|
||||
HOME="$dillo_home" dilloc dump | grep -F 'dat-logo.png'
|
||||
```
|
||||
|
||||
Return to the setup page at version 3:
|
||||
|
||||
```sh
|
||||
latest=$(build/linux/dat url 3 / "$PWD/site")
|
||||
HOME="$dillo_home" dilloc open "$latest/"
|
||||
sleep 2
|
||||
HOME="$dillo_home" dilloc url
|
||||
HOME="$dillo_home" dilloc title
|
||||
HOME="$dillo_home" dilloc dump | grep -F 'The site is online'
|
||||
```
|
||||
|
||||
For another archive, replace `site` with its loaded local archive directory,
|
||||
and choose a version from `build/linux/dat log DIRECTORY`. The same Cesspool node
|
||||
must be reachable by both the Dat-thang GUI and Dillo helper. The bootstrap
|
||||
process reports announced peer and topic counts. A successful run shows the
|
||||
GUI online, a cold Dillo cache populated with `.dat/metadata.remote` and
|
||||
`.dat/content.remote`, and the Dillo title `Site is online`. With this archive,
|
||||
Dillo loads both `+1` (historical listing) and `+3` (the current setup page).
|
||||
|
||||
## Windows with Cygwin Dillo
|
||||
|
||||
Build the helper from the repository root in Cygwin Bash. The DPI uses the
|
||||
normal Cygwin `gcc`, `make`, `libcurl-devel`, and `libsodium-devel` packages.
|
||||
The helper links the bundled S7 reader so `dat.scm` metadata is parsed using the
|
||||
project's Scheme representation; it never evaluates archive-supplied forms.
|
||||
|
||||
```sh
|
||||
make build/linux/dat.dpi
|
||||
install -d /usr/local/lib/dillo/dpi/dat
|
||||
install -m 755 build/linux/dat.dpi /usr/local/lib/dillo/dpi/dat/dat.dpi.exe
|
||||
```
|
||||
|
||||
Put both entries in `~/.dillo/dpidrc` (for this installation that is
|
||||
`C:\cygwin64\home\Home\.dillo\dpidrc`):
|
||||
|
||||
```ini
|
||||
dpi_dir=/usr/local/lib/dillo/dpi
|
||||
proto.dat=dat/dat.dpi.exe
|
||||
```
|
||||
|
||||
Restart Dillo so `dpid` reads the configuration. The `site/` folder in this
|
||||
repository is the archive source; Dillo's cache is separate at
|
||||
`~/.dillo/dat/archives/XX/<remaining-key>`. A seeded archive is browseable only
|
||||
after its feed data is present in that cache or a live Cesspool fetch completes.
|
||||
The setup page source is `site/index.html`. `tests/test_dpi.sh` checks the
|
||||
historical and latest snapshots, S-expression metadata, listing, stat, and
|
||||
dashboard output. The Windows end-to-end flow, including `+N` navigation across
|
||||
multiple archives, has been confirmed by the user. The Linux cold-cache flow
|
||||
above was also run against current Dillo source and the Cesspool bootstrap.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Archive addresses, keys, and write access
|
||||
|
||||
## ELI5
|
||||
|
||||
A Dat address is like a mailbox number that is also a read capability: anyone
|
||||
who knows it can request the archive. The address is not the pen. The private
|
||||
metadata signing key is the pen that lets a writer append new archive history.
|
||||
Dat Desktop's `dat-node` exposes this distinction as `archive.writable`; its
|
||||
file importer requires a writable archive. A reader can download and serve the
|
||||
archive without being able to change its history.
|
||||
|
||||
The historical address
|
||||
`dat://59d8099399510e23a3c693fb5887b7bba1586f4117f7ef1c5872622ada0c1181`
|
||||
contains the public metadata key. It does not contain or reveal the private
|
||||
signing key. Losing that secret does not make the archive unreadable; it means
|
||||
you cannot write as its existing owner. Making a new keypair creates a different
|
||||
archive address. Dat's original archive model is single-writer: a reader does
|
||||
not gain write permission by downloading a copy.
|
||||
|
||||
## How Dat Desktop stores the secret
|
||||
|
||||
Dat Desktop's `dat-secret-storage` indexes each Hypercore feed's secret by its
|
||||
**discovery key**, not by the `dat://` metadata public key. The discovery key
|
||||
is BLAKE2b of `hypercore`, keyed by that feed's public key. The Unix path is
|
||||
sharded:
|
||||
|
||||
```text
|
||||
$HOME/.dat/secret_keys/<first-two-discovery-key-hex>/<remaining-discovery-key-hex>
|
||||
```
|
||||
|
||||
The file is random-access storage for the feed's raw 64-byte Ed25519 secret.
|
||||
The C client reads and writes that same file path and byte format. Dat Desktop
|
||||
also uses a separate local owner marker to avoid competing local writers; the
|
||||
C client does not currently maintain that marker. See the
|
||||
[archived dat-secret-storage implementation](https://github.com/dat-ecosystem-archive/dat-secret-storage/blob/master/index.js)
|
||||
and [dat-storage documentation](https://www.skypack.dev/view/dat-storage).
|
||||
|
||||
## What this C implementation does
|
||||
|
||||
The C implementation now uses the same per-feed discovery-key paths and raw
|
||||
64-byte secret format. Both the metadata feed and content feed have their own
|
||||
files:
|
||||
|
||||
```text
|
||||
$HOME/.dat/secret_keys/<first-two-metadata-discovery-hex>/<remaining-metadata-discovery-hex>
|
||||
$HOME/.dat/secret_keys/<first-two-content-discovery-hex>/<remaining-content-discovery-hex>
|
||||
```
|
||||
|
||||
The public halves remain in `<archive>/.dat/metadata.key` and
|
||||
`<archive>/.dat/content.key`. The C client validates each secret against its
|
||||
matching public key before using it. This discovery-key layout and raw secret
|
||||
file format are the sole supported secret store.
|
||||
|
||||
When the C client opens an archive, its public metadata remains readable
|
||||
without local signing keys. Writing requires the matching metadata and content
|
||||
keypairs in the C secret directory, plus normal operating-system write access
|
||||
to the local archive directory. A local filesystem being writable does not
|
||||
grant Dat write authority; the matching signing secrets are still required.
|
||||
Conversely, having the secrets does not override read-only filesystem
|
||||
permissions.
|
||||
|
||||
## Finding the historical key
|
||||
|
||||
The historical metadata secret's Dat Desktop path is derived from the address
|
||||
above. Compute its `hypercore` discovery key, then use:
|
||||
|
||||
```text
|
||||
$HOME/.dat/secret_keys/<first-two-metadata-discovery-hex>/<remaining-metadata-discovery-hex>
|
||||
```
|
||||
|
||||
The matching content-feed secret is stored by its own content discovery key.
|
||||
The C implementation searches both paths directly; it does not need a
|
||||
conversion step for Dat Desktop's raw secret files. Preserve the original
|
||||
`~/.dat/secret_keys` tree while testing access.
|
||||
|
||||
`dat keys export <archive-directory>` prints the two keypairs as hexadecimal;
|
||||
treat that output as secret. `dat keys import <metadata-keypair-hex>
|
||||
<content-keypair-hex> <archive-directory>` checks that both public halves match
|
||||
the archive before saving their secret bytes in the shared format. Importing newly generated
|
||||
keypairs cannot restore write access to an existing address.
|
||||
|
||||
For context, the archived [dat-node documentation](https://awesome.datproject.org/dat-node)
|
||||
describes `archive.writable` and notes that importing files requires a writable
|
||||
archive. The historical Dat model is single-writer; see the
|
||||
[Dat security FAQ](https://dat-ecosystem.github.io/docs/docs/security-faq).
|
||||
@@ -0,0 +1,62 @@
|
||||
# libcess
|
||||
|
||||
`libcess` is the Cesspool base used by both p2p applications:
|
||||
|
||||
```text
|
||||
┌───────────┐
|
||||
│ libcess │
|
||||
│ UDP, peer │
|
||||
│ discovery│
|
||||
└─────┬─────┘
|
||||
┌───────┴───────┐
|
||||
┌────▼────┐ ┌────▼────┐
|
||||
│ ddat/dat│ │ cable │
|
||||
│ SLEEP │ │ Cabal │
|
||||
│ archives│ │ rooms │
|
||||
└─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
The package owns the UDP socket transport, Cesspool messages, bootstrap and
|
||||
rendezvous client/server logic, peer/topic state, and its Ed25519 bootstrap
|
||||
identity. It has no SLEEP, Dat, Cable, or JSON dependency. Dat and Cable link
|
||||
the same `libcess.a` so a configured `NETWORK_PROTOCOL` selects one discovery
|
||||
network for both applications. The protocol token is data in the root
|
||||
`NETWORK_PROTOCOL` file for the full project; a standalone libcess build reads
|
||||
`libcess/NETWORK_PROTOCOL`.
|
||||
|
||||
## Standalone build
|
||||
|
||||
From the project root:
|
||||
|
||||
```sh
|
||||
make -C libcess
|
||||
make -C libcess install PREFIX="$HOME/.local"
|
||||
```
|
||||
|
||||
This builds `libcess/build/libcess.a` and `libcess/build/cesspool-bootstrap`.
|
||||
Only a C99 compiler, GNU make, and libsodium development files are required.
|
||||
The bootstrap executable links only libcess, libsodium, and the platform UDP
|
||||
socket library; it does not need libcurl or the Dat application library.
|
||||
|
||||
Apps including `cesspool.h` and `cess_net.h` link `libcess.a` after their own
|
||||
static archive, then link libsodium. The installed headers are under
|
||||
`include/libcess/`.
|
||||
|
||||
## Bootstrap node
|
||||
|
||||
`cesspool-bootstrap` is the bootstrap service shipped with the library. It
|
||||
tracks topic announcements and returns peer endpoints; archive and chat data
|
||||
still travels between peers. The process must stay running and its chosen UDP
|
||||
port must be reachable from clients. `--advertise HOST:PORT` prints the
|
||||
externally reachable address; the tool does not call an outside IP lookup
|
||||
service. Configure that address in `ddat` or `cable` settings on each client.
|
||||
|
||||
The default identity location used by the integrated application is
|
||||
`dat-gpt/cesspool-bootstrap-state/identity.key` under the platform's state
|
||||
directory. The raw 96-byte public-plus-secret Ed25519 keypair matches the
|
||||
existing bootstrap identity file layout, so extracting the library keeps a
|
||||
running node's identity stable.
|
||||
|
||||
The old `dat_peer_t`, `datnet_t`, and `datnet_*` spellings remain as thin
|
||||
source-compatibility aliases in the main application headers. New Cesspool
|
||||
consumers should use `cess_peer_t`, `cessnet_t`, and `cessnet_*`.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Network protocol identity
|
||||
|
||||
`NETWORK_PROTOCOL` in the repository root is the single editable protocol
|
||||
token. `cesspool_discovery_key` uses it as the Cesspool topic namespace.
|
||||
Hypercore V6 has no fixed protocol-name field: `userData` is optional
|
||||
application data, and the handshake `id` is protocol-local.
|
||||
|
||||
The default file contains `cesspool`. Production uses the Cesspool discovery
|
||||
and transport path. `hypercore` is reserved for compatibility experiments
|
||||
against the historical Dat/Dillo implementation; it is not the production
|
||||
network stack and does not refer to Cabal's newer Hypercore stack. The V6
|
||||
session opens channel 0 with a clear 24-byte nonce, then encrypts subsequent
|
||||
stream bytes with XSalsa20; channel 1 carries the content feed.
|
||||
|
||||
Dat-thang keeps one client network key in `~/.dat/dat-thang.network-key`.
|
||||
Loading or switching a local or remote archive must not replace that key: the
|
||||
same peer identity announces every archive the client shares. Cable keeps its
|
||||
Cabal-compatible room identity separately. Archive feed keys still determine
|
||||
the archive topic; they are not the client's network identity.
|
||||
The discovery UDP socket tries 3282 first, then 3000, 3002, 3004, 2001, 2003,
|
||||
and 2005, matching Dillo's `hyperdiscovery` port list. Callers can request a
|
||||
specific port.
|
||||
|
||||
The compatibility builds use the packed channel/type header and protobuf payload schema
|
||||
from `extra/schema.proto`. The `cesspool` token selects V7 channel numbering;
|
||||
`hypercore` selects V6 channel numbering. This message codec is independent
|
||||
of the transport and Cesspool's UDP discovery packets.
|
||||
|
||||
The Cesspool V7 handshake includes the selected token in `userData`. The V6
|
||||
Hypercore handshake omits `userData`, because the legacy implementation treats
|
||||
it as application-owned data rather than network identity.
|
||||
|
||||
## Scheme application view
|
||||
|
||||
When S7 is enabled, protocol frames can be viewed as a tagged Scheme record
|
||||
with named fields: `(dat:wire-message (channel . N) (type . N) (payload .
|
||||
#u8(...)))`. `dat-wire->sexp` and `dat-sexp->wire` call the existing C frame
|
||||
codec. The record is an application representation only; the frame header,
|
||||
payload bytes, channel numbering, encryption, and protocol identity continue
|
||||
to come from the selected C network implementation. Scheme code is installed
|
||||
under `share/datgpt/scheme` as part of the default install.
|
||||
@@ -0,0 +1,107 @@
|
||||
# datgpt primitives
|
||||
|
||||
This is the working map of the old `dat://` implementation. Names follow the
|
||||
project: SLEEP owns feeds, cesspool owns Hyperswarm-style discovery, and dat
|
||||
joins the feeds into an archive.
|
||||
|
||||
The standalone [libcess package](libcess.md) owns Cesspool discovery and UDP
|
||||
transport. Both the Dat (`ddat`) and Cable (`cable`) applications link it.
|
||||
|
||||
## SLEEP storage
|
||||
|
||||
- Feed header, tree nodes, signatures, bitfield pages, keys, and data files.
|
||||
- Flat-tree addressing, spans, roots, leaf and parent hashing.
|
||||
- Feed open, length, indexed read, append, flush, and close.
|
||||
- Public-key-only in-memory read-only feeds accept verified remote blocks and
|
||||
retain their proofs without pretending the local node has signing keys. A
|
||||
`.remote` cache reopens sparse blocks only after verifying each stored proof.
|
||||
- Data/tree bit access, index regeneration, scan, encode, and decode.
|
||||
- Key generation, public/secret persistence, hashes, signatures, root signing.
|
||||
- Merkle proof nodes, proof encode/decode, hash folding, and root signature check.
|
||||
|
||||
## Archive
|
||||
|
||||
- Paired metadata and content feeds.
|
||||
- Create, open, close, public keys, writable keypairs.
|
||||
- Separate secret-key save and writable reopen.
|
||||
- Header pointing from metadata to content.
|
||||
- File, directory, symlink, and deletion history entries.
|
||||
- Entry enumeration, reverse path lookup, and content block lookup.
|
||||
- Recursive filesystem import for regular files, directories, and symlinks.
|
||||
- Files are chunked across content blocks and can be exported recursively.
|
||||
- Snapshot sync compares content, mode, target, and millisecond mtime before
|
||||
appending; paths removed locally receive tombstones, while unchanged paths
|
||||
do not extend the feed.
|
||||
- `dat status`, `dat files`, `dat verify`, `dat resolve`, historical `cat`,
|
||||
`checkout`, and versioned URL commands are available from the CLI.
|
||||
|
||||
## Peer protocol
|
||||
|
||||
- The root `NETWORK_PROTOCOL` selects legacy V6 framing (`hypercore`) for the
|
||||
old Dat target; `cesspool` selects V7 framing for the in-tree network mode.
|
||||
- Protobuf feed-open messages, handshake id/live/userData, request/range/data
|
||||
messages, extension payloads, and standard DATA node/signature fields.
|
||||
- Have/want ranges, block requests, cancellation.
|
||||
- Legacy WANT messages with omitted length run to the peer's current feed end;
|
||||
responses are bounded by the local feed length and representable bitfield.
|
||||
- Data plus Merkle proof, and named extensions.
|
||||
- Per-channel replication state, remote bitfield, and request queue.
|
||||
- `dat_session` joins one archive key to its discovery topic, carries discovered
|
||||
peer addresses, opens the archive feeds, gates the wire handshake on an
|
||||
externally authenticated transport, exchanges HAVE ranges, requests missing
|
||||
blocks, and verifies signed proofs before caching received blocks.
|
||||
- A peer callback exposes newly discovered or rediscovered peers so a transport
|
||||
adapter can dial them and call `dat_session_peer_secure` after authentication.
|
||||
- `dat_session_poll` runs an optional transport poll callback alongside the
|
||||
Cesspool UDP poll, refreshes topic membership, and reconciles discovered peers.
|
||||
- `dat_session_tick` is the nonblocking form of that poll cycle.
|
||||
- `dat_session_receive` accepts arbitrary byte-stream chunk boundaries, buffers
|
||||
partial frames, decrypts the V6 stream at byte offsets, and dispatches multiple
|
||||
coalesced frames from one read.
|
||||
- A read-only replica keeps received proofs and can serve them onward. The
|
||||
caller still supplies the authenticated byte transport and poll loop.
|
||||
|
||||
## cesspool
|
||||
|
||||
- UDP socket transport, bootstrap nodes, listen, poll, and destroy.
|
||||
- Legacy lookup, announce, unannounce, ping, and holepunch vocabulary.
|
||||
- Topic join/leave and periodic refresh.
|
||||
- Peer table, failure/last-seen state, topic table, discovery callbacks.
|
||||
- Legacy protobuf requests/responses and compact public/local peer parsing.
|
||||
- XOR-distance peer selection and dht-rpc transaction tracking.
|
||||
|
||||
## uTP
|
||||
|
||||
- Header encode/decode and connection identifiers.
|
||||
- SYN/connect/accept, state acknowledgements, data, FIN, and reset states.
|
||||
- Send segmentation and timer boundary.
|
||||
- Receive reordering, cumulative and selective acknowledgements, congestion
|
||||
window, retransmission timers, and in-flight packet retirement. DATA packets
|
||||
are capped at the 1200-byte payload size, and malformed control packets are
|
||||
rejected before entering the reorder buffer.
|
||||
|
||||
## Name resolution
|
||||
|
||||
- `dat-dns` resolution through HTTPS `/.well-known/dat`, plus the local
|
||||
`~/.dat/dat-dns` name map and TTL-aware cache. The TXT packet decoder is a
|
||||
compatibility helper and is not used for runtime name resolution.
|
||||
- Canonical `dat://<key>` output; the domain remains a name for the resolved
|
||||
archive key rather than a separate archive identity.
|
||||
|
||||
## Later layer
|
||||
|
||||
- S7-facing records use one tagged, named-field form:
|
||||
`(dat:wire-message (channel . N) (type . N) (payload . #u8(...)))`.
|
||||
`dat:record-ref` and `dat:record-set` provide the shared field access layer;
|
||||
protocol payload bytes remain bytevectors.
|
||||
- `dat-wire->sexp` and `dat-sexp->wire` adapt the existing C frame codec. This
|
||||
keeps the C codec authoritative for peer bytes while Scheme is the common
|
||||
application representation; a Scheme record is never serialized directly
|
||||
onto the network.
|
||||
- Initial S7 bindings expose archive create, open, close, entry count, and the
|
||||
wire-record adapter. The Scheme adapters and JSON reader are part of the default install.
|
||||
- Hyperdrive's JS `Index` and `Stat` records should use the same named-field
|
||||
convention as their read/write adapters are added; they must retain their
|
||||
original protocol field numbers and binary fields.
|
||||
- The local CLI sits above the archive API; peer transport polling and the
|
||||
authenticated stream layer remain separate callers of `dat_session`.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Scheme records at protocol boundaries
|
||||
|
||||
## Representation contract
|
||||
|
||||
The old Dat/Cabal implementations use JavaScript objects and JSON because they
|
||||
run on Node. This implementation uses S7 Scheme values and tagged
|
||||
S-expressions as its native application representation. Keep records,
|
||||
configuration, and the C-to-client boundary in that form; translate to JSON
|
||||
only at a compatibility edge whose existing peer format requires JSON. Dat
|
||||
wire bytes and Cabal's canonical Hypercore JSON remain their established
|
||||
protocol formats.
|
||||
|
||||
S7 is the application view for structured records that would otherwise be
|
||||
assembled as JavaScript objects or JSON-shaped maps. New archive descriptions
|
||||
are stored as `dat.scm` association lists. Existing wire codecs, feed files,
|
||||
and peer protocol bytes retain their established formats; `dat.json` is not
|
||||
written for newly created archives.
|
||||
|
||||
## Record form
|
||||
|
||||
Records are tagged lists with named association fields:
|
||||
|
||||
```scheme
|
||||
(dat:wire-message
|
||||
(channel . 1)
|
||||
(type . 3)
|
||||
(payload . #u8(...)))
|
||||
```
|
||||
|
||||
`dat:record-ref` reads a named field and accepts an optional fallback.
|
||||
`dat:record-set` returns a record with one field replaced or added. Names are
|
||||
symbols; strings stay strings; protocol integers stay exact integers; binary
|
||||
data stays an S7 byte-vector. Absence is represented by no field, not by
|
||||
conflating an absent value with `#f` or an empty byte-vector.
|
||||
|
||||
## Wire boundary
|
||||
|
||||
`dat-wire->sexp` and `dat-sexp->wire` adapt the C `dat_wire` frame codec. The
|
||||
frame record keeps the channel, type, and opaque payload bytes named and
|
||||
inspectable. The active C codec remains authoritative for varints, framing,
|
||||
message payloads, channel assignments, protocol identity, and encryption.
|
||||
S-expressions are not sent to Dat peers and do not alter feed blocks or
|
||||
signatures. Cable's local UI and room-event boundary uses S7 and
|
||||
S-expressions inside the native C application; it does not start a Node
|
||||
adapter.
|
||||
|
||||
Protocol-specific adapters can add named records such as `dat:feed-open` or
|
||||
`dat:hyperdrive-stat` above this frame boundary. Each adapter should map
|
||||
explicitly to the existing C type or legacy field numbers, preserve unknown
|
||||
fields when forwarding is required, and keep byte strings as byte-vectors.
|
||||
This allows Cesspool and test-only legacy Hypercore views to share Scheme-level record
|
||||
operations while retaining their separate codecs and wire identities.
|
||||
|
||||
## Build and use
|
||||
|
||||
S7 is bundled and required by the default build. The build produces
|
||||
`ddat-scheme` and links the runtime into the application library. With no
|
||||
arguments it starts the REPL; `-e` evaluates an expression, `-l` loads a file,
|
||||
`-I` adds a module directory, and `--repl` enters the REPL after scripts run. Dat's host
|
||||
runtime registers the C bindings, assembles the module search path, and loads
|
||||
`dat-init.scm`, `dat-record.scm`, `dat-wire.scm`, and `dat-json.scm` before
|
||||
user code. `dat:json-parse` accepts strict JSON and returns tagged Scheme
|
||||
objects/arrays while preserving JSON strings, booleans, numbers, and null.
|
||||
`dat:json-stringify` maps those same tagged Scheme values back to JSON at
|
||||
compatibility boundaries. Scheme/S-expression records remain the application
|
||||
representation; JSON is emitted only where an existing peer format requires
|
||||
it. The test-only Cabal harness owns Cabal's canonical JSON Hypercore encoding.
|
||||
|
||||
The runtime searches `DAT_SCHEME_PATH` (a platform-separated list),
|
||||
`DAT_SCHEME_DIR`, the source-tree `scheme/` directory, and the configured
|
||||
installed directory. Application modules map feature names such as
|
||||
`dat:archive` to `dat/archive.scm`, load through `(dat:require 'dat:archive)`,
|
||||
and finish with `(provide 'dat:archive)`. `dat:with-archive` closes an archive
|
||||
even when its body exits through a Scheme condition. Archive values are S7
|
||||
foreign objects: explicit close is idempotent, and garbage collection closes
|
||||
an archive that the caller abandons.
|
||||
|
||||
This is the embedding boundary S7 expects: the host owns startup, load paths,
|
||||
native types, resource lifetime, CLI policy, and application modules. The C
|
||||
wire/feed implementations still own protocol bytes and on-disk formats.
|
||||
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
# Source and release layout
|
||||
|
||||
The repository has one source tree for both platforms. Generated files have
|
||||
separate destinations:
|
||||
|
||||
- GNU/Linux compiler outputs: `build/linux/`
|
||||
- Windows compiler outputs: `build/windows/`
|
||||
- Windows portable runtime staging: `dist/windows-x64/`
|
||||
- Installers and platform packages: `dist/installers/` and `dist/linux-*/`
|
||||
- Source archive: `dist/source/`
|
||||
|
||||
`build/` is disposable compiler output and is ignored by Git. External Dillo
|
||||
checkouts live in `test-deps/`; machine keys and test-run output live in
|
||||
`local-data/`. Both are local-only and ignored. Reusable tests and archive
|
||||
fixtures live under `tests/`, so a clean operation cannot remove them. Do not
|
||||
zip the working directory directly. To make a source-only archive that retains
|
||||
the historical fixtures and interoperability tests while excluding machine
|
||||
data and generated outputs, run from PowerShell at the repository root:
|
||||
|
||||
```powershell
|
||||
.\package-source.ps1
|
||||
```
|
||||
|
||||
The script writes `dist\source\dat-gpt-v0.0.002-source.zip` (the version comes
|
||||
from `VERSION`). It includes project sources, documentation, assets, the
|
||||
historical code under `extra/`, `third_party/`, the complete `tests/` tree, and
|
||||
the root historical `.dat` verifier fixture. It excludes installed
|
||||
`node_modules/`, `build/`, `dist/`,
|
||||
`local-data/`, `test-deps/`, `.git/`, per-machine Dillo keys, and generated
|
||||
configure files. It also skips local identity keys, cookie jars, SSH private
|
||||
keys, and `.env` files by name. It does not compile or bundle executables.
|
||||
|
||||
Keep persistent node identity files in per-user application data outside the
|
||||
checkout. The source ZIP excludes local runtime state.
|
||||
|
||||
For target packages, use `make package` on GNU/Linux and
|
||||
`build-windows-installer.ps1` on Windows. The Windows `build-windows.ps1` also
|
||||
creates a portable ZIP without requiring Inno Setup.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Windows build
|
||||
|
||||
The native Windows build uses MSYS2's UCRT64 toolchain and creates a runnable
|
||||
folder of PE executables plus their runtime DLLs and data files. It does not
|
||||
reuse the GNU/Linux `build/` directory or generated `config.mk`.
|
||||
|
||||
Install MSYS2, then install the UCRT64 packages needed by the project:
|
||||
|
||||
```sh
|
||||
pacman -S --needed \
|
||||
mingw-w64-ucrt-x86_64-gcc \
|
||||
mingw-w64-ucrt-x86_64-make \
|
||||
mingw-w64-ucrt-x86_64-pkgconf \
|
||||
mingw-w64-ucrt-x86_64-libsodium \
|
||||
mingw-w64-ucrt-x86_64-curl \
|
||||
mingw-w64-ucrt-x86_64-SDL2 \
|
||||
mingw-w64-ucrt-x86_64-SDL2_ttf
|
||||
```
|
||||
|
||||
From the repository root in PowerShell, run:
|
||||
|
||||
```powershell
|
||||
.\build-windows.ps1
|
||||
```
|
||||
|
||||
Compiler outputs, including tests and intermediate files, stay in
|
||||
`build\windows`. Distribution output is staged separately: `dist\windows-x64`
|
||||
contains only the eight user-facing programs, their runtime DLLs, assets,
|
||||
Scheme files, and manuals. `dist\installers` contains a portable ZIP and, when
|
||||
Inno Setup is installed, the setup executable. Do not distribute the contents
|
||||
of `build\windows` directly. The build date's Discordian calendar conversion
|
||||
is compiled into the GUI; no external `ddate` installation is needed.
|
||||
|
||||
To create just the portable ZIP, run `build-windows.ps1`. To create the install
|
||||
wizard as well, run `build-windows-installer.ps1`.
|
||||
|
||||
`cable-thang.exe` is a native C/S7 client using Cesspool and Sleep. Node.js
|
||||
and Cabal JavaScript modules are needed only for optional interoperability
|
||||
tests. Cable audio and video support are experimental and disabled by default,
|
||||
so the regular Windows build does not require FFmpeg. To enable both, install
|
||||
FFmpeg and build with:
|
||||
|
||||
```powershell
|
||||
pacman -S --needed mingw-w64-ucrt-x86_64-ffmpeg
|
||||
.\build-windows.ps1 -EnableCableMedia
|
||||
```
|
||||
|
||||
The UCRT64 FFmpeg libraries provide Cable's AAC-LC encoder/decoder, OBS MPEG-TS
|
||||
ingest, and low-resolution JPEG video exchange; their runtime DLLs are copied
|
||||
beside the Windows executables. In `cable-thang`, `OBS INPUT` (or
|
||||
Ctrl+Shift+V) listens on local UDP port 5000; OBS can publish MPEG-TS there
|
||||
using its Custom FFmpeg Output.
|
||||
|
||||
The GNU/Linux workflow is `./configure`, `make`, and `make install`; its default
|
||||
compiler output is `build/linux`. To create a staged GNU/Linux tarball, use
|
||||
`make package`. Release artifacts go under `dist/` on both platforms. To create
|
||||
a source-only archive, run `package-source.ps1`; its contents and exclusions
|
||||
are documented in `docs\source-distribution.md`.
|
||||
That configuration checks for libcurl because the Dillo-compatible
|
||||
`dat-dns` lookup uses HTTPS `/.well-known/dat`; it does not call the operating
|
||||
system DNS TXT resolver.
|
||||
|
||||
## Install wizard
|
||||
|
||||
Install [Inno Setup 6](https://jrsoftware.org/isinfo.php) after installing the
|
||||
MSYS2 build tools. From PowerShell at the repository root, run:
|
||||
|
||||
```powershell
|
||||
.\build-windows-installer.ps1
|
||||
```
|
||||
|
||||
The script rebuilds the Windows applications, bundles their DLLs and runtime
|
||||
assets, and creates `dist\installers\dat-thang-setup-0.0.002-x64.exe`.
|
||||
The application identifies itself as `dat-thang v0.0.002 (prototype,
|
||||
pre-alpha)`; the executable file version is `0.0.2.0`.
|
||||
The wizard offers a per-user or all-users install, a destination folder,
|
||||
optional desktop shortcut, optional PATH entry for the terminal commands, and
|
||||
an optional launch at the end. Uninstall removes a PATH entry only when this
|
||||
installer added it. The app and installer use the same generic green hexagon
|
||||
icon. The Start menu includes dat-thang and a command prompt with the installed
|
||||
tools on PATH.
|
||||
|
||||
`cable-thang.exe` is included for opening a room address from the command
|
||||
prompt and connects through Cesspool.
|
||||
|
||||
`cesspool-bootstrap.exe` runs a dedicated Cesspool discovery node in the
|
||||
foreground. It is for an operator deploying a reachable public rendezvous
|
||||
host, not something chat users need to run. The node needs a stable state
|
||||
directory and inbound UDP on the selected port; see `docs\cesspool-bootstrap.1`
|
||||
for process-supervisor and firewall setup. The Windows installer includes the
|
||||
executable and the operator manpage in its `docs` folder.
|
||||
|
||||
Keep the node identity outside the checkout. In PowerShell, run the development
|
||||
executable with `--state-dir "$env:LOCALAPPDATA\dat-gpt\cesspool-bootstrap-state"`.
|
||||
That keeps the private identity key in per-user application data rather than in
|
||||
the source tree or disposable compiler output.
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
@@ -0,0 +1 @@
|
||||
/build/
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,72 @@
|
||||
CC ?= cc
|
||||
AR ?= ar
|
||||
RANLIB ?= ranlib
|
||||
OBJCOPY ?= objcopy
|
||||
INSTALL ?= install
|
||||
PREFIX ?= /usr/local
|
||||
DPIDIR ?= $(PREFIX)/lib/dillo/dpi
|
||||
DESTDIR ?=
|
||||
BUILD ?= build
|
||||
CFLAGS ?= -O2 -std=c99 -Wall -Wextra -Wpedantic
|
||||
CPPFLAGS ?=
|
||||
LDFLAGS ?=
|
||||
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CURL_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags libcurl 2>/dev/null)
|
||||
CURL_LIBS ?= $(shell $(PKG_CONFIG) --libs libcurl 2>/dev/null || echo -lcurl)
|
||||
SODIUM_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags libsodium 2>/dev/null)
|
||||
SODIUM_LIBS ?= $(shell $(PKG_CONFIG) --libs libsodium 2>/dev/null || echo -lsodium)
|
||||
|
||||
CPPFLAGS += -Iinc -DSLEEP_SALT -DDAT_BUILD_DIR=\"$(BUILD)\" -DDAT_NETWORK_PROTOCOL=\"$(shell tr -d '\r\n' < NETWORK_PROTOCOL)\" $(CURL_CFLAGS) $(SODIUM_CFLAGS)
|
||||
LIBS = $(SODIUM_LIBS) $(CURL_LIBS) -lm -ldl
|
||||
|
||||
LIB_SOURCES = src/base32768.c src/cable.c src/cable_feed.c src/dat.c src/dat_browse.c src/dat_dns.c src/dat_fetch.c src/dat_walk.c src/dat_net.c src/dat_wire.c src/dat_session.c \
|
||||
src/cesspool.c src/sleep.c src/sleep_tree.c src/sleep_bitfield.c \
|
||||
src/sleep_proto.c src/sleep_crypto.c src/sleep_writer.c \
|
||||
src/sleep_feed.c src/sleep_proof.c src/sleep_replication.c src/sleep_utp.c
|
||||
LIB_OBJECTS = $(patsubst src/%.c,$(BUILD)/%.o,$(LIB_SOURCES)) $(BUILD)/sleep_verify_lib.o
|
||||
DEPFILES = $(LIB_OBJECTS:.o=.d) $(BUILD)/s7.d
|
||||
|
||||
.PHONY: all clean install install-user
|
||||
all: $(BUILD)/dat.dpi
|
||||
|
||||
$(BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
$(BUILD)/%.o: src/%.c | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/sleep_verify_lib.o: src/sleep_verify.c | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -DSLEEP_VERIFY_NO_MAIN -MMD -MP -c $< -o $@
|
||||
|
||||
# S7 is used only as a parser for archive .scm metadata. Rename its optional
|
||||
# command-line entry point so the DPI owns the process main().
|
||||
$(BUILD)/s7.o: src/s7.c | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/dpi_s7.o: $(BUILD)/s7.o
|
||||
$(OBJCOPY) --redefine-sym main=dat_dpi_s7_standalone_main $< $@
|
||||
|
||||
$(BUILD)/libdatdpi.a: $(LIB_OBJECTS)
|
||||
$(AR) rcs $@ $(LIB_OBJECTS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(BUILD)/dat.dpi: src/dat_dpi.c src/dat_dpi_icons.h $(BUILD)/libdatdpi.a $(BUILD)/dpi_s7.o
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) src/dat_dpi.c $(BUILD)/libdatdpi.a $(BUILD)/dpi_s7.o $(LIBS) -o $@
|
||||
|
||||
install: $(BUILD)/dat.dpi
|
||||
$(INSTALL) -d "$(DESTDIR)$(DPIDIR)/dat"
|
||||
$(INSTALL) -m 755 $(BUILD)/dat.dpi "$(DESTDIR)$(DPIDIR)/dat/dat.dpi"
|
||||
$(INSTALL) -d "$(DESTDIR)$(PREFIX)/share/doc/dat-dpi"
|
||||
$(INSTALL) -m 644 README.md "$(DESTDIR)$(PREFIX)/share/doc/dat-dpi/README.md"
|
||||
|
||||
install-user: $(BUILD)/dat.dpi
|
||||
$(INSTALL) -d "$(HOME)/.dillo/dpi/dat"
|
||||
$(INSTALL) -m 755 $(BUILD)/dat.dpi "$(HOME)/.dillo/dpi/dat/dat.dpi"
|
||||
@if ! test -f "$(HOME)/.dillo/dpidrc"; then printf '%s\n' 'dpi_dir=$(DPIDIR)' > "$(HOME)/.dillo/dpidrc"; fi
|
||||
@if ! grep -q '^proto\.dat=' "$(HOME)/.dillo/dpidrc"; then printf '%s\n' 'proto.dat=dat/dat.dpi' >> "$(HOME)/.dillo/dpidrc"; fi
|
||||
|
||||
clean:
|
||||
rm -rf "$(BUILD)"
|
||||
|
||||
-include $(DEPFILES)
|
||||
@@ -0,0 +1 @@
|
||||
cesspool
|
||||
@@ -0,0 +1,92 @@
|
||||
# DAT protocol plugin for Dillo
|
||||
|
||||
This repository builds the standalone Dillo Protocol Interface (DPI) plugin for
|
||||
`dat://` archives. It is kept separate from the dat-thang application repository
|
||||
so it can be built, packaged, and proposed to Dillo as an independent plugin.
|
||||
|
||||
The plugin implements the Dillo DPI server protocol and serves archive pages
|
||||
from the C implementation. Archive contents and metadata are read through SLEEP;
|
||||
`dat.scm` is parsed as S-expressions with the embedded S7 reader and is never
|
||||
evaluated as executable archive code. The networking identity is the Cesspool
|
||||
protocol token in `NETWORK_PROTOCOL`. Dat-DNS is the project's archive-name
|
||||
mapping and is not public DNS.
|
||||
|
||||
## Build requirements
|
||||
|
||||
- A C99 compiler, GNU Make, and binutils (`objcopy`)
|
||||
- `pkg-config`
|
||||
- libsodium and libcurl development headers and libraries
|
||||
- Dillo 3.0.5 or newer with its DPI daemon (`dpid`)
|
||||
|
||||
On Debian-like systems, the build libraries are commonly provided by
|
||||
`libsodium-dev` and `libcurl4-openssl-dev`; package names vary by distribution.
|
||||
|
||||
Build the plugin:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
The result is `build/dat.dpi`. The build output is ignored by Git.
|
||||
|
||||
## Install for one user
|
||||
|
||||
```sh
|
||||
make install-user DPIDIR=/path/to/dillo/dpi
|
||||
```
|
||||
|
||||
This installs `~/.dillo/dpi/dat/dat.dpi`. If `~/.dillo/dpidrc` does not exist, it creates one with `dpi_dir` set to the supplied `DPIDIR`; choose the system DPI directory used by your Dillo installation. It then adds this association to
|
||||
`~/.dillo/dpidrc` if it is not already present:
|
||||
|
||||
```ini
|
||||
proto.dat=dat/dat.dpi
|
||||
```
|
||||
|
||||
Restart Dillo's plugin daemon (`dpidc stop`, then open Dillo again), and navigate
|
||||
to a `dat://` archive address. User plugins are searched before the configured
|
||||
system DPI directory.
|
||||
|
||||
## System install
|
||||
|
||||
```sh
|
||||
sudo make install
|
||||
```
|
||||
|
||||
By default, this installs to `/usr/local/lib/dillo/dpi/dat/dat.dpi`. Add the
|
||||
following line to `~/.dillo/dpidrc` (or the system `dpidrc` used by your Dillo
|
||||
installation), then restart `dpid`:
|
||||
|
||||
```ini
|
||||
proto.dat=dat/dat.dpi
|
||||
```
|
||||
|
||||
Override paths as needed, for example:
|
||||
|
||||
```sh
|
||||
make install PREFIX=/usr DPIDIR=/usr/lib/dillo/dpi DESTDIR=/tmp/package-root
|
||||
```
|
||||
|
||||
## Protocol/version behavior
|
||||
|
||||
The plugin accepts legacy hexadecimal and compact Base32768 Dat keys. A `+N`
|
||||
version suffix selects the requested archive version, matching the versioned
|
||||
navigation supported by the C archive implementation. The plugin accesses
|
||||
archives through the configured Cesspool networking layer when content is not
|
||||
available locally.
|
||||
|
||||
## Dillo plugin integration
|
||||
|
||||
Dillo plugins are independent programs using the DPI protocol. Dillo searches
|
||||
for `.dpi` executables in user and system DPI directories and associates a URL
|
||||
scheme through `proto.<scheme>=<relative-plugin-path>` in `dpidrc`. See the Dillo
|
||||
User Manual's Plugins and Dpidrc sections for the browser-side configuration.
|
||||
|
||||
The Dillo project requests plugin proposals as a repository link and short
|
||||
description. This repository is prepared for that review; it has no configured
|
||||
remote and has not been submitted upstream.
|
||||
|
||||
## Source and license
|
||||
|
||||
The C implementation and required headers are copied from the dat-thang source
|
||||
project. This repository carries the project's GNU AGPL v3 license. The bundled
|
||||
S7 source retains its upstream notices in `src/s7.c`.
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef BASE32768_H
|
||||
#define BASE32768_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* UTF-8 C-string form of qntm/base32768's BMP-safe Base32768 encoding. */
|
||||
int base32768_encode(const uint8_t *input, size_t input_length,
|
||||
char *output, size_t output_capacity);
|
||||
int base32768_decode(const char *input, uint8_t *output,
|
||||
size_t output_capacity, size_t *output_length);
|
||||
|
||||
/* Enumerate the alphabet: bits=15 selects the 32,768 main characters;
|
||||
* bits=7 selects the 128 possible final padding characters. */
|
||||
int base32768_codepoint_at(unsigned bits, uint32_t index, uint32_t *codepoint);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef CABLE_H
|
||||
#define CABLE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "keys.h"
|
||||
#include "sleep_feed.h"
|
||||
|
||||
#define CABLE_KEY_BYTES 32u
|
||||
#define CABLE_CHAT_TEXT_BYTES 768u
|
||||
#define CABLE_CHAT_WIRE_BYTES (4u + 8u + 2u + CABLE_CHAT_TEXT_BYTES)
|
||||
#define CABLE_CHAT_SEAL_BYTES (CABLE_CHAT_WIRE_BYTES + 48u)
|
||||
|
||||
enum cable_address_scheme {
|
||||
CABLE_ADDRESS_CABAL = 1,
|
||||
CABLE_ADDRESS_CABLE = 2,
|
||||
CABLE_ADDRESS_HYPER = 3
|
||||
};
|
||||
|
||||
typedef struct cable_chat_message {
|
||||
uint64_t timestamp_ms;
|
||||
char text[CABLE_CHAT_TEXT_BYTES + 1u];
|
||||
size_t text_length;
|
||||
} cable_chat_message_t;
|
||||
|
||||
/* A Cabal user address is that user's persistent feed public key. A room
|
||||
* address may additionally carry a room moderation/admin key. */
|
||||
typedef struct cable_room_address {
|
||||
uint8_t key[CABLE_KEY_BYTES];
|
||||
uint8_t admin_key[CABLE_KEY_BYTES];
|
||||
enum cable_address_scheme scheme;
|
||||
int has_admin;
|
||||
} cable_room_address_t;
|
||||
|
||||
/* The schemes share a 32-byte key representation; the caller decides whether
|
||||
* it is a room key or a user feed key. hyper:// is a migration alias only. */
|
||||
int cable_address_parse(const char *address, uint8_t key[CABLE_KEY_BYTES],
|
||||
enum cable_address_scheme *scheme);
|
||||
int cable_address_format(const uint8_t key[CABLE_KEY_BYTES],
|
||||
enum cable_address_scheme scheme, char *out, size_t capacity);
|
||||
int cable_room_address_parse(const char *address, cable_room_address_t *room);
|
||||
int cable_room_address_format(const cable_room_address_t *room,
|
||||
enum cable_address_scheme scheme, char *out, size_t capacity);
|
||||
int cable_user_address_format(const uint8_t user_feed_key[CABLE_KEY_BYTES],
|
||||
enum cable_address_scheme scheme, char *out, size_t capacity);
|
||||
/* Cabal stores the local writer in a room's storage, so derive a per-room
|
||||
* local feed-key path instead of sharing one global identity across rooms. */
|
||||
int cable_user_identity_path(const char *home,
|
||||
const uint8_t room_key[CABLE_KEY_BYTES],
|
||||
char *out, size_t capacity);
|
||||
/* Create a Cabal room signing key and persist it in the room directory. */
|
||||
int cable_room_create(const char *home, uint8_t room_key[CABLE_KEY_BYTES]);
|
||||
/* Store/load the user's local feed secret key. The public key is derived and
|
||||
* checked on load. The file contains only the 64-byte Ed25519 secret key. */
|
||||
int cable_user_identity_load_or_create(const char *path, sleep_keypair_t *identity);
|
||||
/* Cabal stores messages as JSON blocks in each user's Hypercore feed. These
|
||||
* helpers use the same Hypercore 7.x on-disk tree/signature layout. */
|
||||
int cable_user_feed_open(const char *home,
|
||||
const uint8_t room_key[CABLE_KEY_BYTES],
|
||||
sleep_keypair_t *identity, sleep_feed_t *feed);
|
||||
int cable_user_feed_append(const char *home,
|
||||
const uint8_t room_key[CABLE_KEY_BYTES],
|
||||
const uint8_t *record, size_t record_length,
|
||||
uint8_t user_key[CABLE_KEY_BYTES]);
|
||||
int cable_user_feed_publish(const char *home,
|
||||
const uint8_t room_key[CABLE_KEY_BYTES],
|
||||
const char *record_type, const char *nick,
|
||||
const char *channel, const char *text,
|
||||
uint8_t user_key[CABLE_KEY_BYTES]);
|
||||
int cable_chat_message_encode(const cable_chat_message_t *message,
|
||||
uint8_t *out, size_t capacity, size_t *written);
|
||||
int cable_chat_message_decode(cable_chat_message_t *message,
|
||||
const uint8_t *input, size_t length);
|
||||
int cable_chat_seal(const uint8_t recipient_ed25519[32],
|
||||
const uint8_t *message, size_t message_length,
|
||||
uint8_t *out, size_t capacity, size_t *written);
|
||||
int cable_chat_open(const sleep_keypair_t *identity,
|
||||
const uint8_t *sealed, size_t sealed_length,
|
||||
uint8_t *out, size_t capacity, size_t *written);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef CABLE_MEDIA_H
|
||||
#define CABLE_MEDIA_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define CABLE_MEDIA_SESSION_BYTES 16u
|
||||
#define CABLE_MEDIA_HEADER_BYTES 42u
|
||||
#define CABLE_MEDIA_MAX_PACKET_BYTES 4096u
|
||||
#define CABLE_MEDIA_AAC_RATE 48000u
|
||||
#define CABLE_MEDIA_AAC_BITRATE 96000u
|
||||
#define CABLE_MEDIA_AAC_CHANNELS 1u
|
||||
#define CABLE_MEDIA_AAC_SAMPLES 1024u
|
||||
|
||||
enum cable_media_kind {
|
||||
CABLE_MEDIA_AUDIO_START = 1,
|
||||
CABLE_MEDIA_AUDIO_FRAME = 2,
|
||||
CABLE_MEDIA_AUDIO_STOP = 3,
|
||||
CABLE_MEDIA_VIDEO_START = 4,
|
||||
CABLE_MEDIA_VIDEO_FRAME = 5,
|
||||
CABLE_MEDIA_VIDEO_STOP = 6
|
||||
};
|
||||
|
||||
typedef struct cable_media_packet {
|
||||
uint8_t kind;
|
||||
uint8_t session[CABLE_MEDIA_SESSION_BYTES];
|
||||
uint64_t sequence;
|
||||
uint64_t timestamp_us;
|
||||
const uint8_t *payload;
|
||||
size_t payload_length;
|
||||
} cable_media_packet_t;
|
||||
|
||||
int cable_media_packet_write(const cable_media_packet_t *packet,
|
||||
uint8_t *output, size_t capacity, size_t *written);
|
||||
int cable_media_packet_read(cable_media_packet_t *packet,
|
||||
const uint8_t *input, size_t length);
|
||||
|
||||
typedef struct cable_aac_encoder { void *implementation; } cable_aac_encoder_t;
|
||||
typedef struct cable_aac_decoder { void *implementation; } cable_aac_decoder_t;
|
||||
typedef int (*cable_aac_frame_fn)(void *context, const uint8_t *adts,
|
||||
size_t length);
|
||||
typedef int (*cable_pcm_samples_fn)(void *context, const int16_t *samples,
|
||||
size_t sample_count);
|
||||
|
||||
int cable_aac_encoder_open(cable_aac_encoder_t *encoder);
|
||||
int cable_aac_encoder_frame(cable_aac_encoder_t *encoder,
|
||||
const int16_t samples[CABLE_MEDIA_AAC_SAMPLES],
|
||||
cable_aac_frame_fn output, void *context);
|
||||
void cable_aac_encoder_close(cable_aac_encoder_t *encoder);
|
||||
int cable_aac_decoder_open(cable_aac_decoder_t *decoder);
|
||||
int cable_aac_decoder_frame(cable_aac_decoder_t *decoder,
|
||||
const uint8_t *adts, size_t length,
|
||||
cable_pcm_samples_fn output, void *context);
|
||||
void cable_aac_decoder_close(cable_aac_decoder_t *decoder);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef CABLE_ROOM_H
|
||||
#define CABLE_ROOM_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#define CABLE_ROOM_CHANNEL_BYTES 64u
|
||||
#define CABLE_ROOM_NICK_BYTES 64u
|
||||
|
||||
enum cable_room_event_type {
|
||||
CABLE_ROOM_PEER_JOIN = 1,
|
||||
CABLE_ROOM_PEER_LEAVE = 2,
|
||||
CABLE_ROOM_NICK = 3,
|
||||
CABLE_ROOM_CHANNEL_JOIN = 4,
|
||||
CABLE_ROOM_TEXT = 5
|
||||
};
|
||||
|
||||
typedef struct cable_room_event {
|
||||
uint8_t type;
|
||||
uint64_t timestamp_ms;
|
||||
uint8_t sender[CABLE_KEY_BYTES];
|
||||
char channel[CABLE_ROOM_CHANNEL_BYTES + 1u];
|
||||
char nick[CABLE_ROOM_NICK_BYTES + 1u];
|
||||
char text[CABLE_CHAT_TEXT_BYTES + 1u];
|
||||
size_t text_length;
|
||||
} cable_room_event_t;
|
||||
|
||||
/* In-memory UI event only. Cable's network records are Cabal Core Hypercore
|
||||
* values and never serialize this C structure. */
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef CABLE_VIDEO_H
|
||||
#define CABLE_VIDEO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct cable_video_source cable_video_source_t;
|
||||
|
||||
/* Read an OBS MPEG-TS feed from a local UDP socket, normally port 5000.
|
||||
* Pixels are copied as RGB24; a generation change means a new frame arrived. */
|
||||
cable_video_source_t *cable_video_source_open(const char *url);
|
||||
void cable_video_source_close(cable_video_source_t *source);
|
||||
int cable_video_source_frame(cable_video_source_t *source, uint8_t *rgb,
|
||||
size_t capacity, unsigned *width, unsigned *height,
|
||||
uint64_t *generation);
|
||||
/* Encode/decode one low-resolution JPEG frame for volatile Cable media. */
|
||||
int cable_video_encode_jpeg(const uint8_t *rgb, unsigned width, unsigned height,
|
||||
uint8_t *jpeg, size_t capacity, size_t *written);
|
||||
int cable_video_decode_jpeg(const uint8_t *jpeg, size_t length, uint8_t *rgb,
|
||||
size_t capacity, unsigned *width, unsigned *height);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,172 @@
|
||||
#ifndef CESSPOOL_H
|
||||
#define CESSPOOL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "keys.h"
|
||||
#include "dat_net.h"
|
||||
|
||||
// bootstrap node
|
||||
// dht
|
||||
// discovery
|
||||
// lookup (key) ↳ lookup1
|
||||
// announce
|
||||
// ping
|
||||
// holepunch udp
|
||||
|
||||
#define CESSPOOL_TOPIC_BYTES 32u
|
||||
#define CESSPOOL_PEER_BYTES 32u
|
||||
|
||||
enum cesspool_message_type {
|
||||
CESSPOOL_LOOKUP = 0,
|
||||
CESSPOOL_ANNOUNCE = 1,
|
||||
CESSPOOL_UNANNOUNCE = 2,
|
||||
CESSPOOL_REPLY = 3,
|
||||
CESSPOOL_PING = 4,
|
||||
CESSPOOL_PONG = 5,
|
||||
CESSPOOL_HOLEPUNCH = 6
|
||||
};
|
||||
|
||||
typedef struct cesspool_message {
|
||||
uint8_t type;
|
||||
uint8_t topic[CESSPOOL_TOPIC_BYTES];
|
||||
uint8_t peer_id[CESSPOOL_PEER_BYTES];
|
||||
uint16_t port;
|
||||
uint8_t payload[2048];
|
||||
size_t payload_len;
|
||||
} cesspool_message_t;
|
||||
typedef struct cesspool_request {
|
||||
uint32_t type;
|
||||
uint16_t port;
|
||||
uint8_t local_address[6];
|
||||
size_t local_address_len;
|
||||
} cesspool_request_t;
|
||||
typedef struct cesspool_response {
|
||||
uint8_t peers[1024];
|
||||
size_t peers_len;
|
||||
uint8_t local_peers[1024];
|
||||
size_t local_peers_len;
|
||||
/* Cesspool rendezvous records: IPv4(4), UDP port(2), peer key(32). */
|
||||
uint8_t peer_info[2048];
|
||||
size_t peer_info_len;
|
||||
} cesspool_response_t;
|
||||
|
||||
#define CESSPOOL_MAX_BOOTSTRAP 16u
|
||||
#define CESSPOOL_MAX_PEERS 256u
|
||||
#define CESSPOOL_MAX_TOPICS 64u
|
||||
#define CESSPOOL_MAX_ANNOUNCEMENTS 1024u
|
||||
#define CESSPOOL_MAX_TRANSACTIONS 64u
|
||||
typedef struct cesspool_peer {
|
||||
dat_peer_t address;
|
||||
uint8_t public_key[32];
|
||||
uint8_t discovery_topic[CESSPOOL_TOPIC_BYTES];
|
||||
uint64_t last_seen;
|
||||
unsigned failures;
|
||||
int local;
|
||||
int discovered;
|
||||
} cesspool_peer_t;
|
||||
typedef struct cesspool_topic {
|
||||
uint8_t key[32];
|
||||
uint16_t port;
|
||||
int announcing;
|
||||
int looking_up;
|
||||
uint64_t refresh_at;
|
||||
} cesspool_topic_t;
|
||||
typedef struct cesspool_transaction {
|
||||
uint16_t id;
|
||||
uint8_t target[32];
|
||||
uint8_t type;
|
||||
uint64_t expires_at;
|
||||
int active;
|
||||
} cesspool_transaction_t;
|
||||
typedef struct cesspool_announcement {
|
||||
uint8_t topic[CESSPOOL_TOPIC_BYTES];
|
||||
uint8_t peer_id[CESSPOOL_PEER_BYTES];
|
||||
dat_peer_t address;
|
||||
uint64_t last_seen;
|
||||
} cesspool_announcement_t;
|
||||
struct cesspool;
|
||||
typedef void (*cesspool_transport_packet_fn)(struct cesspool *pool,
|
||||
const dat_peer_t *source,
|
||||
const uint8_t *packet,
|
||||
size_t packet_len);
|
||||
typedef struct cesspool {
|
||||
datnet_t net;
|
||||
dat_peer_t bootstrap_nodes[CESSPOOL_MAX_BOOTSTRAP];
|
||||
size_t bootstrap_count;
|
||||
uint16_t port;
|
||||
int ready;
|
||||
int bootstrap_server;
|
||||
cesspool_peer_t peers[CESSPOOL_MAX_PEERS];
|
||||
size_t peer_count;
|
||||
cesspool_topic_t topics[CESSPOOL_MAX_TOPICS];
|
||||
size_t topic_count;
|
||||
/* A peer may announce many topics; keep each membership independently. */
|
||||
cesspool_announcement_t announcements[CESSPOOL_MAX_ANNOUNCEMENTS];
|
||||
size_t announcement_count;
|
||||
uint64_t bootstrap_last_seen;
|
||||
uint64_t bootstrap_probe_at;
|
||||
int bootstrap_reachable;
|
||||
cesspool_transaction_t transactions[CESSPOOL_MAX_TRANSACTIONS];
|
||||
uint16_t next_transaction;
|
||||
void (*on_peer)(struct cesspool *, const cesspool_peer_t *);
|
||||
void (*on_holepunch)(struct cesspool *, const dat_peer_t *);
|
||||
cesspool_transport_packet_fn on_transport_packet;
|
||||
void *transport_context;
|
||||
} cesspool_t;
|
||||
|
||||
int cesspool_discovery_key(const uint8_t public_key[32], uint8_t topic[32]);
|
||||
int cesspool_encode(const cesspool_message_t *message, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int cesspool_decode(cesspool_message_t *message, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int cesspool_request_encode(const cesspool_request_t *request, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int cesspool_request_decode(cesspool_request_t *request, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int cesspool_response_encode(const cesspool_response_t *response, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int cesspool_response_decode(cesspool_response_t *response, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int cesspool_process_response(cesspool_t *pool,
|
||||
const cesspool_response_t *response,
|
||||
const uint8_t local_prefix[2]);
|
||||
int cesspool_init(cesspool_t *pool, const uint8_t peer_id[32]);
|
||||
int cesspool_add_bootstrap(cesspool_t *pool, const char *host, uint16_t port);
|
||||
/* Load Cesspool bootstrap endpoints from DDAT_BOOTSTRAP and the per-user
|
||||
* config/dat-gpt/bootstrap file. */
|
||||
int cesspool_configure_bootstraps(cesspool_t *pool);
|
||||
int cesspool_bootstrap_config_add(const char *endpoint);
|
||||
int cesspool_bootstrap_config_set(const char *endpoint);
|
||||
int cesspool_bootstrap_config_get(char *endpoint, size_t capacity);
|
||||
int cesspool_bootstrap_config_list(char *contents, size_t capacity);
|
||||
int cesspool_bootstrap(cesspool_t *pool);
|
||||
int cesspool_listen(cesspool_t *pool, const char *host, uint16_t port);
|
||||
int cesspool_set_bootstrap_server(cesspool_t *pool, int enabled);
|
||||
int cesspool_announce(cesspool_t *pool, const uint8_t topic[32], uint16_t port);
|
||||
int cesspool_lookup(cesspool_t *pool, const uint8_t topic[32]);
|
||||
int cesspool_unannounce(cesspool_t *pool, const uint8_t topic[32], uint16_t port);
|
||||
int cesspool_ping(cesspool_t *pool, const dat_peer_t *peer);
|
||||
int cesspool_holepunch(cesspool_t *pool, const dat_peer_t *peer);
|
||||
int cesspool_poll(cesspool_t *pool, int timeout_ms);
|
||||
int cesspool_add_peer(cesspool_t *pool, const cesspool_peer_t *peer);
|
||||
int cesspool_remove_peer(cesspool_t *pool, const uint8_t public_key[32]);
|
||||
cesspool_peer_t *cesspool_find_peer(cesspool_t *pool, const uint8_t public_key[32]);
|
||||
int cesspool_join(cesspool_t *pool, const uint8_t topic[32], uint16_t port,
|
||||
int announce, int lookup);
|
||||
int cesspool_leave(cesspool_t *pool, const uint8_t topic[32]);
|
||||
int cesspool_tick(cesspool_t *pool, uint64_t now_ms);
|
||||
size_t cesspool_closest(cesspool_t *pool, const uint8_t target[32],
|
||||
cesspool_peer_t **out, size_t capacity);
|
||||
size_t cesspool_topic_peers(cesspool_t *pool,
|
||||
const uint8_t topic[CESSPOOL_TOPIC_BYTES],
|
||||
cesspool_peer_t **out, size_t capacity);
|
||||
size_t cesspool_announced_peer_count(const cesspool_t *pool);
|
||||
size_t cesspool_announced_topic_count(const cesspool_t *pool);
|
||||
int cesspool_transaction_begin(cesspool_t *pool, uint8_t type,
|
||||
const uint8_t target[32], uint64_t expires_at,
|
||||
uint16_t *transaction_id);
|
||||
int cesspool_transaction_finish(cesspool_t *pool, uint16_t transaction_id);
|
||||
int cesspool_destroy(cesspool_t *pool);
|
||||
|
||||
#endif /* CESSPOOL_H */
|
||||
@@ -0,0 +1,114 @@
|
||||
#ifndef DAT_H
|
||||
#define DAT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "keys.h"
|
||||
#include "sleep_proto.h"
|
||||
|
||||
#define DAT_KEY_BYTES 32u
|
||||
#define DAT_BLOCK_BYTES (64u * 1024u)
|
||||
#define DAT_URL_BYTES 71u
|
||||
#define DAT_LINK_BYTES 4096u
|
||||
|
||||
typedef struct dat_archive {
|
||||
char dat_path[1024];
|
||||
uint8_t metadata_key[DAT_KEY_BYTES];
|
||||
uint8_t content_key[DAT_KEY_BYTES];
|
||||
sleep_keypair_t metadata_keys;
|
||||
sleep_keypair_t content_keys;
|
||||
} dat_archive_t;
|
||||
typedef struct dat_link {
|
||||
uint8_t key[DAT_KEY_BYTES];
|
||||
uint64_t version;
|
||||
int has_version;
|
||||
char path[2048];
|
||||
} dat_link_t;
|
||||
typedef struct dat_checkout {
|
||||
const dat_archive_t *archive;
|
||||
uint64_t version;
|
||||
} dat_checkout_t;
|
||||
typedef struct dat_url {
|
||||
char host[256];
|
||||
char path[2048];
|
||||
uint64_t version;
|
||||
int has_version;
|
||||
int wants_stat;
|
||||
int force_directory_listing;
|
||||
} dat_url_t;
|
||||
typedef struct dat_path_info {
|
||||
sleep_metadata_entry_t entry;
|
||||
int exists;
|
||||
int is_directory;
|
||||
} dat_path_info_t;
|
||||
typedef int (*dat_archive_change_fn)(const sleep_metadata_entry_t *entry,
|
||||
uint64_t version, void *context);
|
||||
typedef int (*dat_archive_readdir_fn)(const sleep_metadata_entry_t *entry,
|
||||
const char *name, void *context);
|
||||
|
||||
/* Creates a new two-feed legacy-style archive skeleton under root/.dat. */
|
||||
int dat_archive_create(dat_archive_t *archive, const char *root);
|
||||
int dat_archive_open(dat_archive_t *archive, const char *root);
|
||||
int dat_archive_open_writable(dat_archive_t *archive, const char *root,
|
||||
const char *secret_directory);
|
||||
int dat_archive_save_secrets(const dat_archive_t *archive,
|
||||
const char *secret_directory);
|
||||
void dat_archive_close(dat_archive_t *archive);
|
||||
size_t dat_archive_entry_count(const dat_archive_t *archive);
|
||||
uint64_t dat_archive_version(const dat_archive_t *archive);
|
||||
int dat_archive_append_file(dat_archive_t *archive, const char *path,
|
||||
const uint8_t *data, size_t length,
|
||||
uint32_t mode, uint64_t mtime);
|
||||
int dat_archive_append_directory(dat_archive_t *archive, const char *path,
|
||||
uint32_t mode, uint64_t mtime);
|
||||
int dat_archive_append_symlink(dat_archive_t *archive, const char *path,
|
||||
const char *target, uint64_t mtime);
|
||||
int dat_archive_delete(dat_archive_t *archive, const char *path, uint64_t mtime);
|
||||
int dat_archive_read_entry(const dat_archive_t *archive, size_t index,
|
||||
sleep_metadata_entry_t *entry);
|
||||
int dat_archive_read_file(const dat_archive_t *archive,
|
||||
const sleep_metadata_entry_t *entry,
|
||||
uint8_t **data, size_t *length);
|
||||
int dat_archive_find(const dat_archive_t *archive, const char *path,
|
||||
sleep_metadata_entry_t *entry);
|
||||
int dat_archive_find_at(const dat_archive_t *archive, const char *path,
|
||||
uint64_t version, sleep_metadata_entry_t *entry);
|
||||
int dat_checkout_init(dat_checkout_t *checkout, const dat_archive_t *archive,
|
||||
uint64_t version);
|
||||
int dat_checkout_find(const dat_checkout_t *checkout, const char *path,
|
||||
sleep_metadata_entry_t *entry);
|
||||
int dat_checkout_read(const dat_checkout_t *checkout,
|
||||
const sleep_metadata_entry_t *entry,
|
||||
uint8_t **data, size_t *length);
|
||||
int dat_checkout_export(const dat_checkout_t *checkout, const char *archive_path,
|
||||
const char *filesystem_path);
|
||||
int dat_archive_changes(const dat_archive_t *archive, uint64_t from_version,
|
||||
uint64_t to_version, dat_archive_change_fn callback, void *context);
|
||||
int dat_archive_stat_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, dat_path_info_t *info);
|
||||
int dat_archive_readdir_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, dat_archive_readdir_fn callback,
|
||||
void *context);
|
||||
int dat_archive_resolve_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, int force_directory_listing,
|
||||
dat_path_info_t *info, int *serve_directory_listing);
|
||||
const char *dat_content_type(const char *path);
|
||||
int dat_url_parse(const char *text, dat_url_t *url);
|
||||
int dat_archive_import_path(dat_archive_t *archive, const char *filesystem_path,
|
||||
const char *archive_path);
|
||||
/* Imports only changed paths and records tombstones for paths removed locally. */
|
||||
int dat_archive_sync_path(dat_archive_t *archive, const char *filesystem_path,
|
||||
const char *archive_path);
|
||||
int dat_archive_export_path(const dat_archive_t *archive, const char *archive_path,
|
||||
const char *filesystem_path);
|
||||
int dat_archive_export_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *archive_path, const char *filesystem_path);
|
||||
int dat_key_to_url(const uint8_t key[DAT_KEY_BYTES], char url[DAT_URL_BYTES]);
|
||||
int dat_key_to_compact_url(const uint8_t key[DAT_KEY_BYTES], char url[DAT_URL_BYTES]);
|
||||
int dat_key_to_hex(const uint8_t key[DAT_KEY_BYTES], char hex[65]);
|
||||
int dat_url_to_key(const char *url, uint8_t key[DAT_KEY_BYTES]);
|
||||
int dat_link_format(const dat_link_t *link, char *out, size_t capacity);
|
||||
int dat_link_format_compact(const dat_link_t *link, char *out, size_t capacity);
|
||||
int dat_link_parse(const char *text, dat_link_t *link);
|
||||
|
||||
#endif /* DAT_H */
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef DAT_DNS_H
|
||||
#define DAT_DNS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "dat.h"
|
||||
|
||||
typedef int (*dat_dns_name_iter_fn)(const char *hostname,
|
||||
const uint8_t key[DAT_KEY_BYTES], void *context);
|
||||
|
||||
/* Resolve a legacy Dat DNS TXT record such as datkey=<64 hex digits>. */
|
||||
int dat_dns_resolve(const char *hostname, uint8_t key[DAT_KEY_BYTES],
|
||||
uint32_t *ttl_seconds);
|
||||
|
||||
/* Local dat-dns aliases are stored in $HOME/.dat/dat-dns (or USERPROFILE on
|
||||
* native Windows) and override DNS TXT. */
|
||||
int dat_dns_name_set(const char *hostname, const uint8_t key[DAT_KEY_BYTES]);
|
||||
int dat_dns_name_remove(const char *hostname);
|
||||
int dat_dns_name_resolve(const char *hostname, uint8_t key[DAT_KEY_BYTES]);
|
||||
int dat_dns_name_for_key(const uint8_t key[DAT_KEY_BYTES], char *hostname,
|
||||
size_t hostname_capacity);
|
||||
int dat_dns_name_foreach(dat_dns_name_iter_fn callback, void *context);
|
||||
|
||||
/* Decode TXT record character strings from one DNS answer packet. */
|
||||
int dat_dns_parse_answer(const uint8_t *packet, unsigned packet_len,
|
||||
uint8_t key[DAT_KEY_BYTES], uint32_t *ttl_seconds);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef DAT_FETCH_H
|
||||
#define DAT_FETCH_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Fetch the public metadata and content feeds for a Dat key into root/.dat.
|
||||
* The fetch uses the configured Cesspool network and persists verified sparse
|
||||
* feed blocks so a later Dillo request can resume it. */
|
||||
int dat_fetch_archive(const char *root, const uint8_t metadata_key[32],
|
||||
unsigned timeout_ms);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef DAT_NET_H
|
||||
#define DAT_NET_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "dat_socket.h"
|
||||
|
||||
#define DATNET_PORT 3282
|
||||
#define DATNET_MAX_PACKET 1400
|
||||
#define DATNET_TOPIC_BYTES 32
|
||||
#define DATNET_PEERID_BYTES 32
|
||||
|
||||
typedef struct {
|
||||
dat_socket_t fd;
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addrlen;
|
||||
uint8_t id[DATNET_PEERID_BYTES];
|
||||
} dat_peer_t;
|
||||
|
||||
typedef struct datnet {
|
||||
dat_socket_t sock;
|
||||
uint8_t topic[DATNET_TOPIC_BYTES];
|
||||
uint8_t peerid[DATNET_PEERID_BYTES];
|
||||
void *context;
|
||||
void (*on_message)(struct datnet *net, dat_peer_t *peer, const uint8_t *msg, size_t len);
|
||||
} datnet_t;
|
||||
|
||||
int datnet_init(datnet_t *net, const uint8_t topic[DATNET_TOPIC_BYTES],
|
||||
const uint8_t peerid[DATNET_PEERID_BYTES]);
|
||||
int datnet_bind(datnet_t *net, const char *host, uint16_t port);
|
||||
int datnet_poll(datnet_t *net, int timeout_ms);
|
||||
int datnet_send(datnet_t *net, const dat_peer_t *peer,
|
||||
const uint8_t *message, size_t length);
|
||||
int datnet_close(datnet_t *net);
|
||||
|
||||
#endif /* DAT_NET_H */
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef DAT_PATHS_H
|
||||
#define DAT_PATHS_H
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
/* Return the user's home directory across POSIX shells and native Windows
|
||||
* launches. Windows GUI starts commonly have USERPROFILE but no HOME. */
|
||||
static inline const char *dat_home_directory(void)
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
if (home && *home) return home;
|
||||
#ifdef _WIN32
|
||||
home = getenv("USERPROFILE");
|
||||
if (home && *home) return home;
|
||||
{
|
||||
const char *drive = getenv("HOMEDRIVE");
|
||||
const char *path = getenv("HOMEPATH");
|
||||
static char combined[1024];
|
||||
int n;
|
||||
if (!drive || !*drive || !path || !*path) return NULL;
|
||||
n = snprintf(combined, sizeof(combined), "%s%s", drive, path);
|
||||
return n >= 0 && (size_t)n < sizeof(combined) ? combined : NULL;
|
||||
}
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* POSIX rename replaces an existing file; Windows rename does not. These
|
||||
* state files are written to a sibling temporary file before replacement. */
|
||||
static inline int dat_replace_file(const char *temporary, const char *destination)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExA(temporary, destination,
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) return 0;
|
||||
errno = EIO;
|
||||
return -1;
|
||||
#else
|
||||
return rename(temporary, destination);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef DAT_S7_H
|
||||
#define DAT_S7_H
|
||||
#include "s7.h"
|
||||
#include "dat.h"
|
||||
typedef struct dat_scheme_runtime {
|
||||
s7_scheme *scheme;
|
||||
} dat_scheme_runtime_t;
|
||||
|
||||
int dat_s7_init(s7_scheme *scheme);
|
||||
int dat_scheme_runtime_init(dat_scheme_runtime_t *runtime);
|
||||
void dat_scheme_runtime_close(dat_scheme_runtime_t *runtime);
|
||||
int dat_scheme_write_archive_info(s7_scheme *scheme, const char *root,
|
||||
const dat_archive_t *archive,
|
||||
const char *title, const char *description,
|
||||
int replace);
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
#ifndef DAT_SESSION_H
|
||||
#define DAT_SESSION_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "cesspool.h"
|
||||
#include "dat.h"
|
||||
#include "dat_wire.h"
|
||||
#include "sleep_feed.h"
|
||||
#include "sleep_replication.h"
|
||||
|
||||
#define DAT_SESSION_MAX_PEERS 64u
|
||||
#define DAT_SESSION_CAP_METADATA_ONLY (UINT64_C(1) << 63)
|
||||
|
||||
enum dat_session_state {
|
||||
DAT_SESSION_CLOSED = 0,
|
||||
DAT_SESSION_OPEN = 1,
|
||||
DAT_SESSION_DISCOVERING = 2,
|
||||
DAT_SESSION_ACTIVE = 3,
|
||||
DAT_SESSION_CLOSING = 4,
|
||||
DAT_SESSION_FAILED = 5
|
||||
};
|
||||
|
||||
enum dat_session_peer_state {
|
||||
DAT_SESSION_PEER_DISCOVERED = 1,
|
||||
DAT_SESSION_PEER_SECURE = 2,
|
||||
DAT_SESSION_PEER_READY = 3,
|
||||
DAT_SESSION_PEER_CLOSED = 4
|
||||
};
|
||||
|
||||
enum dat_session_feed {
|
||||
DAT_SESSION_METADATA = 0,
|
||||
DAT_SESSION_CONTENT = 1,
|
||||
DAT_SESSION_FEED_COUNT = 2
|
||||
};
|
||||
|
||||
typedef struct dat_session_peer {
|
||||
uint8_t peer_id[DAT_PEER_ID_BYTES];
|
||||
uint8_t tx_nonce[24];
|
||||
uint8_t rx_nonce[24];
|
||||
uint8_t *rx_buffer;
|
||||
size_t rx_buffer_len;
|
||||
size_t rx_buffer_capacity;
|
||||
uint64_t tx_offset;
|
||||
uint64_t rx_offset;
|
||||
uint8_t remote_feed_mask;
|
||||
uint8_t local_feed_mask;
|
||||
uint8_t remote_content_discovery[32];
|
||||
int remote_content_pending;
|
||||
dat_peer_t address;
|
||||
uint64_t capabilities;
|
||||
uint64_t payload_bytes_sent;
|
||||
uint64_t payload_bytes_received;
|
||||
int state;
|
||||
int transport_authenticated;
|
||||
int paused;
|
||||
int tx_stream_encrypted;
|
||||
int rx_stream_encrypted;
|
||||
sleep_replication_t replication[DAT_SESSION_FEED_COUNT];
|
||||
} dat_session_peer_t;
|
||||
|
||||
struct dat_session;
|
||||
typedef int (*dat_session_send_fn)(void *context,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
const uint8_t *frame, size_t frame_len);
|
||||
typedef int (*dat_session_request_fn)(void *context,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
enum dat_session_feed feed,
|
||||
const dat_request_t *request);
|
||||
/* Called when discovery yields a peer; establish an authenticated transport,
|
||||
* then report success with dat_session_peer_secure(). */
|
||||
typedef void (*dat_session_peer_fn)(void *context,
|
||||
const dat_session_peer_t *peer);
|
||||
typedef int (*dat_session_poll_fn)(void *context, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Session protocol state is separate from connection establishment. In the
|
||||
* Cesspool path, call peer_secure only after the authenticated transport has
|
||||
* bound this peer ID to the topic. In legacy Hypercore V6 mode, call it once
|
||||
* the discovered TCP/uTP stream is connected; V6 performs its feed-key stream
|
||||
* encryption inside the protocol itself.
|
||||
*/
|
||||
typedef struct dat_session {
|
||||
int state;
|
||||
int announcing;
|
||||
int looking_up;
|
||||
uint16_t port;
|
||||
uint8_t archive_key[DAT_KEY_BYTES];
|
||||
uint8_t discovery_topic[CESSPOOL_TOPIC_BYTES];
|
||||
uint8_t peer_id[DAT_PEER_ID_BYTES];
|
||||
uint8_t feed_keys[DAT_SESSION_FEED_COUNT][DAT_KEY_BYTES];
|
||||
int content_key_known;
|
||||
cesspool_t *cesspool;
|
||||
sleep_feed_t *feeds[DAT_SESSION_FEED_COUNT];
|
||||
dat_session_peer_t peers[DAT_SESSION_MAX_PEERS];
|
||||
size_t peer_count;
|
||||
dat_session_send_fn send;
|
||||
dat_session_request_fn on_request;
|
||||
dat_session_peer_fn on_peer;
|
||||
void *peer_context;
|
||||
dat_session_poll_fn poll_transport;
|
||||
void *poll_context;
|
||||
void *context;
|
||||
} dat_session_t;
|
||||
|
||||
int dat_session_init(dat_session_t *session, cesspool_t *cesspool,
|
||||
const uint8_t archive_key[DAT_KEY_BYTES],
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES], uint16_t port,
|
||||
sleep_feed_t *metadata_feed,
|
||||
const uint8_t metadata_key[DAT_KEY_BYTES],
|
||||
sleep_feed_t *content_feed,
|
||||
const uint8_t content_key[DAT_KEY_BYTES],
|
||||
dat_session_send_fn send, void *context);
|
||||
int dat_session_start(dat_session_t *session, int announce, int lookup);
|
||||
int dat_session_set_peer_callback(dat_session_t *session,
|
||||
dat_session_peer_fn callback, void *context);
|
||||
int dat_session_set_transport_poll(dat_session_t *session,
|
||||
dat_session_poll_fn poll, void *context);
|
||||
int dat_session_peer_discovered(dat_session_t *session,
|
||||
const cesspool_peer_t *discovered_peer);
|
||||
int dat_session_peer_secure(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
int transport_authenticated);
|
||||
/* For a metadata-only archive import, install the content key learned from
|
||||
* the verified metadata header and announce that feed to connected peers. */
|
||||
int dat_session_set_content_key(dat_session_t *session,
|
||||
const uint8_t content_key[DAT_KEY_BYTES]);
|
||||
int dat_session_expect_content_key(dat_session_t *session);
|
||||
int dat_session_peer_down(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES]);
|
||||
/* Pause this client's incoming block requests without dropping the peer. */
|
||||
int dat_session_peer_pause(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES], int paused);
|
||||
/* Accept arbitrary transport chunks. Partial and coalesced wire frames are buffered. */
|
||||
int dat_session_receive(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
const uint8_t *frame, size_t frame_len);
|
||||
int dat_session_request_block(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
enum dat_session_feed feed, uint64_t index,
|
||||
uint64_t bytes);
|
||||
int dat_session_send_block(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
enum dat_session_feed feed, uint64_t index);
|
||||
int dat_session_tick(dat_session_t *session, uint64_t now_ms);
|
||||
int dat_session_poll(dat_session_t *session, int timeout_ms, uint64_t now_ms);
|
||||
int dat_session_close(dat_session_t *session);
|
||||
dat_session_peer_t *dat_session_find_peer(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES]);
|
||||
|
||||
#endif /* DAT_SESSION_H */
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef DAT_SOCKET_H
|
||||
#define DAT_SOCKET_H
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
typedef SOCKET dat_socket_t;
|
||||
#define DAT_INVALID_SOCKET INVALID_SOCKET
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
typedef int dat_socket_t;
|
||||
#define DAT_INVALID_SOCKET (-1)
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
#ifndef DAT_WIRE_H
|
||||
#define DAT_WIRE_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum dat_message_type {
|
||||
DAT_MSG_FEED = 0,
|
||||
DAT_MSG_HANDSHAKE = 1,
|
||||
DAT_MSG_INFO = 2,
|
||||
DAT_MSG_HAVE = 3,
|
||||
DAT_MSG_UNHAVE = 4,
|
||||
DAT_MSG_WANT = 5,
|
||||
DAT_MSG_UNWANT = 6,
|
||||
DAT_MSG_REQUEST = 7,
|
||||
DAT_MSG_CANCEL = 8,
|
||||
DAT_MSG_DATA = 9,
|
||||
DAT_MSG_EXTENSION = 15
|
||||
};
|
||||
|
||||
typedef struct dat_feed_open {
|
||||
uint8_t discovery_key[32];
|
||||
uint8_t nonce[24];
|
||||
int has_nonce;
|
||||
} dat_feed_open_t;
|
||||
|
||||
typedef struct dat_wire_message {
|
||||
uint64_t channel;
|
||||
uint64_t type;
|
||||
const uint8_t *payload;
|
||||
size_t payload_len;
|
||||
} dat_wire_message_t;
|
||||
|
||||
#define DAT_PROTOCOL_BYTES 32u
|
||||
#define DAT_PEER_ID_BYTES 32u
|
||||
#ifndef DAT_NETWORK_PROTOCOL
|
||||
#define DAT_NETWORK_PROTOCOL "cesspool"
|
||||
#endif
|
||||
/*
|
||||
* The exact protocol token lives in the repository-root NETWORK_PROTOCOL
|
||||
* file. The Cesspool handshake carries it in `userData`; legacy Hypercore V6
|
||||
* omits that application-owned field and uses the token to select its
|
||||
* discovery namespace and wire mode. This checkout defaults to `cesspool`.
|
||||
*/
|
||||
typedef struct dat_handshake {
|
||||
uint8_t protocol[DAT_PROTOCOL_BYTES];
|
||||
uint8_t peer_id[DAT_PEER_ID_BYTES];
|
||||
uint64_t capabilities;
|
||||
int has_peer_id;
|
||||
} dat_handshake_t;
|
||||
int dat_handshake_init(dat_handshake_t *handshake,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
uint64_t capabilities);
|
||||
int dat_handshake_protocol_matches(const dat_handshake_t *handshake);
|
||||
typedef struct dat_range { uint64_t start; uint64_t length; } dat_range_t;
|
||||
typedef struct dat_info { uint8_t uploading; uint8_t downloading; } dat_info_t;
|
||||
typedef struct dat_request { uint64_t index; uint64_t bytes; uint8_t want_hash; uint8_t want_nodes; } dat_request_t;
|
||||
#define DAT_DATA_MAX_NODES 128u
|
||||
typedef struct dat_data_node {
|
||||
uint64_t index;
|
||||
uint64_t length;
|
||||
uint8_t hash[32];
|
||||
} dat_data_node_t;
|
||||
typedef struct dat_data {
|
||||
uint64_t index;
|
||||
const uint8_t *value;
|
||||
size_t value_len;
|
||||
const uint8_t *proof;
|
||||
size_t proof_len;
|
||||
dat_data_node_t nodes[DAT_DATA_MAX_NODES];
|
||||
size_t node_count;
|
||||
uint8_t signature[64];
|
||||
int has_signature;
|
||||
} dat_data_t;
|
||||
typedef struct dat_cancel { uint64_t index; } dat_cancel_t;
|
||||
typedef struct dat_extension {
|
||||
uint64_t id;
|
||||
const uint8_t *message;
|
||||
size_t message_len;
|
||||
} dat_extension_t;
|
||||
|
||||
/* Hypercore frames are length-prefixed; headers pack channel and type. */
|
||||
int dat_wire_encode(const dat_wire_message_t *message, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int dat_wire_decode(dat_wire_message_t *message, const uint8_t *input,
|
||||
size_t input_len, size_t *consumed);
|
||||
int dat_handshake_encode(const dat_handshake_t *handshake, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int dat_handshake_decode(dat_handshake_t *handshake, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int dat_feed_open_encode(const dat_feed_open_t *feed, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int dat_feed_open_decode(dat_feed_open_t *feed, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int dat_range_encode(const dat_range_t *range, uint8_t *out, size_t capacity, size_t *written);
|
||||
int dat_range_decode(dat_range_t *range, const uint8_t *input, size_t input_len);
|
||||
/* WANT omits length to mean through the current end of a non-live feed. */
|
||||
int dat_want_decode(dat_range_t *range, const uint8_t *input, size_t input_len,
|
||||
uint64_t feed_length);
|
||||
int dat_info_encode(const dat_info_t *info, uint8_t *out, size_t capacity,
|
||||
size_t *written);
|
||||
int dat_info_decode(dat_info_t *info, const uint8_t *input, size_t input_len);
|
||||
int dat_request_encode(const dat_request_t *request, uint8_t *out, size_t capacity, size_t *written);
|
||||
int dat_request_decode(dat_request_t *request, const uint8_t *input, size_t input_len);
|
||||
int dat_data_encode(const dat_data_t *data, uint8_t *out, size_t capacity, size_t *written);
|
||||
int dat_data_decode(dat_data_t *data, const uint8_t *input, size_t input_len);
|
||||
int dat_cancel_encode(const dat_cancel_t *cancel, uint8_t *out, size_t capacity, size_t *written);
|
||||
int dat_cancel_decode(dat_cancel_t *cancel, const uint8_t *input, size_t input_len);
|
||||
int dat_extension_encode(const dat_extension_t *extension, uint8_t *out, size_t capacity, size_t *written);
|
||||
int dat_extension_decode(dat_extension_t *extension, const uint8_t *input, size_t input_len);
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef DATGPT_ERRNO_COMPAT_H
|
||||
#define DATGPT_ERRNO_COMPAT_H
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
/* EKEYREJECTED is a Linux extension. Keep the key-authentication failure
|
||||
* portable while preserving a useful access-denied error on other hosts. */
|
||||
#ifndef EKEYREJECTED
|
||||
#define EKEYREJECTED EACCES
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef KEYS_H
|
||||
#define KEYS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define SLEEP_PUB_KEY_BYTES 32
|
||||
#define SLEEP_SECRET_KEY_BYTES 64
|
||||
#define SLEEP_SIGNATURE_BYTES 64
|
||||
#define SLEEP_BLAKE2B_BYTES 32
|
||||
|
||||
typedef struct {
|
||||
uint8_t pubkey[SLEEP_PUB_KEY_BYTES];
|
||||
uint8_t seckey[SLEEP_SECRET_KEY_BYTES];
|
||||
} sleep_keypair_t;
|
||||
|
||||
|
||||
#endif /* KEYS_H */
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,127 @@
|
||||
// ॐ सूक्ष्म शरीर
|
||||
#ifndef SLEEP_H
|
||||
#define SLEEP_H
|
||||
// TODO implement SLEEP
|
||||
/*metadata.key
|
||||
metadata.signatures
|
||||
metadata.bitfield
|
||||
metadata.tree
|
||||
metadata.data
|
||||
content.key
|
||||
content.signatures
|
||||
content.bitfield
|
||||
content.tree*/
|
||||
#include "keys.h"
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
// test
|
||||
#include <assert.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int sleep_init(const char *root);
|
||||
|
||||
#define SLEEP_HEADER_SIZE 32u
|
||||
|
||||
#define SLEEP_MAGIC_BITFIELD 0x05025700u
|
||||
#define SLEEP_MAGIC_SIGNATURE 0x05025701u
|
||||
#define SLEEP_MAGIC_TREE 0x05025702u
|
||||
|
||||
// TODO MAGIC_BITFIELD, MAGIC_SIG, MAGIC_TREE
|
||||
|
||||
#define SLEEP_ALGO_BLAKE2B "BLAKE2b"
|
||||
#define SLEEP_ALGO_ED25519 "Ed25519"
|
||||
|
||||
#define SLEEP_TREE_ENTRY_SIZE 40u
|
||||
#define SLEEP_SIG_ENTRY_SIZE 64u
|
||||
#define SLEEP_BITFIELD_ENTRY_SIZE 3584u
|
||||
|
||||
enum sleep_hash_type {
|
||||
SLEEP_HASH_LEAF = 0,
|
||||
SLEEP_HASH_PARENT = 1,
|
||||
SLEEP_HASH_ROOT = 2
|
||||
};
|
||||
|
||||
// Wire
|
||||
#if defined(__GNUC__) || defined(__clang__) || defined(__MUSL__)
|
||||
#define SLEEP_PACKED __attribute__((packed))
|
||||
#else
|
||||
#define SLEEP_PACKED
|
||||
#pragma pack(push,1) // pragma deez
|
||||
#endif
|
||||
|
||||
|
||||
typedef struct SLEEP_PACKED sleep_header {
|
||||
uint32_t magic_be;
|
||||
uint8_t version;
|
||||
uint16_t entry_size_be;
|
||||
uint8_t algo_len;
|
||||
char algo[24];
|
||||
} sleep_header_t;
|
||||
// TODO assert must be 32 bytes
|
||||
|
||||
// 32 byte hash + 8 byte BE len
|
||||
typedef struct SLEEP_PACKED sleep_tree_entry {
|
||||
uint8_t hash[32];
|
||||
uint64_t length_be;
|
||||
} sleep_tree_entry_t;
|
||||
// TODO assert
|
||||
|
||||
// 64 bytes
|
||||
typedef struct SLEEP_PACKED sleep_sig_entry {
|
||||
uint8_t sig[64];
|
||||
} sleep_sig_entry_t;
|
||||
// TODO assert
|
||||
|
||||
// 3584-byte historical record. The final 256 bytes are opaque for now.
|
||||
typedef struct SLEEP_PACKED sleep_bitfield_entry {
|
||||
uint8_t data[1024];
|
||||
uint8_t tree[2048];
|
||||
uint8_t index[256];
|
||||
uint8_t reserved[256];
|
||||
} sleep_bitfield_entry_t;
|
||||
// TODO DOTO ODOT assert it
|
||||
|
||||
#if !defined(__GNUC__) && !defined(__clang__)
|
||||
#pragma pack(pop)
|
||||
#endif
|
||||
|
||||
// TODO BE INT into and out of buffers **
|
||||
|
||||
// crypto helpers
|
||||
#ifdef SLEEP_SALT
|
||||
int sleep_gen_ed25519(uint8_t pubkey[32], uint8_t seckey[64]);
|
||||
// sign
|
||||
int sleep_sign_ed25519(const uint8_t *msg, size_t msglen,
|
||||
const uint8_t seckey[64], uint8_t sig_out[64]);
|
||||
// verify
|
||||
int sleep_verify_ed25519(const uint8_t *msg, size_t msglen,
|
||||
const uint8_t pubkey[32], const uint8_t sig[64]);
|
||||
// discovery
|
||||
int sleep_compute_discovery_key(const uint8_t pubkey[32], uint8_t out32[32]);
|
||||
#endif /* SLEEP_SALT */
|
||||
|
||||
// function decs
|
||||
uint64_t sleep_tree_index(uint64_t depth, uint64_t offset);
|
||||
uint64_t sleep_tree_depth(uint64_t index);
|
||||
uint64_t sleep_tree_offset(uint64_t index, uint64_t depth);
|
||||
uint64_t sleep_tree_parent(uint64_t index);
|
||||
int sleep_tree_left_child(uint64_t index, uint64_t *out);
|
||||
int sleep_tree_right_child(uint64_t index, uint64_t *out);
|
||||
int sleep_tree_sibling(uint64_t index, uint64_t *out);
|
||||
uint64_t sleep_tree_left_span(uint64_t index);
|
||||
uint64_t sleep_tree_right_span(uint64_t index);
|
||||
void sleep_tree_spans(uint64_t index, uint64_t *left, uint64_t *right);
|
||||
size_t sleep_tree_count(uint64_t index);
|
||||
size_t sleep_tree_count_leaves(uint64_t index);
|
||||
size_t sleep_tree_full_roots(uint64_t index, uint64_t *out, size_t capacity);
|
||||
int sleep_tree_hash_block(const uint8_t *data, size_t len, uint8_t out_hash[32]);
|
||||
int sleep_tree_hash_parent(const uint8_t left[32], const uint8_t right[32],
|
||||
uint64_t length, uint8_t out_hash[32]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* SLEEP_H */
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef SLEEP_BITFIELD_H
|
||||
#define SLEEP_BITFIELD_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SLEEP_BITFIELD_DATA_BYTES 1024u
|
||||
#define SLEEP_BITFIELD_TREE_BYTES 2048u
|
||||
#define SLEEP_BITFIELD_INDEX_BYTES 256u
|
||||
#define SLEEP_BITFIELD_RESERVED_BYTES 256u
|
||||
|
||||
typedef struct sleep_bitfield {
|
||||
uint8_t data[SLEEP_BITFIELD_DATA_BYTES];
|
||||
uint8_t tree[SLEEP_BITFIELD_TREE_BYTES];
|
||||
uint8_t index[SLEEP_BITFIELD_INDEX_BYTES];
|
||||
uint8_t reserved[SLEEP_BITFIELD_RESERVED_BYTES];
|
||||
} sleep_bitfield_t;
|
||||
|
||||
void sleep_bitfield_clear(sleep_bitfield_t *field);
|
||||
int sleep_bitfield_get_data(const sleep_bitfield_t *field, size_t bit);
|
||||
int sleep_bitfield_get_tree(const sleep_bitfield_t *field, size_t bit);
|
||||
int sleep_bitfield_set_data(sleep_bitfield_t *field, size_t bit, int present);
|
||||
int sleep_bitfield_set_tree(sleep_bitfield_t *field, size_t bit, int present);
|
||||
void sleep_bitfield_reindex(sleep_bitfield_t *field);
|
||||
size_t sleep_bitfield_next_data(const sleep_bitfield_t *field, size_t start, int present);
|
||||
int sleep_bitfield_encode(const sleep_bitfield_t *field, uint8_t *out, size_t capacity);
|
||||
int sleep_bitfield_decode(sleep_bitfield_t *field, const uint8_t *input, size_t length);
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef SLEEP_CRYPTO_H
|
||||
#define SLEEP_CRYPTO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "keys.h"
|
||||
#define SLEEP_ROOT_RECORD_BYTES 48u
|
||||
|
||||
int sleep_crypto_init(void);
|
||||
int sleep_stream_xsalsa20_xor(uint8_t *out, const uint8_t *in, size_t length,
|
||||
const uint8_t nonce[24], const uint8_t key[32],
|
||||
uint64_t stream_offset);
|
||||
int sleep_gen_keypair(sleep_keypair_t *kp);
|
||||
int sleep_save_pubkey(const char *path, const uint8_t pubkey[SLEEP_PUB_KEY_BYTES]);
|
||||
int sleep_load_pubkey(const char *path, uint8_t pubkey[SLEEP_PUB_KEY_BYTES]);
|
||||
int sleep_save_keypair(const char *path, const sleep_keypair_t *keypair);
|
||||
int sleep_load_keypair(const char *path, sleep_keypair_t *keypair);
|
||||
|
||||
int sleep_blake2b(uint8_t out[SLEEP_BLAKE2B_BYTES], const uint8_t *in, size_t inlen);
|
||||
|
||||
int sleep_sign(uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *msg, size_t msglen, const sleep_keypair_t *kp);
|
||||
|
||||
int sleep_verify(const uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *msg, size_t msglen, const uint8_t pubkey[SLEEP_PUB_KEY_BYTES]);
|
||||
|
||||
int sleep_sign_roots(uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *roots, size_t nroots, const uint64_t *indices, const uint64_t *lengths, const sleep_keypair_t *kp);
|
||||
|
||||
int sleep_verify_roots(const uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *roots, size_t nroots, const uint64_t *indices, const uint64_t *lengths, const uint8_t pub[SLEEP_PUB_KEY_BYTES]);
|
||||
|
||||
#endif /* SLEEP_CRYPTO_H */
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef SLEEP_FEED_H
|
||||
#define SLEEP_FEED_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "keys.h"
|
||||
|
||||
typedef struct sleep_feed {
|
||||
char directory[1024];
|
||||
char name[64];
|
||||
uint8_t **blocks;
|
||||
size_t *lengths;
|
||||
uint8_t **proofs;
|
||||
size_t *proof_lengths;
|
||||
size_t length;
|
||||
size_t capacity;
|
||||
sleep_keypair_t keypair;
|
||||
int writable;
|
||||
int dirty;
|
||||
int remote_only;
|
||||
int persist_remote;
|
||||
} sleep_feed_t;
|
||||
|
||||
int sleep_feed_open(sleep_feed_t *feed, const char *directory, const char *name,
|
||||
const sleep_keypair_t *keypair);
|
||||
/* Opens an in-memory, read-only feed replica using only its public key. */
|
||||
int sleep_feed_open_remote(sleep_feed_t *feed, const char *directory,
|
||||
const char *name, const uint8_t public_key[32]);
|
||||
/* Merge verified cached remote blocks and persist subsequent received blocks. */
|
||||
int sleep_feed_load_remote(sleep_feed_t *feed, const uint8_t public_key[32]);
|
||||
size_t sleep_feed_length(const sleep_feed_t *feed);
|
||||
int sleep_feed_read(const sleep_feed_t *feed, size_t index, uint8_t **data, size_t *length);
|
||||
int sleep_feed_append(sleep_feed_t *feed, const uint8_t *data, size_t length);
|
||||
/* Keeps already verified remote blocks and proofs in this session's memory. */
|
||||
int sleep_feed_append_remote(sleep_feed_t *feed, size_t index,
|
||||
const uint8_t *data, size_t length,
|
||||
const uint8_t *proof, size_t proof_length);
|
||||
int sleep_feed_truncate(sleep_feed_t *feed, size_t length);
|
||||
int sleep_feed_has(const sleep_feed_t *feed, size_t index);
|
||||
int sleep_feed_byte_offset(const sleep_feed_t *feed, size_t index,
|
||||
uint64_t *offset);
|
||||
int sleep_feed_flush(sleep_feed_t *feed);
|
||||
void sleep_feed_close(sleep_feed_t *feed);
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef SLEEP_PROOF_H
|
||||
#define SLEEP_PROOF_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "sleep_feed.h"
|
||||
|
||||
#define SLEEP_PROOF_HASH_BYTES 32u
|
||||
#define SLEEP_PROOF_MAX_NODES 64u
|
||||
typedef struct sleep_proof_node {
|
||||
uint64_t index;
|
||||
uint64_t length;
|
||||
uint8_t hash[SLEEP_PROOF_HASH_BYTES];
|
||||
} sleep_proof_node_t;
|
||||
typedef struct sleep_proof {
|
||||
uint64_t block;
|
||||
uint64_t block_length;
|
||||
uint8_t block_hash[SLEEP_PROOF_HASH_BYTES];
|
||||
sleep_proof_node_t nodes[SLEEP_PROOF_MAX_NODES];
|
||||
size_t node_count;
|
||||
sleep_proof_node_t roots[SLEEP_PROOF_MAX_NODES];
|
||||
size_t root_count;
|
||||
uint8_t signature[64];
|
||||
int has_signature;
|
||||
} sleep_proof_t;
|
||||
|
||||
void sleep_proof_init(sleep_proof_t *proof, uint64_t block);
|
||||
int sleep_proof_add_node(sleep_proof_t *proof, uint64_t index,
|
||||
uint64_t length, const uint8_t hash[32]);
|
||||
int sleep_proof_add_root(sleep_proof_t *proof, uint64_t index,
|
||||
uint64_t length, const uint8_t hash[32]);
|
||||
int sleep_proof_create(const sleep_feed_t *feed, uint64_t block,
|
||||
sleep_proof_t *proof);
|
||||
int sleep_proof_from_nodes(uint64_t block, const uint8_t *value,
|
||||
size_t value_len, const sleep_proof_node_t *nodes,
|
||||
size_t node_count, const uint8_t signature[64],
|
||||
uint64_t feed_length, sleep_proof_t *proof);
|
||||
int sleep_proof_encode(const sleep_proof_t *proof, uint8_t *out,
|
||||
size_t capacity, size_t *written);
|
||||
int sleep_proof_decode(sleep_proof_t *proof, const uint8_t *input,
|
||||
size_t input_len);
|
||||
int sleep_proof_verify(const sleep_proof_t *proof, const uint8_t *block,
|
||||
size_t block_len, const uint8_t public_key[32]);
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SLEEP_PROTO_H
|
||||
#define SLEEP_PROTO_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#define SLEEP_PROTO_KEY_BYTES 32u
|
||||
typedef struct sleep_metadata_header { char type[32]; uint8_t content[32]; size_t content_len; } sleep_metadata_header_t;
|
||||
enum sleep_entry_type { SLEEP_ENTRY_FILE=1, SLEEP_ENTRY_DIRECTORY=2, SLEEP_ENTRY_SYMLINK=3, SLEEP_ENTRY_DELETE=4 };
|
||||
typedef struct sleep_metadata_entry {
|
||||
char path[1024];
|
||||
uint32_t type;
|
||||
uint32_t mode;
|
||||
uint64_t size;
|
||||
uint64_t mtime;
|
||||
uint64_t block_offset;
|
||||
uint64_t block_count;
|
||||
char linkname[1024];
|
||||
} sleep_metadata_entry_t;
|
||||
size_t sleep_varint_size(uint64_t value);
|
||||
int sleep_varint_encode(uint64_t value, uint8_t *out, size_t capacity, size_t *written);
|
||||
int sleep_varint_decode(const uint8_t *input, size_t input_len, uint64_t *value, size_t *consumed);
|
||||
int sleep_metadata_header_encode(const sleep_metadata_header_t *header, uint8_t *out, size_t capacity, size_t *written);
|
||||
int sleep_metadata_header_decode(sleep_metadata_header_t *header, const uint8_t *input, size_t input_len);
|
||||
int sleep_write_metadata_header(const char *path, const uint8_t content_pubkey[32]);
|
||||
int sleep_metadata_entry_encode(const sleep_metadata_entry_t *entry, uint8_t *out, size_t capacity, size_t *written);
|
||||
int sleep_metadata_entry_decode(sleep_metadata_entry_t *entry, const uint8_t *input, size_t input_len);
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef SLEEP_REPLICATION_H
|
||||
#define SLEEP_REPLICATION_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "sleep_feed.h"
|
||||
#include "sleep_bitfield.h"
|
||||
#include "sleep_proof.h"
|
||||
|
||||
#define SLEEP_REPLICATION_QUEUE 128u
|
||||
typedef struct sleep_replication_request { uint64_t index; uint64_t bytes; int hash; int nodes; } sleep_replication_request_t;
|
||||
typedef struct sleep_replication {
|
||||
sleep_feed_t *feed;
|
||||
uint64_t channel;
|
||||
uint64_t remote_length;
|
||||
sleep_bitfield_t remote_bitfield;
|
||||
sleep_replication_request_t requests[SLEEP_REPLICATION_QUEUE];
|
||||
size_t request_head, request_count;
|
||||
int live, uploading, downloading;
|
||||
} sleep_replication_t;
|
||||
|
||||
int sleep_replication_init(sleep_replication_t *replication,
|
||||
sleep_feed_t *feed, uint64_t channel);
|
||||
int sleep_replication_have(sleep_replication_t *replication,
|
||||
uint64_t start, uint64_t length);
|
||||
int sleep_replication_unhave(sleep_replication_t *replication,
|
||||
uint64_t start, uint64_t length);
|
||||
int sleep_replication_want(sleep_replication_t *replication,
|
||||
uint64_t start, uint64_t length);
|
||||
int sleep_replication_request(sleep_replication_t *replication,
|
||||
const sleep_replication_request_t *request);
|
||||
int sleep_replication_next(sleep_replication_t *replication,
|
||||
sleep_replication_request_t *request);
|
||||
int sleep_replication_complete(sleep_replication_t *replication,
|
||||
uint64_t index);
|
||||
int sleep_replication_is_requested(const sleep_replication_t *replication,
|
||||
uint64_t index);
|
||||
int sleep_replication_unwant(sleep_replication_t *replication,
|
||||
uint64_t start, uint64_t length);
|
||||
void sleep_replication_clear_requests(sleep_replication_t *replication);
|
||||
int sleep_replication_receive(sleep_replication_t *replication,
|
||||
const uint8_t *block, size_t block_len,
|
||||
const sleep_proof_t *proof,
|
||||
const uint8_t feed_public_key[32]);
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef SLEEP_UTP_H
|
||||
#define SLEEP_UTP_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "dat_socket.h"
|
||||
|
||||
#define SLEEP_UTP_MTU 1200u
|
||||
#define SLEEP_UTP_WINDOW 64u
|
||||
enum sleep_utp_state { SLEEP_UTP_CLOSED, SLEEP_UTP_SYN_SENT, SLEEP_UTP_SYN_RECV, SLEEP_UTP_CONNECTED, SLEEP_UTP_FIN_SENT, SLEEP_UTP_RESET };
|
||||
enum sleep_utp_packet_type { SLEEP_UTP_DATA, SLEEP_UTP_FIN, SLEEP_UTP_STATE, SLEEP_UTP_RESET_PACKET, SLEEP_UTP_SYN };
|
||||
typedef struct sleep_utp_packet { uint8_t type, version; uint16_t connection_id; uint32_t timestamp, timestamp_diff, window_size; uint16_t sequence, acknowledgement; uint32_t selective_ack; int has_selective_ack; const uint8_t *payload; size_t payload_len; } sleep_utp_packet_t;
|
||||
typedef struct sleep_utp_slot { uint16_t sequence; uint8_t data[SLEEP_UTP_MTU]; size_t length; uint64_t sent_at; unsigned retries; int used; } sleep_utp_slot_t;
|
||||
typedef struct sleep_utp {
|
||||
dat_socket_t socket;
|
||||
struct sockaddr_storage remote;
|
||||
socklen_t remote_len;
|
||||
uint16_t send_id, receive_id, sequence, acknowledgement;
|
||||
uint32_t receive_window, send_window;
|
||||
enum sleep_utp_state state;
|
||||
uint64_t retransmit_at;
|
||||
unsigned syn_retries;
|
||||
uint32_t congestion_window, slow_start_threshold;
|
||||
uint32_t round_trip_time, round_trip_variance;
|
||||
size_t bytes_in_flight;
|
||||
sleep_utp_slot_t unacked[SLEEP_UTP_WINDOW];
|
||||
sleep_utp_slot_t reorder[SLEEP_UTP_WINDOW];
|
||||
void *context;
|
||||
void (*on_data)(struct sleep_utp *, const uint8_t *, size_t);
|
||||
} sleep_utp_t;
|
||||
int sleep_utp_init(sleep_utp_t *utp, dat_socket_t udp_socket);
|
||||
int sleep_utp_connect(sleep_utp_t *utp, const struct sockaddr *remote, socklen_t remote_len);
|
||||
int sleep_utp_accept(sleep_utp_t *utp, const sleep_utp_packet_t *syn, const struct sockaddr *remote, socklen_t remote_len);
|
||||
int sleep_utp_packet_encode(const sleep_utp_packet_t *packet, uint8_t *out, size_t capacity, size_t *written);
|
||||
int sleep_utp_packet_decode(sleep_utp_packet_t *packet, const uint8_t *input, size_t input_len);
|
||||
int sleep_utp_input(sleep_utp_t *utp, const uint8_t *packet, size_t packet_len);
|
||||
int sleep_utp_send(sleep_utp_t *utp, const uint8_t *data, size_t length);
|
||||
int sleep_utp_tick(sleep_utp_t *utp, uint64_t now_ms);
|
||||
int sleep_utp_close(sleep_utp_t *utp);
|
||||
#endif
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef SLEEP_VERIFY_H
|
||||
#define SLEEP_VERIFY_H
|
||||
|
||||
int sleep_verify_dat(const char *dat_dir);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef SLEEP_WRITER_H
|
||||
#define SLEEP_WRITER_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "sleep.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int sleep_write_feed(const char *feed_dir,
|
||||
const char *feed_name,
|
||||
const uint8_t *const *blocks,
|
||||
const size_t *lengths,
|
||||
size_t nblocks,
|
||||
const sleep_keypair_t *keypair);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef DATGPT_WIN_COMPAT_H
|
||||
#define DATGPT_WIN_COMPAT_H
|
||||
|
||||
/* Small POSIX spellings used by the shared C sources on native Windows. */
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#include <direct.h>
|
||||
#include <process.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <string.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#ifndef mkdir
|
||||
#define mkdir(path, mode) _mkdir(path)
|
||||
#endif
|
||||
#ifndef fsync
|
||||
#define fsync(fd) _commit(fd)
|
||||
#endif
|
||||
#ifndef getpid
|
||||
#define getpid _getpid
|
||||
#endif
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
#ifndef O_NOFOLLOW
|
||||
#define O_NOFOLLOW 0
|
||||
#endif
|
||||
#ifndef EKEYREJECTED
|
||||
#define EKEYREJECTED EACCES
|
||||
#endif
|
||||
#ifndef S_ISLNK
|
||||
#define S_ISLNK(mode) (((mode) & S_IFMT) == S_IFLNK)
|
||||
#endif
|
||||
#ifndef S_IFLNK
|
||||
#define S_IFLNK 0120000
|
||||
#endif
|
||||
#ifndef fchmod
|
||||
#define fchmod(fd, mode) ((void)(fd), (void)(mode), 0)
|
||||
#endif
|
||||
#ifndef strcasecmp
|
||||
#define strcasecmp _stricmp
|
||||
#endif
|
||||
#ifndef strncasecmp
|
||||
#define strncasecmp _strnicmp
|
||||
#endif
|
||||
static inline char *dat_win_getenv_compat(const char *name)
|
||||
{
|
||||
char *value = getenv(name);
|
||||
if (!value && name && !strcmp(name, "HOME")) value = getenv("USERPROFILE");
|
||||
return value;
|
||||
}
|
||||
#define getenv dat_win_getenv_compat
|
||||
static inline int dat_win_lstat(const char *path, struct stat *info)
|
||||
{
|
||||
DWORD attributes;
|
||||
if (stat(path, info)) return -1;
|
||||
attributes = GetFileAttributesA(path);
|
||||
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT))
|
||||
info->st_mode = (info->st_mode & ~S_IFMT) | S_IFLNK;
|
||||
return 0;
|
||||
}
|
||||
#ifndef lstat
|
||||
#define lstat dat_win_lstat
|
||||
#endif
|
||||
static inline int dat_win_readlink(const char *path, char *target, size_t capacity)
|
||||
{ (void)path; (void)target; (void)capacity; errno = ENOTSUP; return -1; }
|
||||
static inline int dat_win_symlink(const char *target, const char *path)
|
||||
{ (void)target; (void)path; errno = ENOTSUP; return -1; }
|
||||
#ifndef readlink
|
||||
#define readlink dat_win_readlink
|
||||
#endif
|
||||
#ifndef symlink
|
||||
#define symlink dat_win_symlink
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef DATGPT_WIN_DIRENT_H
|
||||
#define DATGPT_WIN_DIRENT_H
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#define NAME_MAX MAX_PATH
|
||||
typedef struct dirent { char d_name[MAX_PATH]; } struct_dirent_t;
|
||||
typedef struct dat_win_dir {
|
||||
HANDLE handle;
|
||||
WIN32_FIND_DATAA data;
|
||||
int first;
|
||||
struct dirent entry;
|
||||
} DIR;
|
||||
static DIR *dat_opendir(const char *path)
|
||||
{
|
||||
DIR *dir = (DIR *)calloc(1, sizeof(*dir));
|
||||
char pattern[MAX_PATH * 2];
|
||||
int n;
|
||||
if (!dir) return NULL;
|
||||
n = snprintf(pattern, sizeof(pattern), "%s\\*", path);
|
||||
if (n < 0 || (size_t)n >= sizeof(pattern)) { free(dir); errno = ENAMETOOLONG; return NULL; }
|
||||
dir->handle = FindFirstFileA(pattern, &dir->data);
|
||||
if (dir->handle == INVALID_HANDLE_VALUE) { free(dir); errno = ENOENT; return NULL; }
|
||||
dir->first = 1;
|
||||
return dir;
|
||||
}
|
||||
static struct dirent *dat_readdir(DIR *dir)
|
||||
{
|
||||
if (!dir) return NULL;
|
||||
if (dir->first) dir->first = 0;
|
||||
else if (!FindNextFileA(dir->handle, &dir->data)) return NULL;
|
||||
strncpy(dir->entry.d_name, dir->data.cFileName, sizeof(dir->entry.d_name) - 1);
|
||||
dir->entry.d_name[sizeof(dir->entry.d_name) - 1] = 0;
|
||||
return &dir->entry;
|
||||
}
|
||||
static int dat_closedir(DIR *dir)
|
||||
{
|
||||
int rc;
|
||||
if (!dir) { errno = EINVAL; return -1; }
|
||||
rc = FindClose(dir->handle) ? 0 : -1;
|
||||
free(dir);
|
||||
return rc;
|
||||
}
|
||||
#define opendir dat_opendir
|
||||
#define readdir dat_readdir
|
||||
#define closedir dat_closedir
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef DATGPT_WIN_FNMATCH_H
|
||||
#define DATGPT_WIN_FNMATCH_H
|
||||
#ifdef _WIN32
|
||||
static int dat_win_fnmatch(const char *pattern, const char *text)
|
||||
{
|
||||
while (*pattern) {
|
||||
if (*pattern == '*') {
|
||||
while (*pattern == '*') pattern++;
|
||||
if (!*pattern) return 0;
|
||||
do { if (dat_win_fnmatch(pattern, text) == 0) return 0; } while (*text++);
|
||||
return 1;
|
||||
}
|
||||
if (!*text) return 1;
|
||||
if (*pattern == '?') { pattern++; text++; continue; }
|
||||
if (*pattern != *text) return 1;
|
||||
pattern++; text++;
|
||||
}
|
||||
return *text ? 1 : 0;
|
||||
}
|
||||
#define fnmatch(pattern, text, flags) ((void)(flags), dat_win_fnmatch((pattern), (text)))
|
||||
#else
|
||||
#include <fnmatch.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Base32768 alphabet and bit-packing ported from qntm/base32768.
|
||||
* Upstream implementation: https://github.com/qntm/base32768
|
||||
* Copyright (c) 2021 qntm. Used under the MIT license; see
|
||||
* third_party/base32768.LICENSE.
|
||||
*/
|
||||
#include "base32768.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct base_range {
|
||||
uint32_t first, last, offset;
|
||||
} base_range_t;
|
||||
|
||||
#define BASE32768_MAX_RANGES 128u
|
||||
static base_range_t repertoire[2][BASE32768_MAX_RANGES];
|
||||
static size_t repertoire_count[2];
|
||||
static uint32_t repertoire_size[2];
|
||||
static int repertoire_ready;
|
||||
static int repertoire_error;
|
||||
|
||||
/* The pairs encode inclusive ranges. Keep in sync with upstream's pairStrings. */
|
||||
static const char *const pair_strings[2] = {
|
||||
"ҠҿԀԟڀڿݠޟ߀ߟကဟႠႿᄀᅟᆀᆟᇠሿበቿዠዿጠጿᎠᏟᐠᙟᚠᛟកសᠠᡟᣀᣟᦀᦟ᧠᧿ᨠᨿᯀᯟᰀᰟᴀᴟ⇠⇿⋀⋟⍀⏟␀␟─❟➀➿⠀⥿⦠⦿⨠⩟⪀⪿⫠⭟ⰀⰟⲀⳟⴀⴟⵀⵟ⺠⻟㇀㇟㐀䶟䷀龿ꀀꑿ꒠꒿ꔀꗿꙀꙟꚠꛟ꜀ꝟꞀꞟꡀꡟ",
|
||||
"ƀƟɀʟ"
|
||||
};
|
||||
|
||||
static int utf8_next(const unsigned char *s, size_t length, size_t *offset,
|
||||
uint32_t *codepoint)
|
||||
{
|
||||
unsigned char a, b, c, d;
|
||||
if (*offset >= length) return -1;
|
||||
a = s[(*offset)++];
|
||||
if (a < 0x80) { *codepoint = a; return 0; }
|
||||
if (a >= 0xc2 && a <= 0xdf) {
|
||||
if (*offset >= length) return -1;
|
||||
b = s[(*offset)++];
|
||||
if ((b & 0xc0) != 0x80) return -1;
|
||||
*codepoint = ((uint32_t)(a & 0x1f) << 6) | (b & 0x3f);
|
||||
return 0;
|
||||
}
|
||||
if (a >= 0xe0 && a <= 0xef) {
|
||||
if (length - *offset < 2) return -1;
|
||||
b = s[(*offset)++]; c = s[(*offset)++];
|
||||
if ((b & 0xc0) != 0x80 || (c & 0xc0) != 0x80 ||
|
||||
(a == 0xe0 && b < 0xa0) || (a == 0xed && b >= 0xa0)) return -1;
|
||||
*codepoint = ((uint32_t)(a & 0x0f) << 12) |
|
||||
((uint32_t)(b & 0x3f) << 6) | (c & 0x3f);
|
||||
return 0;
|
||||
}
|
||||
if (a >= 0xf0 && a <= 0xf4) {
|
||||
if (length - *offset < 3) return -1;
|
||||
b = s[(*offset)++]; c = s[(*offset)++]; d = s[(*offset)++];
|
||||
if ((b & 0xc0) != 0x80 || (c & 0xc0) != 0x80 || (d & 0xc0) != 0x80 ||
|
||||
(a == 0xf0 && b < 0x90) || (a == 0xf4 && b >= 0x90)) return -1;
|
||||
*codepoint = ((uint32_t)(a & 7) << 18) | ((uint32_t)(b & 0x3f) << 12) |
|
||||
((uint32_t)(c & 0x3f) << 6) | (d & 0x3f);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static size_t utf8_put(uint32_t cp, char *out)
|
||||
{
|
||||
if (cp < 0x80) { out[0] = (char)cp; return 1; }
|
||||
if (cp < 0x800) {
|
||||
out[0] = (char)(0xc0 | (cp >> 6));
|
||||
out[1] = (char)(0x80 | (cp & 0x3f)); return 2;
|
||||
}
|
||||
if (cp < 0x10000) {
|
||||
out[0] = (char)(0xe0 | (cp >> 12));
|
||||
out[1] = (char)(0x80 | ((cp >> 6) & 0x3f));
|
||||
out[2] = (char)(0x80 | (cp & 0x3f)); return 3;
|
||||
}
|
||||
out[0] = (char)(0xf0 | (cp >> 18));
|
||||
out[1] = (char)(0x80 | ((cp >> 12) & 0x3f));
|
||||
out[2] = (char)(0x80 | ((cp >> 6) & 0x3f));
|
||||
out[3] = (char)(0x80 | (cp & 0x3f)); return 4;
|
||||
}
|
||||
|
||||
static int initialize_repertoire(void)
|
||||
{
|
||||
size_t row;
|
||||
if (repertoire_ready) return repertoire_error ? -1 : 0;
|
||||
repertoire_ready = 1;
|
||||
for (row = 0; row < 2; row++) {
|
||||
const char *text = pair_strings[row];
|
||||
size_t length = strlen(text), offset = 0;
|
||||
uint32_t points[BASE32768_MAX_RANGES * 2u];
|
||||
size_t point_count = 0, i;
|
||||
while (offset < length) {
|
||||
if (point_count >= sizeof(points) / sizeof(points[0]) ||
|
||||
utf8_next((const unsigned char *)text, length, &offset,
|
||||
&points[point_count])) {
|
||||
repertoire_error = 1; return -1;
|
||||
}
|
||||
point_count++;
|
||||
}
|
||||
if (point_count % 2 || point_count / 2 > BASE32768_MAX_RANGES) {
|
||||
repertoire_error = 1; return -1;
|
||||
}
|
||||
for (i = 0; i < point_count; i += 2) {
|
||||
base_range_t *range = &repertoire[row][repertoire_count[row]++];
|
||||
if (points[i] > points[i + 1]) { repertoire_error = 1; return -1; }
|
||||
range->first = points[i]; range->last = points[i + 1];
|
||||
range->offset = repertoire_size[row];
|
||||
repertoire_size[row] += points[i + 1] - points[i] + 1;
|
||||
}
|
||||
}
|
||||
if (repertoire_size[0] != 32768u || repertoire_size[1] != 128u) {
|
||||
repertoire_error = 1; return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int index_to_codepoint(unsigned bits, uint32_t index, uint32_t *out)
|
||||
{
|
||||
size_t row = bits == 15 ? 0 : 1, i;
|
||||
if (initialize_repertoire()) return -1;
|
||||
for (i = 0; i < repertoire_count[row]; i++) {
|
||||
const base_range_t *range = &repertoire[row][i];
|
||||
uint32_t count = range->last - range->first + 1;
|
||||
if (index >= range->offset && index - range->offset < count) {
|
||||
*out = range->first + index - range->offset;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int base32768_codepoint_at(unsigned bits, uint32_t index, uint32_t *codepoint)
|
||||
{
|
||||
if (!codepoint || (bits == 15 && index >= 32768u) ||
|
||||
(bits == 7 && index >= 128u) || (bits != 15 && bits != 7)) return -1;
|
||||
return index_to_codepoint(bits, index, codepoint);
|
||||
}
|
||||
|
||||
static int codepoint_to_index(uint32_t cp, unsigned *bits, uint32_t *index)
|
||||
{
|
||||
size_t row, i;
|
||||
if (initialize_repertoire()) return -1;
|
||||
for (row = 0; row < 2; row++) for (i = 0; i < repertoire_count[row]; i++) {
|
||||
const base_range_t *range = &repertoire[row][i];
|
||||
if (cp >= range->first && cp <= range->last) {
|
||||
*bits = row == 0 ? 15u : 7u;
|
||||
*index = range->offset + cp - range->first;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int base32768_encode(const uint8_t *input, size_t input_length,
|
||||
char *output, size_t output_capacity)
|
||||
{
|
||||
size_t i, used = 0;
|
||||
uint32_t value = 0;
|
||||
unsigned value_bits = 0;
|
||||
if ((!input && input_length) || !output || !output_capacity) { errno = EINVAL; return -1; }
|
||||
if (initialize_repertoire()) { errno = EPROTO; return -1; }
|
||||
for (i = 0; i < input_length; i++) {
|
||||
int bit;
|
||||
for (bit = 7; bit >= 0; bit--) {
|
||||
uint32_t cp;
|
||||
char encoded[4];
|
||||
size_t n;
|
||||
value = (value << 1) | ((input[i] >> bit) & 1u);
|
||||
if (++value_bits == 15) {
|
||||
if (index_to_codepoint(15, value, &cp)) { errno = EPROTO; return -1; }
|
||||
n = utf8_put(cp, encoded);
|
||||
if (used + n >= output_capacity) { errno = ENOSPC; return -1; }
|
||||
memcpy(output + used, encoded, n); used += n;
|
||||
value = 0; value_bits = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value_bits) {
|
||||
unsigned target_bits = value_bits >= 8 ? 15u : 7u;
|
||||
uint32_t cp;
|
||||
char encoded[4];
|
||||
size_t n;
|
||||
while (value_bits < target_bits) {
|
||||
value = (value << 1) | 1u;
|
||||
value_bits++;
|
||||
}
|
||||
if (index_to_codepoint(target_bits, value, &cp)) { errno = EPROTO; return -1; }
|
||||
n = utf8_put(cp, encoded);
|
||||
if (used + n >= output_capacity) { errno = ENOSPC; return -1; }
|
||||
memcpy(output + used, encoded, n); used += n;
|
||||
}
|
||||
output[used] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int base32768_decode(const char *input, uint8_t *output,
|
||||
size_t output_capacity, size_t *output_length)
|
||||
{
|
||||
size_t length, offset = 0, used = 0, i = 0, total_characters = 0, scan = 0;
|
||||
unsigned pending_bits = 0;
|
||||
uint32_t pending = 0;
|
||||
if (!input || (!output && output_capacity) || !output_length) { errno = EINVAL; return -1; }
|
||||
*output_length = 0;
|
||||
length = strlen(input);
|
||||
if (initialize_repertoire()) { errno = EPROTO; return -1; }
|
||||
while (scan < length) {
|
||||
uint32_t ignored;
|
||||
if (utf8_next((const unsigned char *)input, length, &scan, &ignored)) {
|
||||
errno = EILSEQ; return -1;
|
||||
}
|
||||
total_characters++;
|
||||
}
|
||||
while (offset < length) {
|
||||
uint32_t cp, value;
|
||||
unsigned bits, bit;
|
||||
if (utf8_next((const unsigned char *)input, length, &offset, &cp) ||
|
||||
codepoint_to_index(cp, &bits, &value)) { errno = EILSEQ; return -1; }
|
||||
if (bits == 7 && i + 1 != total_characters) { errno = EINVAL; return -1; }
|
||||
for (bit = bits; bit > 0; bit--) {
|
||||
pending = (pending << 1) | ((value >> (bit - 1)) & 1u);
|
||||
if (++pending_bits == 8) {
|
||||
if (used >= output_capacity) { errno = ENOSPC; return -1; }
|
||||
output[used++] = (uint8_t)pending;
|
||||
pending = 0; pending_bits = 0;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (pending != ((1u << pending_bits) - 1u)) { errno = EINVAL; return -1; }
|
||||
*output_length = used;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "cable.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <sodium.h>
|
||||
|
||||
static int hex_value(char c)
|
||||
{
|
||||
if(c>='0'&&c<='9')return c-'0';
|
||||
if(c>='a'&&c<='f')return c-'a'+10;
|
||||
if(c>='A'&&c<='F')return c-'A'+10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cable_address_parse(const char *address,uint8_t key[32],enum cable_address_scheme *scheme)
|
||||
{
|
||||
const char *hex=address; enum cable_address_scheme parsed=0; size_t i;
|
||||
if(!address||!key){errno=EINVAL;return -1;}
|
||||
if(!strncmp(hex,"cabal://",8)){parsed=CABLE_ADDRESS_CABAL;hex+=8;}
|
||||
else if(!strncmp(hex,"cable://",8)){parsed=CABLE_ADDRESS_CABLE;hex+=8;}
|
||||
else if(!strncmp(hex,"hyper://",8)){parsed=CABLE_ADDRESS_HYPER;hex+=8;}
|
||||
if(strlen(hex)!=64){errno=EINVAL;return -1;}
|
||||
for(i=0;i<32;i++){int hi=hex_value(hex[i*2]),lo=hex_value(hex[i*2+1]);if(hi<0||lo<0){errno=EINVAL;return -1;}key[i]=(uint8_t)((hi<<4)|lo);}
|
||||
if(scheme)*scheme=parsed?parsed:CABLE_ADDRESS_CABAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_address_format(const uint8_t key[32],enum cable_address_scheme scheme,char *out,size_t cap)
|
||||
{
|
||||
static const char digits[]="0123456789abcdef";const char *prefix;size_t i;
|
||||
if(!key||!out){errno=EINVAL;return -1;}
|
||||
switch(scheme){case CABLE_ADDRESS_CABAL:prefix="cabal://";break;case CABLE_ADDRESS_CABLE:prefix="cable://";break;case CABLE_ADDRESS_HYPER:prefix="hyper://";break;default:errno=EINVAL;return -1;}
|
||||
if(cap<73){errno=ENOSPC;return -1;}
|
||||
memcpy(out,prefix,8);for(i=0;i<32;i++){out[8+i*2]=digits[key[i]>>4];out[9+i*2]=digits[key[i]&15];}out[72]='\0';return 0;
|
||||
}
|
||||
|
||||
int cable_room_address_parse(const char *address,cable_room_address_t *room)
|
||||
{
|
||||
const char *query; char key_text[73]; size_t key_len;
|
||||
if(!address||!room){errno=EINVAL;return -1;}
|
||||
memset(room,0,sizeof(*room));
|
||||
query=strchr(address,'?');
|
||||
key_len=query?(size_t)(query-address):strlen(address);
|
||||
if(key_len>=sizeof(key_text)){errno=EINVAL;return -1;}
|
||||
memcpy(key_text,address,key_len);key_text[key_len]='\0';
|
||||
if(cable_address_parse(key_text,room->key,&room->scheme))return -1;
|
||||
if(query){
|
||||
static const char prefix[]="?admin=";
|
||||
if(strncmp(query,prefix,sizeof(prefix)-1)){errno=EINVAL;return -1;}
|
||||
if(cable_address_parse(query+sizeof(prefix)-1,room->admin_key,NULL))return -1;
|
||||
room->has_admin=1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_room_address_format(const cable_room_address_t *room,enum cable_address_scheme scheme,char *out,size_t cap)
|
||||
{
|
||||
char key[73],admin[73];
|
||||
if(!room||!out){errno=EINVAL;return -1;}
|
||||
if(cable_address_format(room->key,scheme,key,sizeof(key)))return -1;
|
||||
if(!room->has_admin){if(cap<sizeof(key)){errno=ENOSPC;return -1;}memcpy(out,key,sizeof(key));return 0;}
|
||||
if(cable_address_format(room->admin_key,CABLE_ADDRESS_CABAL,admin,sizeof(admin)))return -1;
|
||||
if(cap<8+64+7+64+1){errno=ENOSPC;return -1;}
|
||||
memcpy(out,key,72);memcpy(out+72,"?admin=",7);memcpy(out+79,admin+8,64);out[143]='\0';return 0;
|
||||
}
|
||||
|
||||
int cable_user_address_format(const uint8_t key[32],enum cable_address_scheme scheme,char *out,size_t cap)
|
||||
{
|
||||
return cable_address_format(key,scheme,out,cap);
|
||||
}
|
||||
|
||||
int cable_user_identity_path(const char *home,const uint8_t room_key[32],char *out,size_t cap)
|
||||
{
|
||||
static const char digits[]="0123456789abcdef";
|
||||
char base[1024],rooms[1056],roomdir[1120];size_t i;int n;
|
||||
if(!home||!home[0]||!room_key||!out){errno=EINVAL;return -1;}
|
||||
n=snprintf(base,sizeof(base),"%s/.cable",home);
|
||||
if(n<0||(size_t)n>=sizeof(base)){errno=ENAMETOOLONG;return -1;}
|
||||
if(mkdir(base,0700)<0&&errno!=EEXIST)return -1;
|
||||
n=snprintf(rooms,sizeof(rooms),"%s/rooms",base);
|
||||
if(n<0||(size_t)n>=sizeof(rooms)){errno=ENAMETOOLONG;return -1;}
|
||||
if(mkdir(rooms,0700)<0&&errno!=EEXIST)return -1;
|
||||
if(strlen(rooms)+1+64>=sizeof(roomdir)){errno=ENAMETOOLONG;return -1;}
|
||||
strcpy(roomdir,rooms);strcat(roomdir,"/");
|
||||
for(i=0;i<32;i++){roomdir[strlen(rooms)+1+i*2]=digits[room_key[i]>>4];roomdir[strlen(rooms)+2+i*2]=digits[room_key[i]&15];}
|
||||
roomdir[strlen(rooms)+65]='\0';
|
||||
if(mkdir(roomdir,0700)<0&&errno!=EEXIST)return -1;
|
||||
n=snprintf(out,cap,"%s/user-feed.key",roomdir);
|
||||
if(n<0||(size_t)n>=cap){errno=ENAMETOOLONG;return -1;}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_room_create(const char *home,uint8_t room_key[32])
|
||||
{
|
||||
sleep_keypair_t room; char feed_path[1200], secret_path[1300];
|
||||
size_t offset=0; int fd=-1, saved=0, rc=-1; ssize_t wrote;
|
||||
if(!home||!room_key){errno=EINVAL;return -1;}
|
||||
secret_path[0]='\0';
|
||||
memset(&room,0,sizeof(room));
|
||||
if(sleep_crypto_init()||sleep_gen_keypair(&room))goto done;
|
||||
if(cable_user_identity_path(home,room.pubkey,feed_path,sizeof(feed_path)))goto done;
|
||||
{
|
||||
static const char suffix[]="/user-feed.key";
|
||||
size_t n=strlen(feed_path);
|
||||
if(n<sizeof(suffix)-1||strcmp(feed_path+n-(sizeof(suffix)-1),suffix)){errno=EINVAL;goto done;}
|
||||
feed_path[n-(sizeof(suffix)-1)]='\0';
|
||||
}
|
||||
if(snprintf(secret_path,sizeof(secret_path),"%s/room-secret.key",feed_path) >= (int)sizeof(secret_path)) { errno=ENAMETOOLONG; goto done; }
|
||||
fd=open(secret_path,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0600);
|
||||
if(fd<0)goto done;
|
||||
while(offset<sizeof(room.seckey)){
|
||||
wrote=write(fd,room.seckey+offset,sizeof(room.seckey)-offset);
|
||||
if(wrote<0&&errno==EINTR)continue;
|
||||
if(wrote<=0){if(!wrote)errno=EIO;goto done;}
|
||||
offset+=(size_t)wrote;
|
||||
}
|
||||
if(fsync(fd))goto done;
|
||||
if(close(fd)){fd=-1;goto done;} fd=-1;
|
||||
memcpy(room_key,room.pubkey,sizeof(room.pubkey)); rc=0;
|
||||
done:
|
||||
saved=errno;
|
||||
if(fd>=0)close(fd);
|
||||
if(rc&&secret_path[0])unlink(secret_path);
|
||||
sodium_memzero(&room,sizeof(room));
|
||||
if(rc)errno=saved;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cable_user_identity_load_or_create(const char *path,sleep_keypair_t *identity)
|
||||
{
|
||||
int fd; struct stat st; char parent[1024]; char *slash; ssize_t n; uint8_t check[32];
|
||||
if(!path||!identity||strlen(path)>=sizeof(parent)){errno=EINVAL;return -1;}
|
||||
if(sleep_crypto_init())return -1;
|
||||
strcpy(parent,path);slash=strrchr(parent,'/');
|
||||
if(slash){*slash='\0';if(parent[0]&&mkdir(parent,0700)<0&&errno!=EEXIST)return -1;}
|
||||
fd=open(path,O_RDONLY|O_CLOEXEC|O_NOFOLLOW);
|
||||
if(fd>=0){
|
||||
if(fstat(fd,&st)||!S_ISREG(st.st_mode)||st.st_size!=crypto_sign_SECRETKEYBYTES){close(fd);errno=EINVAL;return -1;}
|
||||
if((st.st_mode&077)!=0&&fchmod(fd,0600)){int saved=errno;close(fd);errno=saved;return -1;}
|
||||
n=read(fd,identity->seckey,crypto_sign_SECRETKEYBYTES);close(fd);
|
||||
if(n!=crypto_sign_SECRETKEYBYTES||crypto_sign_ed25519_sk_to_pk(identity->pubkey,identity->seckey)!=0){errno=EIO;return -1;}
|
||||
if(crypto_sign_ed25519_sk_to_pk(check,identity->seckey)!=0||sodium_memcmp(check,identity->pubkey,32)){errno=EINVAL;return -1;}
|
||||
return 0;
|
||||
}
|
||||
if(errno!=ENOENT)return -1;
|
||||
if(sleep_gen_keypair(identity))return -1;
|
||||
fd=open(path,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0600);
|
||||
if(fd<0){if(errno==EEXIST)return cable_user_identity_load_or_create(path,identity);return -1;}
|
||||
n=write(fd,identity->seckey,crypto_sign_SECRETKEYBYTES);
|
||||
if(n!=crypto_sign_SECRETKEYBYTES){int saved=errno?errno:EIO;close(fd);unlink(path);errno=saved;return -1;}
|
||||
if(fsync(fd)){int saved=errno;close(fd);unlink(path);errno=saved;return -1;}
|
||||
if(close(fd)){int saved=errno;unlink(path);errno=saved;return -1;}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void put64be(uint8_t *p,uint64_t v){int i;for(i=0;i<8;i++)p[i]=(uint8_t)(v>>(56-8*i));}
|
||||
static uint64_t get64be(const uint8_t *p){uint64_t v=0;int i;for(i=0;i<8;i++)v=(v<<8)|p[i];return v;}
|
||||
|
||||
int cable_chat_message_encode(const cable_chat_message_t *m,uint8_t *out,size_t cap,size_t *written)
|
||||
{
|
||||
size_t total;
|
||||
if(!m||!out||!written||m->text_length>CABLE_CHAT_TEXT_BYTES||strlen(m->text)!=m->text_length){errno=EINVAL;return -1;}
|
||||
total=14+m->text_length;if(cap<total){errno=ENOSPC;return -1;}
|
||||
memcpy(out,"CBL1",4);put64be(out+4,m->timestamp_ms);out[12]=(uint8_t)(m->text_length>>8);out[13]=(uint8_t)m->text_length;
|
||||
memcpy(out+14,m->text,m->text_length);*written=total;return 0;
|
||||
}
|
||||
|
||||
int cable_chat_message_decode(cable_chat_message_t *m,const uint8_t *in,size_t n)
|
||||
{
|
||||
size_t text_len;
|
||||
if(!m||!in||n<14||memcmp(in,"CBL1",4)){errno=EPROTO;return -1;}
|
||||
text_len=((size_t)in[12]<<8)|in[13];if(text_len>CABLE_CHAT_TEXT_BYTES||n!=14+text_len||memchr(in+14,0,text_len)){errno=EPROTO;return -1;}
|
||||
memset(m,0,sizeof(*m));m->timestamp_ms=get64be(in+4);memcpy(m->text,in+14,text_len);m->text_length=text_len;return 0;
|
||||
}
|
||||
|
||||
int cable_chat_seal(const uint8_t recipient[32],const uint8_t *message,size_t n,uint8_t *out,size_t cap,size_t *written)
|
||||
{
|
||||
uint8_t curve[crypto_box_PUBLICKEYBYTES];
|
||||
if(!recipient||(!message&&n)||!out||!written){errno=EINVAL;return -1;}
|
||||
if(cap<n+crypto_box_SEALBYTES){errno=ENOSPC;return -1;}
|
||||
if(crypto_sign_ed25519_pk_to_curve25519(curve,recipient)!=0){errno=EINVAL;return -1;}
|
||||
if(crypto_box_seal(out,message,(unsigned long long)n,curve)!=0){errno=EIO;return -1;}
|
||||
*written=n+crypto_box_SEALBYTES;return 0;
|
||||
}
|
||||
|
||||
int cable_chat_open(const sleep_keypair_t *identity,const uint8_t *sealed,size_t n,uint8_t *out,size_t cap,size_t *written)
|
||||
{
|
||||
uint8_t curve_pk[crypto_box_PUBLICKEYBYTES],curve_sk[crypto_box_SECRETKEYBYTES];size_t plain;
|
||||
if(!identity||!sealed||!out||!written||n<crypto_box_SEALBYTES){errno=EINVAL;return -1;}
|
||||
plain=n-crypto_box_SEALBYTES;if(cap<plain){errno=ENOSPC;return -1;}
|
||||
if(crypto_sign_ed25519_pk_to_curve25519(curve_pk,identity->pubkey)!=0||crypto_sign_ed25519_sk_to_curve25519(curve_sk,identity->seckey)!=0){errno=EINVAL;return -1;}
|
||||
if(crypto_box_seal_open(out,sealed,(unsigned long long)n,curve_pk,curve_sk)!=0){errno=EACCES;return -1;}
|
||||
sodium_memzero(curve_sk,sizeof(curve_sk));*written=plain;return 0;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "cable.h"
|
||||
#include "cable_room.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sodium.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int ensure_link(const char *directory, const char *link_name,
|
||||
const char *target)
|
||||
{
|
||||
char path[1400], observed[1024];
|
||||
ssize_t length;
|
||||
struct stat info;
|
||||
int n = snprintf(path, sizeof(path), "%s/%s", directory, link_name);
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (symlink(target, path) == 0) return 0;
|
||||
if (errno != EEXIST) return -1;
|
||||
if (lstat(path, &info) || !S_ISLNK(info.st_mode)) { errno = EEXIST; return -1; }
|
||||
length = readlink(path, observed, sizeof(observed) - 1);
|
||||
if (length < 0) return -1;
|
||||
observed[length] = '\0';
|
||||
if (strcmp(observed, target)) { errno = EEXIST; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ensure_localname(const char *directory)
|
||||
{
|
||||
char path[1400], contents[16];
|
||||
FILE *file;
|
||||
size_t length;
|
||||
int n = snprintf(path, sizeof(path), "%s/localname", directory);
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) { errno = ENAMETOOLONG; return -1; }
|
||||
file = fopen(path, "rb");
|
||||
if (file) {
|
||||
length = fread(contents, 1, sizeof(contents), file);
|
||||
if (ferror(file) || fclose(file)) return -1;
|
||||
if (length != 5 || memcmp(contents, "local", 5)) { errno = EEXIST; return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (errno != ENOENT) return -1;
|
||||
file = fopen(path, "wbx");
|
||||
if (!file) return -1;
|
||||
if (fwrite("local", 1, 5, file) != 5 || fflush(file)) {
|
||||
int saved = errno ? errno : EIO;
|
||||
fclose(file); errno = saved; return -1;
|
||||
}
|
||||
if (fclose(file)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_user_feed_open(const char *home, const uint8_t room_key[32],
|
||||
sleep_keypair_t *identity, sleep_feed_t *feed)
|
||||
{
|
||||
char identity_path[1200], room_dir[1200], feed_dir[1240];
|
||||
char *slash;
|
||||
int n;
|
||||
if (!home || !room_key || !identity || !feed) { errno = EINVAL; return -1; }
|
||||
if (cable_user_identity_path(home, room_key, identity_path,
|
||||
sizeof(identity_path)) ||
|
||||
cable_user_identity_load_or_create(identity_path, identity)) return -1;
|
||||
slash = strrchr(identity_path, '/');
|
||||
if (!slash) { errno = EINVAL; goto fail_identity; }
|
||||
*slash = '\0';
|
||||
n = snprintf(room_dir, sizeof(room_dir), "%s/0", identity_path);
|
||||
if (n < 0 || (size_t)n >= sizeof(room_dir)) { errno = ENAMETOOLONG; goto fail_identity; }
|
||||
if (mkdir(room_dir, 0700) < 0 && errno != EEXIST) goto fail_identity;
|
||||
n = snprintf(feed_dir, sizeof(feed_dir), "%s", room_dir);
|
||||
if (n < 0 || (size_t)n >= sizeof(feed_dir)) { errno = ENAMETOOLONG; goto fail_identity; }
|
||||
/* Kappa Core 3 / multifeed 3 stores the local writer in room/0 and
|
||||
* addresses its storage by the bare Hypercore names. Keep the SLEEP
|
||||
* filename convention as symlinked aliases over the same Hypercore files. */
|
||||
if (ensure_link(feed_dir, "feed.tree", "tree") ||
|
||||
ensure_link(feed_dir, "feed.signatures", "signatures") ||
|
||||
ensure_link(feed_dir, "feed.bitfield", "bitfield") ||
|
||||
ensure_link(feed_dir, "feed.data", "data") ||
|
||||
ensure_link(feed_dir, "feed.key", "key") ||
|
||||
ensure_link(feed_dir, "secret_key", "../user-feed.key") ||
|
||||
ensure_localname(feed_dir)) goto fail_identity;
|
||||
if (sleep_feed_open(feed, feed_dir, "feed", identity)) goto fail_identity;
|
||||
return 0;
|
||||
fail_identity:
|
||||
sodium_memzero(identity, sizeof(*identity));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cable_user_feed_append(const char *home, const uint8_t room_key[32],
|
||||
const uint8_t *record, size_t record_length,
|
||||
uint8_t user_key[32])
|
||||
{
|
||||
sleep_keypair_t identity;
|
||||
sleep_feed_t feed;
|
||||
int rc = -1;
|
||||
memset(&identity, 0, sizeof(identity));
|
||||
memset(&feed, 0, sizeof(feed));
|
||||
if (!record || !record_length || !user_key) { errno = EINVAL; return -1; }
|
||||
if (cable_user_feed_open(home, room_key, &identity, &feed)) return -1;
|
||||
if (sleep_feed_append(&feed, record, record_length) || sleep_feed_flush(&feed)) goto done;
|
||||
memcpy(user_key, identity.pubkey, sizeof(identity.pubkey));
|
||||
rc = 0;
|
||||
done:
|
||||
sleep_feed_close(&feed);
|
||||
sodium_memzero(&identity, sizeof(identity));
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int append_json_text(char *out, size_t capacity, size_t *offset,
|
||||
const char *text)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)text;
|
||||
if (*offset >= capacity) { errno = ENOSPC; return -1; }
|
||||
out[(*offset)++] = '"';
|
||||
for (; *p; p++) {
|
||||
const char *escaped = NULL;
|
||||
char control[7];
|
||||
switch (*p) {
|
||||
case '"': escaped = "\\\""; break;
|
||||
case '\\': escaped = "\\\\"; break;
|
||||
case '\b': escaped = "\\b"; break;
|
||||
case '\f': escaped = "\\f"; break;
|
||||
case '\n': escaped = "\\n"; break;
|
||||
case '\r': escaped = "\\r"; break;
|
||||
case '\t': escaped = "\\t"; break;
|
||||
default: break;
|
||||
}
|
||||
if (*p < 0x20 && !escaped) {
|
||||
snprintf(control, sizeof(control), "\\u%04x", *p);
|
||||
escaped = control;
|
||||
}
|
||||
if (escaped) {
|
||||
size_t n = strlen(escaped);
|
||||
if (n > capacity - *offset) { errno = ENOSPC; return -1; }
|
||||
memcpy(out + *offset, escaped, n); *offset += n;
|
||||
} else {
|
||||
if (*offset >= capacity) { errno = ENOSPC; return -1; }
|
||||
out[(*offset)++] = (char)*p;
|
||||
}
|
||||
}
|
||||
if (*offset >= capacity) { errno = ENOSPC; return -1; }
|
||||
out[(*offset)++] = '"';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_user_feed_publish(const char *home, const uint8_t room_key[32],
|
||||
const char *record_type, const char *nick,
|
||||
const char *channel, const char *text,
|
||||
uint8_t user_key[32])
|
||||
{
|
||||
char *record;
|
||||
size_t capacity, offset = 0;
|
||||
struct timespec now;
|
||||
unsigned long long timestamp;
|
||||
int n, rc = -1;
|
||||
if (!home || !room_key || !record_type || !user_key ||
|
||||
(strcmp(record_type, "about") && strcmp(record_type, "chat/text")) ||
|
||||
(!strcmp(record_type, "about") && !nick) ||
|
||||
(!strcmp(record_type, "chat/text") && (!channel || !text))) {
|
||||
errno = EINVAL; return -1;
|
||||
}
|
||||
if (strlen(record_type) > 16 || (nick && strlen(nick) > CABLE_CHAT_TEXT_BYTES) ||
|
||||
(channel && strlen(channel) > CABLE_ROOM_CHANNEL_BYTES) ||
|
||||
(text && strlen(text) > CABLE_CHAT_TEXT_BYTES)) { errno = EMSGSIZE; return -1; }
|
||||
capacity = 512 + strlen(record_type) * 6 + (nick ? strlen(nick) * 6 : 0) +
|
||||
(channel ? strlen(channel) * 6 : 0) + (text ? strlen(text) * 6 : 0);
|
||||
record = malloc(capacity);
|
||||
if (!record) return -1;
|
||||
if (clock_gettime(CLOCK_REALTIME, &now)) goto done;
|
||||
timestamp = (unsigned long long)now.tv_sec * 1000u +
|
||||
(unsigned long long)now.tv_nsec / 1000000u;
|
||||
n = snprintf(record, capacity, "{\"type\":");
|
||||
if (n < 0 || (size_t)n >= capacity) { errno = ENOSPC; goto done; }
|
||||
offset = (size_t)n;
|
||||
if (append_json_text(record, capacity, &offset, record_type)) goto done;
|
||||
n = snprintf(record + offset, capacity - offset, ",\"content\":{");
|
||||
if (n < 0 || (size_t)n >= capacity - offset) { errno = ENOSPC; goto done; }
|
||||
offset += (size_t)n;
|
||||
if (!strcmp(record_type, "about")) {
|
||||
n = snprintf(record + offset, capacity - offset, "\"name\":");
|
||||
if (n < 0 || (size_t)n >= capacity - offset) { errno = ENOSPC; goto done; }
|
||||
offset += (size_t)n;
|
||||
if (append_json_text(record, capacity, &offset, nick)) goto done;
|
||||
} else {
|
||||
n = snprintf(record + offset, capacity - offset, "\"channel\":");
|
||||
if (n < 0 || (size_t)n >= capacity - offset) { errno = ENOSPC; goto done; }
|
||||
offset += (size_t)n;
|
||||
if (append_json_text(record, capacity, &offset, channel)) goto done;
|
||||
n = snprintf(record + offset, capacity - offset, ",\"text\":");
|
||||
if (n < 0 || (size_t)n >= capacity - offset) { errno = ENOSPC; goto done; }
|
||||
offset += (size_t)n;
|
||||
if (append_json_text(record, capacity, &offset, text)) goto done;
|
||||
}
|
||||
n = snprintf(record + offset, capacity - offset,
|
||||
"},\"timestamp\":%llu}", timestamp);
|
||||
if (n < 0 || (size_t)n >= capacity - offset) { errno = ENOSPC; goto done; }
|
||||
offset += (size_t)n;
|
||||
rc = cable_user_feed_append(home, room_key, (const uint8_t *)record,
|
||||
offset, user_key);
|
||||
done:
|
||||
free(record);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
// hyperswarm dht
|
||||
#include "cesspool.h"
|
||||
#include "dat_wire.h"
|
||||
#include "sleep_proto.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef SLEEP_SALT
|
||||
#include <sodium.h>
|
||||
#endif
|
||||
|
||||
#define CESSPOOL_PACKET_BYTES 72u
|
||||
#define CESSPOOL_MAX_RENDEZVOUS_PEERS 34u
|
||||
#define CESSPOOL_PEER_TTL_MS 600000u
|
||||
#define CESSPOOL_BOOTSTRAP_PROBE_MS 5000u
|
||||
#define CESSPOOL_BOOTSTRAP_TIMEOUT_MS 15000u
|
||||
static uint64_t monotonic_ms(void)
|
||||
{
|
||||
struct timespec now;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0;
|
||||
return (uint64_t)now.tv_sec * 1000u + (uint64_t)now.tv_nsec / 1000000u;
|
||||
}
|
||||
static int pb_var(uint8_t tag,uint64_t value,uint8_t*out,size_t cap,size_t*off){size_t n;if(*off>=cap){errno=ENOSPC;return-1;}out[(*off)++]=tag;if(sleep_varint_encode(value,out+*off,cap-*off,&n))return-1;*off+=n;return 0;}
|
||||
static int pb_bytes(uint8_t tag,const uint8_t*data,size_t len,uint8_t*out,size_t cap,size_t*off){size_t n;if(*off>=cap){errno=ENOSPC;return-1;}out[(*off)++]=tag;if(sleep_varint_encode(len,out+*off,cap-*off,&n))return-1;*off+=n;if(len>cap-*off){errno=ENOSPC;return-1;}memcpy(out+*off,data,len);*off+=len;return 0;}
|
||||
|
||||
int cesspool_request_encode(const cesspool_request_t*r,uint8_t*out,size_t cap,size_t*written){size_t off=0;if(!r||!out){errno=EINVAL;return-1;}if(pb_var(0x08,r->type,out,cap,&off))return-1;if(r->port&&pb_var(0x10,r->port,out,cap,&off))return-1;if(r->local_address_len&&pb_bytes(0x1a,r->local_address,r->local_address_len,out,cap,&off))return-1;if(written)*written=off;return 0;}
|
||||
int cesspool_request_decode(cesspool_request_t*r,const uint8_t*in,size_t len){size_t off=0,n;uint64_t v;if(!r||!in){errno=EINVAL;return-1;}memset(r,0,sizeof(*r));while(off<len){uint8_t tag=in[off++];if(tag==0x08||tag==0x10){if(sleep_varint_decode(in+off,len-off,&v,&n))return-1;off+=n;if(tag==0x08)r->type=(uint32_t)v;else r->port=(uint16_t)v;}else if(tag==0x1a){if(sleep_varint_decode(in+off,len-off,&v,&n))return-1;off+=n;if(v>sizeof(r->local_address)||v>len-off){errno=EPROTO;return-1;}memcpy(r->local_address,in+off,(size_t)v);r->local_address_len=(size_t)v;off+=(size_t)v;}else{errno=EPROTO;return-1;}}return 0;}
|
||||
int cesspool_response_encode(const cesspool_response_t *r, uint8_t *out,
|
||||
size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!r || !out) { errno = EINVAL; return -1; }
|
||||
if (r->peers_len && pb_bytes(0x0a, r->peers, r->peers_len, out, cap, &off)) return -1;
|
||||
if (r->local_peers_len && pb_bytes(0x12, r->local_peers, r->local_peers_len, out, cap, &off)) return -1;
|
||||
if (r->peer_info_len && pb_bytes(0x1a, r->peer_info, r->peer_info_len, out, cap, &off)) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
int cesspool_response_decode(cesspool_response_t *r, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0, n;
|
||||
uint64_t v;
|
||||
if (!r || !in) { errno = EINVAL; return -1; }
|
||||
memset(r, 0, sizeof(*r));
|
||||
while (off < len) {
|
||||
uint8_t tag = in[off++], *dst;
|
||||
size_t *dstlen, capacity;
|
||||
if (sleep_varint_decode(in + off, len - off, &v, &n)) return -1;
|
||||
off += n;
|
||||
if (v > len - off) { errno = EPROTO; return -1; }
|
||||
if (tag == 0x0a) { dst = r->peers; dstlen = &r->peers_len; capacity = sizeof(r->peers); }
|
||||
else if (tag == 0x12) { dst = r->local_peers; dstlen = &r->local_peers_len; capacity = sizeof(r->local_peers); }
|
||||
else if (tag == 0x1a) { dst = r->peer_info; dstlen = &r->peer_info_len; capacity = sizeof(r->peer_info); }
|
||||
else { off += (size_t)v; continue; }
|
||||
if (v > capacity) { errno = EOVERFLOW; return -1; }
|
||||
memcpy(dst, in + off, (size_t)v);
|
||||
*dstlen = (size_t)v;
|
||||
off += (size_t)v;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int cesspool_process_response(cesspool_t*p,const cesspool_response_t*r,const uint8_t prefix[2]){size_t off;cesspool_peer_t peer;struct sockaddr_in*address;if(!p||!r){errno=EINVAL;return-1;}if(r->peers_len%6||r->local_peers_len%4){errno=EPROTO;return-1;}for(off=0;off<r->peers_len;off+=6){memset(&peer,0,sizeof(peer));address=(struct sockaddr_in*)&peer.address.addr;address->sin_family=AF_INET;memcpy(&address->sin_addr,r->peers+off,4);memcpy(&address->sin_port,r->peers+off+4,2);peer.address.addrlen=sizeof(*address);memcpy(peer.public_key,r->peers+off,6);cesspool_add_peer(p,&peer);}if(prefix)for(off=0;off<r->local_peers_len;off+=4){memset(&peer,0,sizeof(peer));address=(struct sockaddr_in*)&peer.address.addr;address->sin_family=AF_INET;((uint8_t*)&address->sin_addr)[0]=prefix[0];((uint8_t*)&address->sin_addr)[1]=prefix[1];((uint8_t*)&address->sin_addr)[2]=r->local_peers[off];((uint8_t*)&address->sin_addr)[3]=r->local_peers[off+1];memcpy(&address->sin_port,r->local_peers+off+2,2);peer.address.addrlen=sizeof(*address);peer.local=1;memcpy(peer.public_key,&address->sin_addr,4);memcpy(peer.public_key+4,&address->sin_port,2);cesspool_add_peer(p,&peer);}return 0;}
|
||||
|
||||
int cesspool_discovery_key(const uint8_t key[32], uint8_t topic[32])
|
||||
{
|
||||
if (!key || !topic) { errno = EINVAL; return -1; }
|
||||
#ifdef SLEEP_SALT
|
||||
/* The root NETWORK_PROTOCOL token selects the discovery namespace.
|
||||
* With "hypercore", this matches legacy Dat's discoveryKey derivation. */
|
||||
return crypto_generichash(topic, 32,
|
||||
(const unsigned char *)DAT_NETWORK_PROTOCOL,
|
||||
strlen(DAT_NETWORK_PROTOCOL), key, 32);
|
||||
#else
|
||||
memcpy(topic, key, 32);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int cesspool_encode(const cesspool_message_t *m, uint8_t *out,
|
||||
size_t capacity, size_t *written)
|
||||
{
|
||||
size_t total = CESSPOOL_PACKET_BYTES;
|
||||
if (!m || !out) { errno = EINVAL; return -1; }
|
||||
if (m->type > CESSPOOL_HOLEPUNCH) { errno = EINVAL; return -1; }
|
||||
if (m->type == CESSPOOL_REPLY) {
|
||||
if (capacity < CESSPOOL_PACKET_BYTES + 2 ||
|
||||
m->payload_len > UINT16_MAX ||
|
||||
m->payload_len > capacity - CESSPOOL_PACKET_BYTES - 2) {
|
||||
errno = ENOSPC; return -1;
|
||||
}
|
||||
total += 2 + m->payload_len;
|
||||
}
|
||||
if (capacity < total) { errno = ENOSPC; return -1; }
|
||||
memcpy(out, "CSP0", 4);
|
||||
out[4] = m->type;
|
||||
out[5] = m->type == CESSPOOL_REPLY ? 1 : 0;
|
||||
out[6] = (uint8_t)(m->port >> 8);
|
||||
out[7] = (uint8_t)m->port;
|
||||
memcpy(out + 8, m->topic, 32);
|
||||
memcpy(out + 40, m->peer_id, 32);
|
||||
if (m->type == CESSPOOL_REPLY) {
|
||||
out[72] = (uint8_t)(m->payload_len >> 8);
|
||||
out[73] = (uint8_t)m->payload_len;
|
||||
memcpy(out + 74, m->payload, m->payload_len);
|
||||
}
|
||||
if (written) *written = total;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cesspool_decode(cesspool_message_t *m, const uint8_t *in, size_t len)
|
||||
{
|
||||
if (!m || !in) { errno = EINVAL; return -1; }
|
||||
if (len < CESSPOOL_PACKET_BYTES || memcmp(in, "CSP0", 4) ||
|
||||
in[4] > CESSPOOL_HOLEPUNCH) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
memset(m, 0, sizeof(*m));
|
||||
m->type = in[4];
|
||||
m->port = ((uint16_t)in[6] << 8) | in[7];
|
||||
memcpy(m->topic, in + 8, 32);
|
||||
memcpy(m->peer_id, in + 40, 32);
|
||||
if (m->type == CESSPOOL_REPLY) {
|
||||
if (in[5] != 1 || len < CESSPOOL_PACKET_BYTES + 2) { errno = EPROTO; return -1; }
|
||||
m->payload_len = ((size_t)in[72] << 8) | in[73];
|
||||
if (m->payload_len > sizeof(m->payload) || len != CESSPOOL_PACKET_BYTES + 2 + m->payload_len) { errno = EPROTO; return -1; }
|
||||
memcpy(m->payload, in + 74, m->payload_len);
|
||||
} else if (in[5] != 0 || len != CESSPOOL_PACKET_BYTES) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_message(cesspool_t *pool,const dat_peer_t *peer,uint8_t type,const uint8_t topic[32],uint16_t port)
|
||||
{
|
||||
cesspool_message_t message;uint8_t packet[DATNET_MAX_PACKET];size_t len;
|
||||
memset(&message,0,sizeof(message));message.type=type;message.port=port;if(topic)memcpy(message.topic,topic,32);memcpy(message.peer_id,pool->net.peerid,32);
|
||||
if(cesspool_encode(&message,packet,sizeof(packet),&len))return-1;
|
||||
return datnet_send(&pool->net,peer,packet,len);
|
||||
}
|
||||
static int send_reply(cesspool_t *pool, const dat_peer_t *peer,
|
||||
const uint8_t topic[32], const cesspool_response_t *response)
|
||||
{
|
||||
cesspool_message_t message;
|
||||
uint8_t packet[DATNET_MAX_PACKET];
|
||||
size_t payload_len, packet_len;
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.type = CESSPOOL_REPLY;
|
||||
memcpy(message.topic, topic, CESSPOOL_TOPIC_BYTES);
|
||||
memcpy(message.peer_id, pool->net.peerid, CESSPOOL_PEER_BYTES);
|
||||
if (cesspool_response_encode(response, message.payload,
|
||||
sizeof(message.payload), &payload_len)) return -1;
|
||||
message.payload_len = payload_len;
|
||||
if (cesspool_encode(&message, packet, sizeof(packet), &packet_len)) return -1;
|
||||
return datnet_send(&pool->net, peer, packet, packet_len);
|
||||
}
|
||||
static int send_bootstrap(cesspool_t *pool,uint8_t type,const uint8_t topic[32],uint16_t port)
|
||||
{
|
||||
static const uint16_t lan_ports[] = {DATNET_PORT,3000,3002,3004,2001,2003,2005};
|
||||
size_t i;int sent=0,lan_sent=0;
|
||||
if (!pool) { errno = EINVAL; return -1; }
|
||||
for(i=0;i<pool->bootstrap_count;i++)
|
||||
if(send_message(pool,&pool->bootstrap_nodes[i],type,topic,port)==0)sent++;
|
||||
/* Always keep zero-configuration LAN discovery active, even when a
|
||||
* rendezvous node is configured. Routers do not forward these broadcasts;
|
||||
* this complements Internet discovery and lets same-LAN peers connect
|
||||
* without requiring them to share bootstrap configuration. */
|
||||
for (i = 0; i < sizeof(lan_ports) / sizeof(lan_ports[0]); i++) {
|
||||
dat_peer_t target;
|
||||
struct sockaddr_in *address;
|
||||
memset(&target, 0, sizeof(target));
|
||||
address = (struct sockaddr_in *)&target.addr;
|
||||
address->sin_family = AF_INET;
|
||||
address->sin_addr.s_addr = htonl(INADDR_BROADCAST);
|
||||
address->sin_port = htons(lan_ports[i]);
|
||||
target.addrlen = sizeof(*address);
|
||||
if (send_message(pool, &target, type, topic, port) == 0) lan_sent++;
|
||||
}
|
||||
if (pool->bootstrap_count) return (sent || lan_sent) ? 0 : -1;
|
||||
return 0; /* LAN broadcasts are best-effort, never a join failure. */
|
||||
}
|
||||
static int has_topic(const cesspool_t *pool,const uint8_t topic[32],int announcing)
|
||||
{
|
||||
size_t i;
|
||||
for(i=0;i<pool->topic_count;i++)if(!memcmp(pool->topics[i].key,topic,32)&&(!announcing||pool->topics[i].announcing))return 1;
|
||||
return 0;
|
||||
}
|
||||
static int same_peer_address(const dat_peer_t *a, const dat_peer_t *b)
|
||||
{
|
||||
const struct sockaddr_in *aa, *bb;
|
||||
if (!a || !b || a->addrlen < (socklen_t)sizeof(struct sockaddr_in) ||
|
||||
b->addrlen < (socklen_t)sizeof(struct sockaddr_in)) return 0;
|
||||
aa = (const struct sockaddr_in *)&a->addr;
|
||||
bb = (const struct sockaddr_in *)&b->addr;
|
||||
return aa->sin_family == AF_INET && bb->sin_family == AF_INET &&
|
||||
aa->sin_addr.s_addr == bb->sin_addr.s_addr && aa->sin_port == bb->sin_port;
|
||||
}
|
||||
|
||||
static int announcement_add(cesspool_t *pool, const uint8_t topic[32],
|
||||
const uint8_t peer_id[32], const dat_peer_t *address,
|
||||
uint64_t now)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < pool->announcement_count; i++) {
|
||||
cesspool_announcement_t *entry = &pool->announcements[i];
|
||||
if (!memcmp(entry->topic, topic, 32) && !memcmp(entry->peer_id, peer_id, 32)) {
|
||||
entry->address = *address;
|
||||
entry->last_seen = now;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (pool->announcement_count >= CESSPOOL_MAX_ANNOUNCEMENTS) {
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
cesspool_announcement_t *entry = &pool->announcements[pool->announcement_count++];
|
||||
memcpy(entry->topic, topic, 32);
|
||||
memcpy(entry->peer_id, peer_id, 32);
|
||||
entry->address = *address;
|
||||
entry->last_seen = now;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void announcement_remove(cesspool_t *pool, const uint8_t topic[32],
|
||||
const uint8_t peer_id[32])
|
||||
{
|
||||
size_t i = 0;
|
||||
while (i < pool->announcement_count) {
|
||||
cesspool_announcement_t *entry = &pool->announcements[i];
|
||||
if (!memcmp(entry->topic, topic, 32) && !memcmp(entry->peer_id, peer_id, 32)) {
|
||||
pool->announcements[i] = pool->announcements[--pool->announcement_count];
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static int peer_has_announcement(const cesspool_t *pool, const uint8_t peer_id[32])
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < pool->announcement_count; i++)
|
||||
if (!memcmp(pool->announcements[i].peer_id, peer_id, 32)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_rendezvous_reply(cesspool_t *pool, dat_peer_t *requester,
|
||||
const uint8_t topic[32])
|
||||
{
|
||||
cesspool_response_t response;
|
||||
size_t i;
|
||||
memset(&response, 0, sizeof(response));
|
||||
for (i = 0; i < pool->announcement_count; i++) {
|
||||
const cesspool_announcement_t *peer = &pool->announcements[i];
|
||||
const struct sockaddr_in *address;
|
||||
uint8_t *record;
|
||||
if (memcmp(peer->topic, topic, 32) || same_peer_address(&peer->address, requester) ||
|
||||
peer->address.addrlen < (socklen_t)sizeof(struct sockaddr_in)) continue;
|
||||
address = (const struct sockaddr_in *)&peer->address.addr;
|
||||
if (address->sin_family != AF_INET ||
|
||||
response.peer_info_len / 38 >= CESSPOOL_MAX_RENDEZVOUS_PEERS ||
|
||||
response.peer_info_len + 38 > sizeof(response.peer_info)) continue;
|
||||
record = response.peer_info + response.peer_info_len;
|
||||
memcpy(record, &address->sin_addr, 4);
|
||||
memcpy(record + 4, &address->sin_port, 2);
|
||||
memcpy(record + 6, peer->peer_id, 32);
|
||||
response.peer_info_len += 38;
|
||||
}
|
||||
return send_reply(pool, requester, topic, &response);
|
||||
}
|
||||
|
||||
static int receive_rendezvous_reply(cesspool_t *pool,
|
||||
const cesspool_message_t *message)
|
||||
{
|
||||
cesspool_response_t response;
|
||||
size_t i;
|
||||
if (cesspool_response_decode(&response, message->payload, message->payload_len)) return -1;
|
||||
if (response.peer_info_len % 38) { errno = EPROTO; return -1; }
|
||||
for (i = 0; i < response.peer_info_len; i += 38) {
|
||||
const uint8_t *record = response.peer_info + i;
|
||||
cesspool_peer_t peer;
|
||||
struct sockaddr_in *address;
|
||||
memset(&peer, 0, sizeof(peer));
|
||||
address = (struct sockaddr_in *)&peer.address.addr;
|
||||
address->sin_family = AF_INET;
|
||||
memcpy(&address->sin_addr, record, 4);
|
||||
memcpy(&address->sin_port, record + 4, 2);
|
||||
peer.address.addrlen = sizeof(*address);
|
||||
memcpy(peer.public_key, record + 6, 32);
|
||||
memcpy(peer.discovery_topic, message->topic, 32);
|
||||
peer.discovered = 1;
|
||||
peer.last_seen = monotonic_ms();
|
||||
if (announcement_add(pool, message->topic, peer.public_key,
|
||||
&peer.address, peer.last_seen)) continue;
|
||||
if (cesspool_add_peer(pool, &peer)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cesspool_on_message(datnet_t *net,dat_peer_t *source,const uint8_t *packet,size_t length)
|
||||
{
|
||||
cesspool_t *pool=(cesspool_t *)net->context;
|
||||
cesspool_message_t message;
|
||||
cesspool_peer_t peer;
|
||||
if(!pool)return;
|
||||
if(cesspool_decode(&message,packet,length)){
|
||||
if(pool->on_transport_packet)pool->on_transport_packet(pool,source,packet,length);
|
||||
return;
|
||||
}
|
||||
if(message.type>CESSPOOL_HOLEPUNCH||!memcmp(message.peer_id,net->peerid,32))return;
|
||||
if (message.type == CESSPOOL_REPLY) {
|
||||
size_t i;
|
||||
int from_bootstrap = 0;
|
||||
for (i = 0; i < pool->bootstrap_count; i++)
|
||||
if (same_peer_address(&pool->bootstrap_nodes[i], source)) {
|
||||
from_bootstrap = 1;
|
||||
break;
|
||||
}
|
||||
if (!from_bootstrap) return;
|
||||
/* A malformed datagram is not proof that the configured rendezvous
|
||||
* endpoint is speaking Cesspool, so validate before refreshing online. */
|
||||
if (receive_rendezvous_reply(pool, &message)) return;
|
||||
pool->bootstrap_last_seen = monotonic_ms();
|
||||
pool->bootstrap_reachable = 1;
|
||||
return;
|
||||
}
|
||||
if (message.type == CESSPOOL_PONG) {
|
||||
size_t i;
|
||||
for (i = 0; i < pool->bootstrap_count; i++) {
|
||||
if (same_peer_address(&pool->bootstrap_nodes[i], source)) {
|
||||
pool->bootstrap_last_seen = monotonic_ms();
|
||||
pool->bootstrap_reachable = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
memset(&peer,0,sizeof(peer));peer.address=*source;memcpy(peer.public_key,message.peer_id,32);
|
||||
peer.last_seen = monotonic_ms();
|
||||
if (message.type == CESSPOOL_ANNOUNCE) {
|
||||
peer.discovered = 1;
|
||||
memcpy(peer.discovery_topic, message.topic, sizeof(peer.discovery_topic));
|
||||
if (announcement_add(pool, message.topic, message.peer_id, source,
|
||||
peer.last_seen)) return;
|
||||
}
|
||||
cesspool_add_peer(pool,&peer);
|
||||
if (message.type == CESSPOOL_UNANNOUNCE) {
|
||||
cesspool_peer_t *known = cesspool_find_peer(pool, message.peer_id);
|
||||
announcement_remove(pool, message.topic, message.peer_id);
|
||||
if (known) known->discovered = peer_has_announcement(pool, message.peer_id);
|
||||
} else if(message.type==CESSPOOL_PING)send_message(pool,source,CESSPOOL_PONG,NULL,pool->port);
|
||||
else if(message.type==CESSPOOL_LOOKUP&&pool->bootstrap_server)
|
||||
(void)send_rendezvous_reply(pool, source, message.topic);
|
||||
else if(message.type==CESSPOOL_LOOKUP&&has_topic(pool,message.topic,1))
|
||||
send_message(pool,source,CESSPOOL_ANNOUNCE,message.topic,pool->port);
|
||||
else if(message.type==CESSPOOL_HOLEPUNCH&&pool->on_holepunch)pool->on_holepunch(pool,source);
|
||||
}
|
||||
int cesspool_init(cesspool_t *pool,const uint8_t peer_id[32])
|
||||
{
|
||||
uint8_t empty[32]={0};if(!pool||!peer_id){errno=EINVAL;return-1;}memset(pool,0,sizeof(*pool));if(datnet_init(&pool->net,empty,peer_id))return-1;pool->net.context=pool;pool->net.on_message=cesspool_on_message;return 0;
|
||||
}
|
||||
int cesspool_add_bootstrap(cesspool_t *pool,const char *host,uint16_t port)
|
||||
{
|
||||
struct addrinfo hints, *resolved = NULL;
|
||||
struct sockaddr_in *address;
|
||||
dat_peer_t *peer;
|
||||
if(!pool||!host||!port||pool->bootstrap_count>=CESSPOOL_MAX_BOOTSTRAP){errno=EINVAL;return-1;}
|
||||
memset(&hints,0,sizeof(hints));hints.ai_family=AF_INET;hints.ai_socktype=SOCK_DGRAM;
|
||||
if(getaddrinfo(host,NULL,&hints,&resolved)!=0||!resolved){errno=EHOSTUNREACH;return-1;}
|
||||
peer=&pool->bootstrap_nodes[pool->bootstrap_count];memset(peer,0,sizeof(*peer));
|
||||
memcpy(&peer->addr,resolved->ai_addr,resolved->ai_addrlen);peer->addrlen=(socklen_t)resolved->ai_addrlen;
|
||||
address=(struct sockaddr_in*)&peer->addr;address->sin_port=htons(port);
|
||||
freeaddrinfo(resolved);
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < pool->bootstrap_count; i++)
|
||||
if (same_peer_address(&pool->bootstrap_nodes[i], peer)) return 0;
|
||||
}
|
||||
if (pool->bootstrap_count >= CESSPOOL_MAX_BOOTSTRAP) { errno = ENOSPC; return -1; }
|
||||
pool->bootstrap_count++;return 0;
|
||||
}
|
||||
static int add_endpoint_list(cesspool_t *pool, const char *setting)
|
||||
{
|
||||
char list[2048], *item, *next;
|
||||
int added = 0;
|
||||
if (!setting || !*setting) return 0;
|
||||
if (strlen(setting) >= sizeof(list)) { errno = EOVERFLOW; return -1; }
|
||||
strcpy(list, setting);
|
||||
for (item = list; item && *item; item = next) {
|
||||
char *separator, *end;
|
||||
unsigned long port;
|
||||
next = strpbrk(item, ",;\r\n");
|
||||
if (next) *next++ = '\0';
|
||||
separator = strrchr(item, ':');
|
||||
if (!separator || separator == item || !separator[1]) continue;
|
||||
*separator++ = '\0';
|
||||
port = strtoul(separator, &end, 10);
|
||||
if (*end || port == 0 || port > 65535) continue;
|
||||
if (cesspool_add_bootstrap(pool, item, (uint16_t)port) != 0) return -1;
|
||||
added++;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
static int bootstrap_config_path(char *path, size_t capacity, int make_dirs)
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
const char *config = getenv("XDG_CONFIG_HOME");
|
||||
char directory[1024], config_directory[1024];
|
||||
if (!home || !*home) home = getenv("USERPROFILE");
|
||||
#ifdef _WIN32
|
||||
if ((!home || !*home) && getenv("HOMEDRIVE") && getenv("HOMEPATH")) {
|
||||
static char combined[1024];
|
||||
if (snprintf(combined, sizeof(combined), "%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"))
|
||||
< (int)sizeof(combined)) home = combined;
|
||||
}
|
||||
#endif
|
||||
if (!home || !*home) home = ".";
|
||||
if (!config || !*config) {
|
||||
if (snprintf(config_directory, sizeof(config_directory), "%s/.config", home) >= (int)sizeof(config_directory)) {
|
||||
errno = ENAMETOOLONG; return -1;
|
||||
}
|
||||
config = config_directory;
|
||||
}
|
||||
if (snprintf(directory, sizeof(directory), "%s/dat-gpt", config) >= (int)sizeof(directory) ||
|
||||
snprintf(path, capacity, "%s/bootstrap", directory) >= (int)capacity) {
|
||||
errno = ENAMETOOLONG; return -1;
|
||||
}
|
||||
if (make_dirs) {
|
||||
char parent[1024];
|
||||
if (snprintf(parent, sizeof(parent), "%s", config) >= (int)sizeof(parent)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (mkdir(parent, 0700) && errno != EEXIST) return -1;
|
||||
if (mkdir(directory, 0700) && errno != EEXIST) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cesspool_configure_bootstraps(cesspool_t *pool)
|
||||
{
|
||||
char path[1200], line[512];
|
||||
FILE *file;
|
||||
int count = 0, loaded;
|
||||
if (!pool) { errno = EINVAL; return -1; }
|
||||
loaded = add_endpoint_list(pool, getenv("DDAT_BOOTSTRAP"));
|
||||
if (loaded < 0) return -1;
|
||||
count += loaded;
|
||||
loaded = add_endpoint_list(pool, getenv("CABLE_BOOTSTRAP"));
|
||||
if (loaded < 0) return -1;
|
||||
count += loaded;
|
||||
if (bootstrap_config_path(path, sizeof(path), 0)) return count;
|
||||
file = fopen(path, "r");
|
||||
if (!file) return errno == ENOENT ? count : -1;
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
line[strcspn(line, "\r\n#")] = '\0';
|
||||
loaded = add_endpoint_list(pool, line);
|
||||
if (loaded < 0) { fclose(file); return -1; }
|
||||
count += loaded;
|
||||
}
|
||||
if (ferror(file)) { fclose(file); return -1; }
|
||||
fclose(file);
|
||||
return count;
|
||||
}
|
||||
|
||||
int cesspool_bootstrap_config_add(const char *endpoint)
|
||||
{
|
||||
char path[1200], copy[512], *separator, *end;
|
||||
unsigned long port;
|
||||
FILE *file;
|
||||
if (!endpoint || strlen(endpoint) >= sizeof(copy)) { errno = EINVAL; return -1; }
|
||||
strcpy(copy, endpoint);
|
||||
separator = strrchr(copy, ':');
|
||||
if (!separator || separator == copy || !separator[1]) { errno = EINVAL; return -1; }
|
||||
*separator++ = '\0'; port = strtoul(separator, &end, 10);
|
||||
if (*end || port == 0 || port > 65535) { errno = EINVAL; return -1; }
|
||||
if (bootstrap_config_path(path, sizeof(path), 1)) return -1;
|
||||
file = fopen(path, "a");
|
||||
if (!file) return -1;
|
||||
{
|
||||
int failed = fprintf(file, "%s:%lu\n", copy, port) < 0;
|
||||
if (fclose(file)) failed = 1;
|
||||
return failed ? -1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
int cesspool_bootstrap_config_set(const char *endpoint)
|
||||
{
|
||||
char path[1200], temporary[1240], copy[512], *separator, *end;
|
||||
unsigned long port;
|
||||
FILE *file;
|
||||
int n, failed;
|
||||
if (!endpoint || strlen(endpoint) >= sizeof(copy)) { errno = EINVAL; return -1; }
|
||||
strcpy(copy, endpoint);
|
||||
separator = strrchr(copy, ':');
|
||||
if (!separator || separator == copy || !separator[1]) { errno = EINVAL; return -1; }
|
||||
*separator++ = '\0';
|
||||
port = strtoul(separator, &end, 10);
|
||||
if (*end || port == 0 || port > 65535) { errno = EINVAL; return -1; }
|
||||
if (bootstrap_config_path(path, sizeof(path), 1)) return -1;
|
||||
n = snprintf(temporary, sizeof(temporary), "%s.tmp", path);
|
||||
if (n < 0 || (size_t)n >= sizeof(temporary)) { errno = ENAMETOOLONG; return -1; }
|
||||
file = fopen(temporary, "w");
|
||||
if (!file) return -1;
|
||||
failed = fprintf(file, "%s:%lu\n", copy, port) < 0;
|
||||
if (fclose(file)) failed = 1;
|
||||
if (failed || chmod(temporary, S_IRUSR | S_IWUSR)) {
|
||||
int saved = errno;
|
||||
(void)remove(temporary);
|
||||
errno = saved;
|
||||
return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!MoveFileExA(temporary, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
(void)remove(temporary);
|
||||
errno = EIO;
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
if (rename(temporary, path)) {
|
||||
int saved = errno;
|
||||
(void)remove(temporary);
|
||||
errno = saved;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cesspool_bootstrap_config_get(char *endpoint, size_t capacity)
|
||||
{
|
||||
char path[1200], line[512];
|
||||
FILE *file;
|
||||
size_t length;
|
||||
if (!endpoint || !capacity) { errno = EINVAL; return -1; }
|
||||
endpoint[0] = '\0';
|
||||
if (bootstrap_config_path(path, sizeof(path), 0)) return -1;
|
||||
file = fopen(path, "r");
|
||||
if (!file) return -1;
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
line[strcspn(line, "\r\n#")] = '\0';
|
||||
if (!line[0]) continue;
|
||||
length = strlen(line);
|
||||
if (length >= capacity) { fclose(file); errno = ENOSPC; return -1; }
|
||||
memcpy(endpoint, line, length + 1);
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
fclose(file);
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cesspool_bootstrap_config_list(char *contents, size_t capacity)
|
||||
{
|
||||
char path[1200];
|
||||
FILE *file;
|
||||
size_t length;
|
||||
if (!contents || !capacity) { errno = EINVAL; return -1; }
|
||||
contents[0] = '\0';
|
||||
if (bootstrap_config_path(path, sizeof(path), 0)) return -1;
|
||||
file = fopen(path, "r");
|
||||
if (!file) return -1;
|
||||
length = fread(contents, 1, capacity - 1, file);
|
||||
if (ferror(file)) { int saved = errno; fclose(file); errno = saved; return -1; }
|
||||
if (!feof(file)) { fclose(file); errno = ENOSPC; return -1; }
|
||||
contents[length] = '\0';
|
||||
return fclose(file) == 0 ? 0 : -1;
|
||||
}
|
||||
int cesspool_bootstrap(cesspool_t *pool){if(!pool||!pool->bootstrap_count){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_PING,NULL,pool->port);}
|
||||
int cesspool_listen(cesspool_t *pool,const char *host,uint16_t port){
|
||||
static const uint16_t defaults[]={DATNET_PORT,3000,3002,3004,2001,2003,2005};
|
||||
struct sockaddr_in bound;socklen_t length=sizeof(bound);size_t i,count;
|
||||
if(!pool){errno=EINVAL;return-1;}
|
||||
count=port?1:sizeof(defaults)/sizeof(defaults[0]);
|
||||
for(i=0;i<count;i++){
|
||||
uint16_t candidate=port?port:defaults[i];
|
||||
if(datnet_bind(&pool->net,host,candidate)==0)break;
|
||||
if(port||errno!=EADDRINUSE)return-1;
|
||||
}
|
||||
if(i==count){errno=EADDRINUSE;return-1;}
|
||||
if(getsockname(pool->net.sock,(struct sockaddr*)&bound,&length)){datnet_close(&pool->net);return-1;}
|
||||
pool->port=ntohs(bound.sin_port);pool->ready=1;return 0;
|
||||
}
|
||||
int cesspool_set_bootstrap_server(cesspool_t *pool, int enabled)
|
||||
{
|
||||
if (!pool || !pool->ready) { errno = EINVAL; return -1; }
|
||||
pool->bootstrap_server = !!enabled;
|
||||
return 0;
|
||||
}
|
||||
int cesspool_announce(cesspool_t *pool,const uint8_t topic[32],uint16_t port){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_ANNOUNCE,topic,port?port:pool->port);}
|
||||
int cesspool_lookup(cesspool_t *pool,const uint8_t topic[32]){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_LOOKUP,topic,0);}
|
||||
int cesspool_unannounce(cesspool_t *pool,const uint8_t topic[32],uint16_t port){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_UNANNOUNCE,topic,port);}
|
||||
int cesspool_ping(cesspool_t *pool,const dat_peer_t *peer){if(!pool||!peer){errno=EINVAL;return-1;}return send_message(pool,peer,CESSPOOL_PING,NULL,pool->port);}
|
||||
int cesspool_holepunch(cesspool_t *pool,const dat_peer_t *peer){if(!pool||!peer){errno=EINVAL;return-1;}return send_message(pool,peer,CESSPOOL_HOLEPUNCH,NULL,pool->port);}
|
||||
int cesspool_poll(cesspool_t *pool,int timeout_ms){if(!pool){errno=EINVAL;return-1;}return datnet_poll(&pool->net,timeout_ms);}
|
||||
cesspool_peer_t *cesspool_find_peer(cesspool_t*p,const uint8_t key[32]){size_t i;if(!p||!key)return NULL;for(i=0;i<p->peer_count;i++)if(!memcmp(p->peers[i].public_key,key,32))return&p->peers[i];return NULL;}
|
||||
int cesspool_add_peer(cesspool_t*p,const cesspool_peer_t*peer){cesspool_peer_t*old;int newly_discovered;if(!p||!peer){errno=EINVAL;return-1;}old=cesspool_find_peer(p,peer->public_key);if(old){newly_discovered=peer->discovered&&(!old->discovered||memcmp(peer->discovery_topic,old->discovery_topic,32));{cesspool_peer_t merged=*peer;if(!peer->discovered&&old->discovered){merged.discovered=1;memcpy(merged.discovery_topic,old->discovery_topic,32);}*old=merged;}if(newly_discovered&&p->on_peer)p->on_peer(p,old);return 0;}if(p->peer_count>=CESSPOOL_MAX_PEERS){errno=ENOSPC;return-1;}p->peers[p->peer_count++]=*peer;if(peer->discovered&&p->on_peer)p->on_peer(p,peer);return 0;}
|
||||
int cesspool_remove_peer(cesspool_t*p,const uint8_t key[32]){size_t i;if(!p||!key){errno=EINVAL;return-1;}for(i=0;i<p->peer_count;i++)if(!memcmp(p->peers[i].public_key,key,32)){p->peers[i]=p->peers[--p->peer_count];return 0;}errno=ENOENT;return-1;}
|
||||
int cesspool_join(cesspool_t *p, const uint8_t topic[32], uint16_t port,
|
||||
int announce, int lookup)
|
||||
{
|
||||
cesspool_topic_t entry;
|
||||
int announced_now = 0;
|
||||
if (!p || !topic || (!announce && !lookup)) { errno = EINVAL; return -1; }
|
||||
if (p->topic_count >= CESSPOOL_MAX_TOPICS) { errno = ENOSPC; return -1; }
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
memcpy(entry.key, topic, CESSPOOL_TOPIC_BYTES);
|
||||
entry.port = port ? port : p->port;
|
||||
entry.announcing = !!announce;
|
||||
entry.looking_up = !!lookup;
|
||||
/* No configured rendezvous falls back to best-effort LAN broadcast. */
|
||||
if (announce) {
|
||||
if (cesspool_announce(p, topic, entry.port)) return -1;
|
||||
announced_now = 1;
|
||||
}
|
||||
if (lookup && cesspool_lookup(p, topic)) {
|
||||
if (announced_now) (void)cesspool_unannounce(p, topic, entry.port);
|
||||
return -1;
|
||||
}
|
||||
p->topics[p->topic_count++] = entry;
|
||||
return 0;
|
||||
}
|
||||
int cesspool_leave(cesspool_t*p,const uint8_t topic[32]){size_t i;if(!p||!topic){errno=EINVAL;return-1;}for(i=0;i<p->topic_count;i++)if(!memcmp(p->topics[i].key,topic,32)){if(p->topics[i].announcing)cesspool_unannounce(p,topic,p->topics[i].port);p->topics[i]=p->topics[--p->topic_count];return 0;}errno=ENOENT;return-1;}
|
||||
int cesspool_tick(cesspool_t *p, uint64_t now)
|
||||
{
|
||||
size_t i = 0;
|
||||
uint64_t wall_now;
|
||||
if (!p) { errno = EINVAL; return -1; }
|
||||
for (i = 0; i < p->topic_count; i++) if (now >= p->topics[i].refresh_at) {
|
||||
if (p->topics[i].announcing) (void)cesspool_announce(p, p->topics[i].key, p->topics[i].port);
|
||||
if (p->topics[i].looking_up) (void)cesspool_lookup(p, p->topics[i].key);
|
||||
p->topics[i].refresh_at = now + 240000u;
|
||||
}
|
||||
if (!p->bootstrap_server && p->bootstrap_count && now >= p->bootstrap_probe_at) {
|
||||
int probed = 0;
|
||||
for (i = 0; i < p->topic_count; i++) if (p->topics[i].looking_up) {
|
||||
(void)cesspool_lookup(p, p->topics[i].key);
|
||||
probed = 1;
|
||||
}
|
||||
if (!probed) (void)cesspool_bootstrap(p);
|
||||
p->bootstrap_probe_at = now + CESSPOOL_BOOTSTRAP_PROBE_MS;
|
||||
}
|
||||
for (i = 0; i < CESSPOOL_MAX_TRANSACTIONS; i++)
|
||||
if (p->transactions[i].active && now >= p->transactions[i].expires_at)
|
||||
p->transactions[i].active = 0;
|
||||
i = 0;
|
||||
wall_now = monotonic_ms();
|
||||
if (p->bootstrap_count && p->bootstrap_last_seen && wall_now >= p->bootstrap_last_seen)
|
||||
p->bootstrap_reachable = wall_now - p->bootstrap_last_seen < CESSPOOL_BOOTSTRAP_TIMEOUT_MS;
|
||||
else if (p->bootstrap_count) p->bootstrap_reachable = 0;
|
||||
while (i < p->announcement_count) {
|
||||
cesspool_announcement_t *entry = &p->announcements[i];
|
||||
if (!entry->last_seen || (wall_now >= entry->last_seen &&
|
||||
wall_now - entry->last_seen >= CESSPOOL_PEER_TTL_MS)) {
|
||||
p->announcements[i] = p->announcements[--p->announcement_count];
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
/* Client peer rows are a view of live topic announcements. Without
|
||||
* reconciling this bit after pruning, a peer remains "discovered" forever
|
||||
* even though its rendezvous record has expired. */
|
||||
for (i = 0; i < p->peer_count; i++) {
|
||||
cesspool_peer_t *peer = &p->peers[i];
|
||||
size_t announcement;
|
||||
peer->discovered = 0;
|
||||
for (announcement = 0; announcement < p->announcement_count; announcement++) {
|
||||
const cesspool_announcement_t *entry = &p->announcements[announcement];
|
||||
if (memcmp(peer->public_key, entry->peer_id, CESSPOOL_PEER_BYTES)) continue;
|
||||
peer->discovered = 1;
|
||||
memcpy(peer->discovery_topic, entry->topic, CESSPOOL_TOPIC_BYTES);
|
||||
if (entry->last_seen > peer->last_seen) peer->last_seen = entry->last_seen;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p->bootstrap_server) {
|
||||
i = 0;
|
||||
while (i < p->peer_count) {
|
||||
cesspool_peer_t *peer = &p->peers[i];
|
||||
if (!peer->last_seen || (wall_now >= peer->last_seen &&
|
||||
wall_now - peer->last_seen >= CESSPOOL_PEER_TTL_MS)) {
|
||||
p->peers[i] = p->peers[--p->peer_count];
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int distance_less(const uint8_t*a,const uint8_t*b,const uint8_t target[32]){size_t i;for(i=0;i<32;i++){uint8_t da=a[i]^target[i],db=b[i]^target[i];if(da!=db)return da<db;}return 0;}
|
||||
size_t cesspool_closest(cesspool_t*p,const uint8_t target[32],cesspool_peer_t**out,size_t cap){size_t i,j,n=0;if(!p||!target||(!out&&cap))return 0;for(i=0;i<p->peer_count;i++){if(n<cap)out[n++]=&p->peers[i];else if(cap&&distance_less(p->peers[i].public_key,out[n-1]->public_key,target))out[n-1]=&p->peers[i];for(j=n;j>1&&distance_less(out[j-1]->public_key,out[j-2]->public_key,target);j--){cesspool_peer_t*t=out[j-1];out[j-1]=out[j-2];out[j-2]=t;}}return n;}
|
||||
size_t cesspool_topic_peers(cesspool_t *p, const uint8_t topic[32],
|
||||
cesspool_peer_t **out, size_t cap)
|
||||
{
|
||||
size_t i, n = 0;
|
||||
if (!p || !topic || (!out && cap)) return 0;
|
||||
for (i = 0; i < p->announcement_count && n < cap; i++) {
|
||||
cesspool_announcement_t *entry = &p->announcements[i];
|
||||
cesspool_peer_t *peer;
|
||||
size_t j;
|
||||
if (memcmp(entry->topic, topic, 32)) continue;
|
||||
for (j = 0; j < n; j++) if (!memcmp(out[j]->public_key, entry->peer_id, 32)) break;
|
||||
if (j < n) continue;
|
||||
peer = cesspool_find_peer(p, entry->peer_id);
|
||||
if (peer) out[n++] = peer;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t cesspool_announced_peer_count(const cesspool_t *p)
|
||||
{
|
||||
size_t i, j, count = 0;
|
||||
if (!p) return 0;
|
||||
for (i = 0; i < p->announcement_count; i++) {
|
||||
for (j = 0; j < i; j++)
|
||||
if (!memcmp(p->announcements[i].peer_id, p->announcements[j].peer_id, 32)) break;
|
||||
if (j == i) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t cesspool_announced_topic_count(const cesspool_t *p)
|
||||
{
|
||||
size_t i, j, count = 0;
|
||||
if (!p) return 0;
|
||||
for (i = 0; i < p->announcement_count; i++) {
|
||||
for (j = 0; j < i; j++)
|
||||
if (!memcmp(p->announcements[i].topic, p->announcements[j].topic, 32)) break;
|
||||
if (j == i) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
int cesspool_transaction_begin(cesspool_t*p,uint8_t type,const uint8_t target[32],uint64_t expires,uint16_t*out){size_t i,j,attempt;uint16_t candidate;if(!p||!target||!out){errno=EINVAL;return-1;}for(i=0;i<CESSPOOL_MAX_TRANSACTIONS;i++)if(!p->transactions[i].active){for(attempt=0;attempt<UINT16_MAX;attempt++){candidate=++p->next_transaction;if(!candidate)candidate=++p->next_transaction;for(j=0;j<CESSPOOL_MAX_TRANSACTIONS;j++)if(p->transactions[j].active&&p->transactions[j].id==candidate)break;if(j==CESSPOOL_MAX_TRANSACTIONS){cesspool_transaction_t*t=&p->transactions[i];memset(t,0,sizeof(*t));t->active=1;t->type=type;t->id=candidate;t->expires_at=expires;memcpy(t->target,target,32);*out=t->id;return 0;}}errno=ENOSPC;return-1;}errno=ENOSPC;return-1;}
|
||||
int cesspool_transaction_finish(cesspool_t*p,uint16_t id){size_t i;if(!p){errno=EINVAL;return-1;}for(i=0;i<CESSPOOL_MAX_TRANSACTIONS;i++)if(p->transactions[i].active&&p->transactions[i].id==id){p->transactions[i].active=0;return 0;}errno=ENOENT;return-1;}
|
||||
int cesspool_destroy(cesspool_t *pool){if(!pool){errno=EINVAL;return-1;}pool->ready=0;return datnet_close(&pool->net);}
|
||||
@@ -0,0 +1,235 @@
|
||||
#include "dat.h"
|
||||
#include "base32768.h"
|
||||
#include "sleep.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include "sleep_proto.h"
|
||||
#include "sleep_writer.h"
|
||||
#include "sleep_feed.h"
|
||||
#include "errno_compat.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sodium.h>
|
||||
|
||||
int dat_key_to_hex(const uint8_t key[DAT_KEY_BYTES], char hex[65])
|
||||
{
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
size_t i;
|
||||
if (!key || !hex) { errno = EINVAL; return -1; }
|
||||
for (i = 0; i < DAT_KEY_BYTES; i++) {
|
||||
hex[i * 2] = digits[key[i] >> 4];
|
||||
hex[i * 2 + 1] = digits[key[i] & 15];
|
||||
}
|
||||
hex[64] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_key_to_url(const uint8_t key[DAT_KEY_BYTES], char url[DAT_URL_BYTES])
|
||||
{
|
||||
if (!key || !url) { errno = EINVAL; return -1; }
|
||||
memcpy(url, "dat://", 6);
|
||||
if (dat_key_to_hex(key, url + 6)) return -1;
|
||||
return 0;
|
||||
}
|
||||
int dat_key_to_compact_url(const uint8_t key[DAT_KEY_BYTES], char url[DAT_URL_BYTES])
|
||||
{
|
||||
if (!key || !url) { errno = EINVAL; return -1; }
|
||||
memcpy(url, "dat://", 6);
|
||||
if (base32768_encode(key, DAT_KEY_BYTES, url + 6, DAT_URL_BYTES - 6)) return -1;
|
||||
return 0;
|
||||
}
|
||||
static int hex_value(char c){if(c>='0'&&c<='9')return c-'0';if(c>='a'&&c<='f')return c-'a'+10;if(c>='A'&&c<='F')return c-'A'+10;return-1;}
|
||||
int dat_url_to_key(const char *url, uint8_t key[DAT_KEY_BYTES])
|
||||
{
|
||||
size_t i, length, decoded_length = 0;
|
||||
int hi, lo;
|
||||
if (!url || !key) { errno = EINVAL; return -1; }
|
||||
if (!strncmp(url, "dat://", 6)) url += 6;
|
||||
length = strlen(url);
|
||||
if (length == 64) {
|
||||
for (i = 0; i < DAT_KEY_BYTES; i++) {
|
||||
hi = hex_value(url[i * 2]); lo = hex_value(url[i * 2 + 1]);
|
||||
if (hi < 0 || lo < 0) { errno = EINVAL; return -1; }
|
||||
key[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (base32768_decode(url, key, DAT_KEY_BYTES, &decoded_length) ||
|
||||
decoded_length != DAT_KEY_BYTES) { errno = EINVAL; return -1; }
|
||||
return 0;
|
||||
}
|
||||
static int dat_link_format_with(const dat_link_t*l,char*out,size_t cap,int compact){char base[DAT_URL_BYTES];int n;if(!l||!out){errno=EINVAL;return-1;}if((compact?dat_key_to_compact_url(l->key,base):dat_key_to_url(l->key,base)))return-1;if(l->has_version)n=snprintf(out,cap,"%s+%llu%s%s",base,(unsigned long long)l->version,*l->path&&l->path[0]!='/'?"/":"",l->path);else n=snprintf(out,cap,"%s%s%s",base,*l->path&&l->path[0]!='/'?"/":"",l->path);if(n<0||(size_t)n>=cap){errno=ENOSPC;return-1;}return 0;}
|
||||
int dat_link_format(const dat_link_t*l,char*out,size_t cap){return dat_link_format_with(l,out,cap,0);}
|
||||
int dat_link_format_compact(const dat_link_t*l,char*out,size_t cap){return dat_link_format_with(l,out,cap,1);}
|
||||
int dat_link_parse(const char *text, dat_link_t *link)
|
||||
{
|
||||
const char *p, *suffix, *plus;
|
||||
char encoded[DAT_URL_BYTES], version[32];
|
||||
size_t key_length, n;
|
||||
if (!text || !link) { errno = EINVAL; return -1; }
|
||||
memset(link, 0, sizeof(*link));
|
||||
p = !strncmp(text, "dat://", 6) ? text + 6 : text;
|
||||
suffix = strpbrk(p, "/+?#");
|
||||
key_length = suffix ? (size_t)(suffix - p) : strlen(p);
|
||||
if (!key_length || key_length >= sizeof(encoded)) { errno = EINVAL; return -1; }
|
||||
memcpy(encoded, p, key_length); encoded[key_length] = '\0';
|
||||
if (dat_url_to_key(encoded, link->key)) return -1;
|
||||
suffix = p + key_length;
|
||||
plus = *suffix == '+' ? suffix : NULL;
|
||||
if (plus) {
|
||||
const char *end = plus + 1;
|
||||
while (*end >= '0' && *end <= '9') end++;
|
||||
n = (size_t)(end - (plus + 1));
|
||||
if (!n || n >= sizeof(version)) { errno = EINVAL; return -1; }
|
||||
memcpy(version, plus + 1, n); version[n] = '\0';
|
||||
link->version = (uint64_t)strtoull(version, NULL, 10);
|
||||
link->has_version = 1; suffix = end;
|
||||
}
|
||||
if (*suffix && *suffix != '/') { errno = EINVAL; return -1; }
|
||||
if (*suffix == '/') suffix++;
|
||||
if (strlen(suffix) >= sizeof(link->path)) { errno = ENAMETOOLONG; return -1; }
|
||||
strcpy(link->path, suffix);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_archive_create(dat_archive_t *archive, const char *root)
|
||||
{
|
||||
sleep_keypair_t metadata_keys, content_keys;
|
||||
sleep_metadata_header_t header;
|
||||
uint8_t metadata_block[128], empty = 0;
|
||||
const uint8_t *metadata_blocks[1], *content_blocks[1];
|
||||
size_t metadata_lengths[1], content_lengths[1] = { 0 };
|
||||
size_t metadata_len;
|
||||
int n;
|
||||
|
||||
if (!archive || !root || !*root) { errno = EINVAL; return -1; }
|
||||
memset(archive, 0, sizeof(*archive));
|
||||
n = snprintf(archive->dat_path, sizeof(archive->dat_path), "%s/.dat", root);
|
||||
if (n < 0 || (size_t)n >= sizeof(archive->dat_path)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (sleep_init(root) || sleep_crypto_init() || sleep_gen_keypair(&metadata_keys) || sleep_gen_keypair(&content_keys)) return -1;
|
||||
|
||||
memset(&header, 0, sizeof(header));
|
||||
strcpy(header.type, "hyperdrive");
|
||||
memcpy(header.content, content_keys.pubkey, 32); header.content_len = 32;
|
||||
if (sleep_metadata_header_encode(&header, metadata_block, sizeof(metadata_block), &metadata_len)) return -1;
|
||||
|
||||
metadata_blocks[0] = metadata_block; metadata_lengths[0] = metadata_len;
|
||||
content_blocks[0] = ∅
|
||||
if (sleep_write_feed(archive->dat_path, "metadata", metadata_blocks, metadata_lengths, 1, &metadata_keys)) return -1;
|
||||
if (sleep_write_feed(archive->dat_path, "content", content_blocks, content_lengths, 1, &content_keys)) return -1;
|
||||
memcpy(archive->metadata_key, metadata_keys.pubkey, 32);
|
||||
memcpy(archive->content_key, content_keys.pubkey, 32);
|
||||
archive->metadata_keys = metadata_keys;
|
||||
archive->content_keys = content_keys;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_archive_open(dat_archive_t*a,const char*root){char path[1200];int n;if(!a||!root){errno=EINVAL;return-1;}memset(a,0,sizeof(*a));n=snprintf(a->dat_path,sizeof(a->dat_path),"%s/.dat",root);if(n<0||(size_t)n>=sizeof(a->dat_path)){errno=ENAMETOOLONG;return-1;}snprintf(path,sizeof(path),"%s/metadata.key",a->dat_path);if(sleep_load_pubkey(path,a->metadata_key))return-1;snprintf(path,sizeof(path),"%s/content.key",a->dat_path);return sleep_load_pubkey(path,a->content_key);}
|
||||
/* Dat Desktop's dat-secret-storage indexes each Hypercore secret by the
|
||||
* discovery key and stores the raw 64-byte Ed25519 secret at that path. */
|
||||
static int feed_secret_path(char *out,size_t cap,const char *root,const uint8_t pub[32])
|
||||
{
|
||||
uint8_t disc[32]; char hex[65], shard[1200]; int n;
|
||||
if(sleep_compute_discovery_key(pub,disc)||dat_key_to_hex(disc,hex))return -1;
|
||||
n=snprintf(shard,sizeof(shard),"%s/%c%c",root,hex[0],hex[1]);
|
||||
if(n<0||(size_t)n>=sizeof(shard)){errno=ENAMETOOLONG;return -1;}
|
||||
n=snprintf(out,cap,"%s/%s",shard,hex+2);
|
||||
if(n<0||(size_t)n>=cap){errno=ENAMETOOLONG;return -1;} return 0;
|
||||
}
|
||||
|
||||
static int load_feed_secret(const char *root,const uint8_t pub[32],sleep_keypair_t *kp)
|
||||
{
|
||||
char path[1400]; FILE *f; uint8_t raw[sizeof(*kp)]; size_t n; int saved;
|
||||
if(feed_secret_path(path,sizeof(path),root,pub))return -1;
|
||||
f=fopen(path,"rb");
|
||||
if(f){n=fread(raw,1,sizeof(raw),f);if(ferror(f)){saved=errno;fclose(f);errno=saved;return -1;}fclose(f);
|
||||
if(n==SLEEP_SECRET_KEY_BYTES){memcpy(kp->seckey,raw,n);if(crypto_sign_ed25519_sk_to_pk(kp->pubkey,kp->seckey)!=0||memcmp(kp->pubkey,pub,32)){errno=EKEYREJECTED;return -1;}return 0;}
|
||||
errno=EINVAL;return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int save_feed_secret(const char *root,const sleep_keypair_t *kp)
|
||||
{
|
||||
char path[1400], parent[1400], *slash; FILE *f; int fd, saved;
|
||||
if(feed_secret_path(path,sizeof(path),root,kp->pubkey))return -1;
|
||||
strcpy(parent,path); slash=strrchr(parent,'/'); if(!slash){errno=EINVAL;return -1;} *slash='\0';
|
||||
if(mkdir(parent,0700)&&errno!=EEXIST)return -1;
|
||||
f=fopen(path,"rb"); if(f){uint8_t raw[sizeof(*kp)];size_t n=fread(raw,1,sizeof(raw),f);fclose(f);
|
||||
if(n==SLEEP_SECRET_KEY_BYTES&&!memcmp(raw,kp->seckey,SLEEP_SECRET_KEY_BYTES))return 0;
|
||||
errno=EEXIST;return -1;
|
||||
}
|
||||
fd=open(path,O_WRONLY|O_CREAT|O_EXCL,0600);if(fd<0){if(errno==EEXIST)return save_feed_secret(root,kp);return -1;}
|
||||
if(write(fd,kp->seckey,SLEEP_SECRET_KEY_BYTES)!=(ssize_t)SLEEP_SECRET_KEY_BYTES){saved=errno;close(fd);unlink(path);errno=saved?saved:EIO;return -1;}
|
||||
if(close(fd)){saved=errno;unlink(path);errno=saved;return -1;}return 0;
|
||||
}
|
||||
|
||||
int dat_archive_open_writable(dat_archive_t*a,const char*root,const char*secrets)
|
||||
{
|
||||
if(dat_archive_open(a,root))return -1;
|
||||
if(!secrets){errno=EINVAL;return -1;}
|
||||
if(load_feed_secret(secrets,a->metadata_key,&a->metadata_keys)||
|
||||
load_feed_secret(secrets,a->content_key,&a->content_keys))return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_archive_save_secrets(const dat_archive_t*a,const char*secrets)
|
||||
{
|
||||
if(!a||!secrets){errno=EINVAL;return -1;}
|
||||
if(mkdir(secrets,0700)&&errno!=EEXIST)return -1;
|
||||
if(save_feed_secret(secrets,&a->metadata_keys))return -1;
|
||||
return save_feed_secret(secrets,&a->content_keys);
|
||||
}
|
||||
void dat_archive_close(dat_archive_t*a){if(a)memset(a,0,sizeof(*a));}
|
||||
size_t dat_archive_entry_count(const dat_archive_t*a){sleep_feed_t f;size_t n=0;if(a&&sleep_feed_open(&f,a->dat_path,"metadata",NULL)==0){n=sleep_feed_length(&f);sleep_feed_close(&f);}return n?n-1:0;}
|
||||
uint64_t dat_archive_version(const dat_archive_t*a){return (uint64_t)dat_archive_entry_count(a);}
|
||||
|
||||
int dat_archive_append_file(dat_archive_t *a,const char *path,const uint8_t *data,size_t length,uint32_t mode,uint64_t mtime)
|
||||
{
|
||||
sleep_feed_t content,metadata;sleep_metadata_entry_t entry;uint8_t encoded[2300];size_t encoded_len,block,off=0,count=0,part;
|
||||
if(!a||!path||(!data&&length)||strlen(path)>=sizeof(entry.path)){errno=EINVAL;return-1;}
|
||||
if(sleep_feed_open(&content,a->dat_path,"content",&a->content_keys))return-1;
|
||||
block=sleep_feed_length(&content);do{part=length-off>DAT_BLOCK_BYTES?DAT_BLOCK_BYTES:length-off;if(sleep_feed_append(&content,data?data+off:NULL,part)){sleep_feed_close(&content);return-1;}off+=part;count++;}while(off<length);if(sleep_feed_flush(&content)){sleep_feed_close(&content);return-1;}sleep_feed_close(&content);
|
||||
memset(&entry,0,sizeof(entry));strcpy(entry.path,path);entry.type=SLEEP_ENTRY_FILE;entry.mode=mode;entry.size=length;entry.mtime=mtime;entry.block_offset=block;entry.block_count=count;
|
||||
if(sleep_metadata_entry_encode(&entry,encoded,sizeof(encoded),&encoded_len))return-1;
|
||||
if(sleep_feed_open(&metadata,a->dat_path,"metadata",&a->metadata_keys))return-1;
|
||||
if(sleep_feed_append(&metadata,encoded,encoded_len)||sleep_feed_flush(&metadata)){sleep_feed_close(&metadata);return-1;}sleep_feed_close(&metadata);return 0;
|
||||
}
|
||||
int dat_archive_read_entry(const dat_archive_t *a,size_t index,sleep_metadata_entry_t *entry)
|
||||
{
|
||||
sleep_feed_t f;uint8_t *data;size_t len;int rc;if(!a||!entry){errno=EINVAL;return-1;}if(sleep_feed_open(&f,a->dat_path,"metadata",NULL))return-1;
|
||||
/* Metadata block zero is the archive header. */ rc=sleep_feed_read(&f,index+1,&data,&len);sleep_feed_close(&f);if(rc)return-1;rc=sleep_metadata_entry_decode(entry,data,len);free(data);return rc;
|
||||
}
|
||||
int dat_archive_read_file(const dat_archive_t *a,const sleep_metadata_entry_t *entry,uint8_t **data,size_t *length)
|
||||
{
|
||||
sleep_feed_t f;uint8_t*out,*block;size_t total=0,n,i;if(!a||!entry||!data||!length||entry->type!=SLEEP_ENTRY_FILE){errno=EINVAL;return-1;}if(entry->size>SIZE_MAX){errno=EOVERFLOW;return-1;}out=malloc(entry->size?(size_t)entry->size:1);if(!out)return-1;if(sleep_feed_open(&f,a->dat_path,"content",NULL)){free(out);return-1;}for(i=0;i<entry->block_count;i++){if(sleep_feed_read(&f,(size_t)entry->block_offset+i,&block,&n)||n>(size_t)entry->size-total){sleep_feed_close(&f);free(out);return-1;}if(n)memcpy(out+total,block,n);total+=n;free(block);}sleep_feed_close(&f);if(total!=(size_t)entry->size){free(out);errno=EPROTO;return-1;}*data=out;*length=total;return 0;
|
||||
}
|
||||
|
||||
static int append_metadata_only(dat_archive_t *a,const sleep_metadata_entry_t *entry)
|
||||
{
|
||||
sleep_feed_t metadata;uint8_t encoded[2300];size_t len;int rc;
|
||||
if(sleep_metadata_entry_encode(entry,encoded,sizeof(encoded),&len))return-1;
|
||||
if(sleep_feed_open(&metadata,a->dat_path,"metadata",&a->metadata_keys))return-1;
|
||||
rc=sleep_feed_append(&metadata,encoded,len);if(!rc)rc=sleep_feed_flush(&metadata);sleep_feed_close(&metadata);return rc;
|
||||
}
|
||||
int dat_archive_append_directory(dat_archive_t *a,const char *path,uint32_t mode,uint64_t mtime)
|
||||
{
|
||||
sleep_metadata_entry_t e;if(!a||!path||strlen(path)>=sizeof(e.path)){errno=EINVAL;return-1;}memset(&e,0,sizeof(e));strcpy(e.path,path);e.type=SLEEP_ENTRY_DIRECTORY;e.mode=mode;e.mtime=mtime;return append_metadata_only(a,&e);
|
||||
}
|
||||
int dat_archive_append_symlink(dat_archive_t *a,const char *path,const char *target,uint64_t mtime)
|
||||
{
|
||||
sleep_metadata_entry_t e;if(!a||!path||!target||strlen(path)>=sizeof(e.path)||strlen(target)>=sizeof(e.linkname)){errno=EINVAL;return-1;}memset(&e,0,sizeof(e));strcpy(e.path,path);strcpy(e.linkname,target);e.type=SLEEP_ENTRY_SYMLINK;e.mtime=mtime;return append_metadata_only(a,&e);
|
||||
}
|
||||
int dat_archive_delete(dat_archive_t*a,const char*path,uint64_t mtime){sleep_metadata_entry_t e;if(!a||!path||strlen(path)>=sizeof(e.path)){errno=EINVAL;return-1;}memset(&e,0,sizeof(e));strcpy(e.path,path);e.type=SLEEP_ENTRY_DELETE;e.mtime=mtime;return append_metadata_only(a,&e);}
|
||||
int dat_archive_find(const dat_archive_t*a,const char*path,sleep_metadata_entry_t*entry){size_t n,i;if(!a||!path||!entry){errno=EINVAL;return-1;}n=dat_archive_entry_count(a);for(i=n;i>0;i--){if(dat_archive_read_entry(a,i-1,entry)==0&&!strcmp(entry->path,path)){if(entry->type==SLEEP_ENTRY_DELETE){errno=ENOENT;return-1;}return 0;}}errno=ENOENT;return-1;}
|
||||
int dat_archive_find_at(const dat_archive_t*a,const char*path,uint64_t version,sleep_metadata_entry_t*entry){size_t n,i;if(!a||!path||!entry){errno=EINVAL;return-1;}n=dat_archive_entry_count(a);if(version>n){errno=ERANGE;return-1;}for(i=(size_t)version;i>0;i--){if(dat_archive_read_entry(a,i-1,entry)==0&&!strcmp(entry->path,path)){if(entry->type==SLEEP_ENTRY_DELETE){errno=ENOENT;return-1;}return 0;}}errno=ENOENT;return-1;}
|
||||
int dat_checkout_init(dat_checkout_t*c,const dat_archive_t*a,uint64_t version){if(!c||!a||version>dat_archive_version(a)){errno=ERANGE;return-1;}c->archive=a;c->version=version;return 0;}
|
||||
int dat_checkout_find(const dat_checkout_t*c,const char*path,sleep_metadata_entry_t*entry){if(!c){errno=EINVAL;return-1;}return dat_archive_find_at(c->archive,path,c->version,entry);}
|
||||
int dat_checkout_read(const dat_checkout_t*c,const sleep_metadata_entry_t*entry,uint8_t**data,size_t*length){if(!c){errno=EINVAL;return-1;}return dat_archive_read_file(c->archive,entry,data,length);}
|
||||
int dat_checkout_export(const dat_checkout_t*c,const char*archive_path,const char*filesystem_path){if(!c){errno=EINVAL;return-1;}return dat_archive_export_at(c->archive,c->version,archive_path,filesystem_path);}
|
||||
int dat_archive_changes(const dat_archive_t*a,uint64_t from,uint64_t to,dat_archive_change_fn callback,void*context){sleep_metadata_entry_t entry;uint64_t version,last;if(!a||!callback){errno=EINVAL;return-1;}last=dat_archive_version(a);if(from>to||to>last){errno=ERANGE;return-1;}for(version=from+1;version<=to;version++){if(dat_archive_read_entry(a,(size_t)(version-1),&entry)||callback(&entry,version,context))return-1;}return 0;}
|
||||
@@ -0,0 +1,314 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "dat.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
static int normalize_url_path(const char *src, char *dst, size_t capacity)
|
||||
{
|
||||
char decoded[2048];
|
||||
size_t i = 0, decoded_len = 0, out = 1, segments[1024], count = 0;
|
||||
int trailing_slash;
|
||||
if (!src || !dst || capacity < 2 || strlen(src) >= sizeof(decoded)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
while (src[i]) {
|
||||
unsigned char c = (unsigned char)src[i++];
|
||||
if (c == '%') {
|
||||
int hi, lo;
|
||||
if (!src[i] || !src[i + 1]) { errno = EINVAL; return -1; }
|
||||
hi = isdigit((unsigned char)src[i]) ? src[i] - '0' :
|
||||
(tolower((unsigned char)src[i]) >= 'a' && tolower((unsigned char)src[i]) <= 'f' ? tolower((unsigned char)src[i]) - 'a' + 10 : -1);
|
||||
lo = isdigit((unsigned char)src[i + 1]) ? src[i + 1] - '0' :
|
||||
(tolower((unsigned char)src[i + 1]) >= 'a' && tolower((unsigned char)src[i + 1]) <= 'f' ? tolower((unsigned char)src[i + 1]) - 'a' + 10 : -1);
|
||||
if (hi < 0 || lo < 0) { errno = EINVAL; return -1; }
|
||||
c = (unsigned char)((hi << 4) | lo); i += 2;
|
||||
}
|
||||
if (!c || c == '\\') { errno = EINVAL; return -1; }
|
||||
decoded[decoded_len++] = (char)c;
|
||||
}
|
||||
decoded[decoded_len] = '\0';
|
||||
trailing_slash = decoded_len == 0 || decoded[decoded_len - 1] == '/';
|
||||
dst[0] = '/';
|
||||
i = 0;
|
||||
while (i < decoded_len) {
|
||||
size_t start, n;
|
||||
while (i < decoded_len && decoded[i] == '/') i++;
|
||||
start = i;
|
||||
while (i < decoded_len && decoded[i] != '/') i++;
|
||||
n = i - start;
|
||||
if (!n) break;
|
||||
if (n == 1 && decoded[start] == '.') { trailing_slash = 1; continue; }
|
||||
if (n == 2 && decoded[start] == '.' && decoded[start + 1] == '.') {
|
||||
trailing_slash = 1;
|
||||
if (count) out = segments[--count];
|
||||
continue;
|
||||
}
|
||||
trailing_slash = 0;
|
||||
if (count >= sizeof(segments) / sizeof(segments[0]) ||
|
||||
out + n + 2 > capacity) { errno = ENAMETOOLONG; return -1; }
|
||||
segments[count++] = out;
|
||||
if (out > 1) dst[out++] = '/';
|
||||
memcpy(dst + out, decoded + start, n);
|
||||
out += n;
|
||||
}
|
||||
if (trailing_slash && out > 1) dst[out++] = '/';
|
||||
if (out >= capacity) { errno = ENAMETOOLONG; return -1; }
|
||||
dst[out] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_decimal(const char *s, uint64_t *value)
|
||||
{
|
||||
char *end;
|
||||
unsigned long long v;
|
||||
if (!s || !*s || !value) return -1;
|
||||
errno = 0; v = strtoull(s, &end, 10);
|
||||
if (errno || *end) return -1;
|
||||
*value = (uint64_t)v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_url_parse(const char *text, dat_url_t *url)
|
||||
{
|
||||
const char *authority, *path, *query, *end;
|
||||
size_t host_len, path_len;
|
||||
char query_copy[512], raw_path[2048], *item, *save;
|
||||
char *plus;
|
||||
if (!text || !url || strncmp(text, "dat://", 6)) { errno = EINVAL; return -1; }
|
||||
memset(url, 0, sizeof(*url));
|
||||
authority = text + 6;
|
||||
path = strpbrk(authority, "/?");
|
||||
end = path ? path : authority + strlen(authority);
|
||||
host_len = (size_t)(end - authority);
|
||||
if (!host_len || host_len >= sizeof(url->host)) { errno = EINVAL; return -1; }
|
||||
memcpy(url->host, authority, host_len); url->host[host_len] = 0;
|
||||
plus = strchr(url->host, '+');
|
||||
if (plus) {
|
||||
if (parse_decimal(plus + 1, &url->version)) { errno = EINVAL; return -1; }
|
||||
*plus = 0; url->has_version = 1;
|
||||
if (!url->host[0]) { errno = EINVAL; return -1; }
|
||||
}
|
||||
query = (*end == '?') ? end : strchr(end, '?');
|
||||
if (*end == '/') {
|
||||
path_len = query ? (size_t)(query - end) : strlen(end);
|
||||
if (path_len >= sizeof(raw_path)) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(raw_path, end, path_len); raw_path[path_len] = 0;
|
||||
} else { strcpy(raw_path, "/"); path_len = 1; }
|
||||
if (path_len >= 2 && raw_path[path_len - 1] == '/' && raw_path[path_len - 2] == '/')
|
||||
url->force_directory_listing = 1;
|
||||
if (normalize_url_path(raw_path, url->path, sizeof(url->path))) return -1;
|
||||
if (query) {
|
||||
if (strlen(query + 1) >= sizeof(query_copy)) { errno = EOVERFLOW; return -1; }
|
||||
strcpy(query_copy, query + 1);
|
||||
for (item = strtok_r(query_copy, "&", &save); item; item = strtok_r(NULL, "&", &save)) {
|
||||
char *equals = strchr(item, '=');
|
||||
if (equals) *equals++ = 0;
|
||||
if (!strcmp(item, "version")) {
|
||||
if (parse_decimal(equals, &url->version)) { errno = EINVAL; return -1; }
|
||||
if (url->has_version && url->version != (uint64_t)strtoull(equals, NULL, 10)) { errno = EINVAL; return -1; }
|
||||
url->has_version = 1;
|
||||
} else if (!strcmp(item, "stat")) {
|
||||
url->wants_stat = !equals || strcmp(equals, "0") != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int canonical_entry_path(const char *input, char *out, size_t capacity)
|
||||
{
|
||||
const char *p = input;
|
||||
size_t n;
|
||||
while (*p == '/') p++;
|
||||
n = strlen(p);
|
||||
while (n && p[n - 1] == '/') n--;
|
||||
if (n >= capacity) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(out, p, n); out[n] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int find_normalized(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, sleep_metadata_entry_t *entry)
|
||||
{
|
||||
size_t i;
|
||||
for (i = (size_t)version; i > 0; i--) {
|
||||
sleep_metadata_entry_t current;
|
||||
const char *candidate;
|
||||
if (dat_archive_read_entry(archive, i - 1, ¤t)) return -1;
|
||||
candidate = current.path; while (*candidate == '/') candidate++;
|
||||
if (strcmp(candidate, path)) continue;
|
||||
if (current.type == SLEEP_ENTRY_DELETE) { errno = ENOENT; return -1; }
|
||||
*entry = current;
|
||||
return 0;
|
||||
}
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dat_archive_stat_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, dat_path_info_t *info)
|
||||
{
|
||||
char wanted[2048];
|
||||
size_t i;
|
||||
if (!archive || !path || !info || canonical_entry_path(path, wanted, sizeof(wanted))) { errno = EINVAL; return -1; }
|
||||
if (version > dat_archive_version(archive)) { errno = ERANGE; return -1; }
|
||||
memset(info, 0, sizeof(*info));
|
||||
if (find_normalized(archive, version, wanted, &info->entry) == 0) {
|
||||
info->exists = 1;
|
||||
info->is_directory = info->entry.type == SLEEP_ENTRY_DIRECTORY;
|
||||
return 0;
|
||||
}
|
||||
if (errno != ENOENT) return -1;
|
||||
for (i = 0; i < (size_t)version; i++) {
|
||||
sleep_metadata_entry_t e, latest;
|
||||
const char *candidate;
|
||||
size_t prefix = strlen(wanted);
|
||||
if (dat_archive_read_entry(archive, i, &e)) return -1;
|
||||
candidate = e.path; while (*candidate == '/') candidate++;
|
||||
if (prefix && (strncmp(candidate, wanted, prefix) || candidate[prefix] != '/')) continue;
|
||||
if (!prefix && !*candidate) continue;
|
||||
if (find_normalized(archive, version, candidate, &latest)) {
|
||||
if (errno == ENOENT) continue;
|
||||
return -1;
|
||||
}
|
||||
if (latest.type == SLEEP_ENTRY_DELETE) continue;
|
||||
memset(info, 0, sizeof(*info));
|
||||
strcpy(info->entry.path, wanted);
|
||||
info->entry.type = SLEEP_ENTRY_DIRECTORY;
|
||||
info->exists = 1; info->is_directory = 1;
|
||||
return 0;
|
||||
}
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dat_archive_readdir_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, dat_archive_readdir_fn callback,
|
||||
void *context)
|
||||
{
|
||||
char wanted[2048];
|
||||
size_t i, prefix, seen_count = 0;
|
||||
char **seen;
|
||||
dat_path_info_t info;
|
||||
if (!archive || !path || !callback || canonical_entry_path(path, wanted, sizeof(wanted))) { errno = EINVAL; return -1; }
|
||||
if (dat_archive_stat_at(archive, version, wanted, &info) || !info.is_directory) { if (!errno) errno = ENOTDIR; return -1; }
|
||||
prefix = strlen(wanted);
|
||||
seen = calloc(version ? (size_t)version : 1, sizeof(*seen));
|
||||
if (!seen) return -1;
|
||||
for (i = 0; i < (size_t)version; i++) {
|
||||
sleep_metadata_entry_t e, latest;
|
||||
const char *candidate, *relative, *slash;
|
||||
char name[1024], child[2048];
|
||||
size_t child_len;
|
||||
if (dat_archive_read_entry(archive, i, &e)) { free(seen); return -1; }
|
||||
candidate = e.path; while (*candidate == '/') candidate++;
|
||||
if (prefix) {
|
||||
if (strncmp(candidate, wanted, prefix) || candidate[prefix] != '/') continue;
|
||||
relative = candidate + prefix + 1;
|
||||
} else relative = candidate;
|
||||
if (!*relative) continue;
|
||||
slash = strchr(relative, '/');
|
||||
child_len = slash ? (size_t)(slash - relative) : strlen(relative);
|
||||
if (!child_len || child_len >= sizeof(name)) continue;
|
||||
memcpy(name, relative, child_len); name[child_len] = 0;
|
||||
{
|
||||
size_t s;
|
||||
for (s = 0; s < seen_count; s++) if (!strcmp(seen[s], name)) break;
|
||||
if (s < seen_count) continue;
|
||||
}
|
||||
if (prefix) snprintf(child, sizeof(child), "%s/%s", wanted, name);
|
||||
else snprintf(child, sizeof(child), "%s", name);
|
||||
if (find_normalized(archive, version, child, &latest)) {
|
||||
if (errno == ENOENT) {
|
||||
/* A descendant may imply a directory even without an explicit record. */
|
||||
if (!slash || dat_archive_stat_at(archive, version, child, &info)) continue;
|
||||
latest = info.entry;
|
||||
} else { free(seen); return -1; }
|
||||
}
|
||||
if (latest.type == SLEEP_ENTRY_DELETE) continue;
|
||||
seen[seen_count] = strdup(name);
|
||||
if (!seen[seen_count]) { while (seen_count) free(seen[--seen_count]); free(seen); return -1; }
|
||||
seen_count++;
|
||||
if (callback(&latest, name, context)) { while (seen_count) free(seen[--seen_count]); free(seen); return -1; }
|
||||
}
|
||||
while (seen_count) free(seen[--seen_count]);
|
||||
free(seen);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lookup_index(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, dat_path_info_t *info)
|
||||
{
|
||||
char index_path[2048];
|
||||
int n;
|
||||
if (*path) n = snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
||||
else n = snprintf(index_path, sizeof(index_path), "index.html");
|
||||
if (n < 0 || (size_t)n >= sizeof(index_path)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (dat_archive_stat_at(archive, version, index_path, info)) return -1;
|
||||
return info->entry.type == SLEEP_ENTRY_FILE ? 0 : -1;
|
||||
}
|
||||
|
||||
int dat_archive_resolve_at(const dat_archive_t *archive, uint64_t version,
|
||||
const char *path, int force_listing,
|
||||
dat_path_info_t *info, int *serve_listing)
|
||||
{
|
||||
char normalized[2048], html_path[2048];
|
||||
const char *p;
|
||||
size_t n;
|
||||
int trailing_slash, is_directory;
|
||||
if (!archive || !path || !info || !serve_listing || canonical_entry_path(path, normalized, sizeof(normalized))) { errno = EINVAL; return -1; }
|
||||
p = path + strlen(path);
|
||||
trailing_slash = p > path && p[-1] == '/';
|
||||
*serve_listing = 0;
|
||||
if (dat_archive_stat_at(archive, version, normalized, info)) {
|
||||
/* Hyperdrive resolves /name to /name.html when the direct path is absent. */
|
||||
if (errno != ENOENT || force_listing) return -1;
|
||||
n = strlen(normalized);
|
||||
if (n + sizeof(".html") > sizeof(html_path)) { errno = ENAMETOOLONG; return -1; }
|
||||
snprintf(html_path, sizeof(html_path), "%s.html", normalized);
|
||||
if (!dat_archive_stat_at(archive, version, html_path, info) &&
|
||||
info->entry.type == SLEEP_ENTRY_FILE) return 0;
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
is_directory = info->is_directory;
|
||||
if (force_listing) {
|
||||
if (!is_directory) { errno = ENOTDIR; return -1; }
|
||||
*serve_listing = 1; return 0;
|
||||
}
|
||||
if (trailing_slash || is_directory) {
|
||||
if (!lookup_index(archive, version, normalized, info)) return 0;
|
||||
if (!is_directory) { errno = ENOENT; return -1; }
|
||||
*serve_listing = 1; return 0;
|
||||
}
|
||||
if (info->entry.type == SLEEP_ENTRY_FILE) return 0;
|
||||
n = strlen(normalized);
|
||||
if (n + sizeof(".html") > sizeof(html_path)) { errno = ENAMETOOLONG; return -1; }
|
||||
snprintf(html_path, sizeof(html_path), "%s.html", normalized);
|
||||
if (!dat_archive_stat_at(archive, version, html_path, info) && info->entry.type == SLEEP_ENTRY_FILE) return 0;
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *dat_content_type(const char *path)
|
||||
{
|
||||
const char *dot = strrchr(path ? path : "", '.');
|
||||
if (!dot) return "text/plain";
|
||||
dot++;
|
||||
if (!strcasecmp(dot, "html") || !strcasecmp(dot, "htm")) return "text/html";
|
||||
if (!strcasecmp(dot, "css")) return "text/css";
|
||||
if (!strcasecmp(dot, "js")) return "application/javascript";
|
||||
if (!strcasecmp(dot, "json")) return "application/json";
|
||||
if (!strcasecmp(dot, "svg")) return "image/svg+xml";
|
||||
if (!strcasecmp(dot, "png")) return "image/png";
|
||||
if (!strcasecmp(dot, "gif")) return "image/gif";
|
||||
if (!strcasecmp(dot, "jpg") || !strcasecmp(dot, "jpeg")) return "image/jpeg";
|
||||
if (!strcasecmp(dot, "webp")) return "image/webp";
|
||||
return "text/plain";
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include "dat_dns.h"
|
||||
#include "dat_paths.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <curl/curl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#define DAT_DNS_PACKET_MAX 65536
|
||||
#define DAT_DNS_TXT_MAX 4096
|
||||
#define DAT_DNS_CACHE_SIZE 32
|
||||
|
||||
typedef struct dat_dns_cache_entry {
|
||||
char hostname[256];
|
||||
uint8_t key[DAT_KEY_BYTES];
|
||||
uint64_t expires_at;
|
||||
int active;
|
||||
} dat_dns_cache_entry_t;
|
||||
|
||||
static dat_dns_cache_entry_t dns_cache[DAT_DNS_CACHE_SIZE];
|
||||
static size_t dns_cache_next;
|
||||
static int curl_initialized;
|
||||
static int hex_value(unsigned char c);
|
||||
|
||||
typedef struct dat_dns_body {
|
||||
char text[4096];
|
||||
size_t length;
|
||||
int overflow;
|
||||
} dat_dns_body_t;
|
||||
|
||||
static size_t receive_dat_dns_body(char *data, size_t size, size_t count, void *context)
|
||||
{
|
||||
dat_dns_body_t *body = (dat_dns_body_t *)context;
|
||||
size_t bytes = size * count;
|
||||
if (bytes > sizeof(body->text) - body->length - 1) { body->overflow = 1; return 0; }
|
||||
memcpy(body->text + body->length, data, bytes);
|
||||
body->length += bytes;
|
||||
body->text[body->length] = '\0';
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static int parse_wellknown_record(const char *body, size_t length,
|
||||
uint8_t key[DAT_KEY_BYTES], uint32_t *ttl_out)
|
||||
{
|
||||
const char *line_end, *ttl_line;
|
||||
size_t key_length, i;
|
||||
uint32_t ttl = 3600;
|
||||
if (!body || !key) { errno = EINVAL; return -1; }
|
||||
line_end = memchr(body, '\n', length);
|
||||
if (!line_end) line_end = body + length;
|
||||
if ((size_t)(line_end - body) < 70 || strncasecmp(body, "dat://", 6)) { errno = EPROTO; return -1; }
|
||||
key_length = (size_t)(line_end - body) - 6;
|
||||
if (key_length < 64) { errno = EPROTO; return -1; }
|
||||
for (i = 0; i < DAT_KEY_BYTES; i++) {
|
||||
int hi = hex_value((unsigned char)body[6 + i * 2]);
|
||||
int lo = hex_value((unsigned char)body[7 + i * 2]);
|
||||
if (hi < 0 || lo < 0) { errno = EPROTO; return -1; }
|
||||
key[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
if (key_length > 64 && body[70] != '+' && body[70] != '/' && body[70] != '?') { errno = EPROTO; return -1; }
|
||||
ttl_line = line_end < body + length ? line_end + 1 : NULL;
|
||||
if (ttl_line && !strncasecmp(ttl_line, "ttl=", 4)) {
|
||||
char *end;
|
||||
unsigned long parsed = strtoul(ttl_line + 4, &end, 10);
|
||||
if (end != ttl_line + 4 && (*end == '\n' || *end == '\r' || *end == '\0'))
|
||||
ttl = parsed > 604800ul ? 604800u : (uint32_t)parsed;
|
||||
}
|
||||
if (ttl_out) *ttl_out = ttl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hex_value(unsigned char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int normalize_hostname(const char *input, char output[256])
|
||||
{
|
||||
size_t n, label_start = 0, i;
|
||||
if (!input || !*input) { errno = EINVAL; return -1; }
|
||||
n = strlen(input);
|
||||
if (n && input[n - 1] == '.') n--;
|
||||
if (!n || n > 253) { errno = EINVAL; return -1; }
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)input[i];
|
||||
if (c == '.') {
|
||||
size_t label_length = i - label_start;
|
||||
if (!label_length || label_length > 63 || input[label_start] == '-' || input[i - 1] == '-') {
|
||||
errno = EINVAL; return -1;
|
||||
}
|
||||
output[i] = '.';
|
||||
label_start = i + 1;
|
||||
} else if (isalnum(c) || c == '-') output[i] = (char)tolower(c);
|
||||
else { errno = EINVAL; return -1; }
|
||||
}
|
||||
if (label_start == n || n - label_start > 63 || input[label_start] == '-' || input[n - 1] == '-') {
|
||||
errno = EINVAL; return -1;
|
||||
}
|
||||
output[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int names_file(char *path, size_t capacity, int create)
|
||||
{
|
||||
const char *home = dat_home_directory();
|
||||
char directory[1024];
|
||||
int n;
|
||||
if (!home || !*home) { errno = ENOENT; return -1; }
|
||||
n = snprintf(directory, sizeof(directory), "%s/.dat", home);
|
||||
if (n < 0 || (size_t)n >= sizeof(directory)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (create && mkdir(directory, 0700) && errno != EEXIST) return -1;
|
||||
n = snprintf(path, capacity, "%s/dat-dns", directory);
|
||||
if (n < 0 || (size_t)n >= capacity) { errno = ENAMETOOLONG; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_name_line(char *line, char hostname[256], uint8_t key[DAT_KEY_BYTES])
|
||||
{
|
||||
char name[256], hex[65];
|
||||
size_t i;
|
||||
if (sscanf(line, "%255s %64s", name, hex) != 2 || strlen(hex) != 64 ||
|
||||
normalize_hostname(name, hostname)) return -1;
|
||||
for (i = 0; i < DAT_KEY_BYTES; i++) {
|
||||
int hi = hex_value((unsigned char)hex[i * 2]);
|
||||
int lo = hex_value((unsigned char)hex[i * 2 + 1]);
|
||||
if (hi < 0 || lo < 0) return -1;
|
||||
key[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_dns_name_foreach(dat_dns_name_iter_fn callback, void *context)
|
||||
{
|
||||
char path[1200], line[400], hostname[256];
|
||||
uint8_t key[DAT_KEY_BYTES];
|
||||
FILE *file;
|
||||
if (!callback || names_file(path, sizeof(path), 0)) { if (!callback) errno = EINVAL; return -1; }
|
||||
file = fopen(path, "r");
|
||||
if (!file) return errno == ENOENT ? 0 : -1;
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
if (!parse_name_line(line, hostname, key) && callback(hostname, key, context)) {
|
||||
fclose(file); return 1;
|
||||
}
|
||||
}
|
||||
if (ferror(file)) { fclose(file); return -1; }
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct name_lookup_context {
|
||||
const uint8_t *key;
|
||||
char *hostname;
|
||||
size_t capacity;
|
||||
int found;
|
||||
} name_lookup_context_t;
|
||||
|
||||
static int find_name_for_key(const char *name, const uint8_t key[DAT_KEY_BYTES], void *context)
|
||||
{
|
||||
name_lookup_context_t *lookup = (name_lookup_context_t *)context;
|
||||
if (!memcmp(lookup->key, key, DAT_KEY_BYTES)) {
|
||||
if (strlen(name) >= lookup->capacity) { errno = ENOSPC; return 1; }
|
||||
strcpy(lookup->hostname, name);
|
||||
lookup->found = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_dns_name_for_key(const uint8_t key[DAT_KEY_BYTES], char *hostname,
|
||||
size_t hostname_capacity)
|
||||
{
|
||||
name_lookup_context_t lookup;
|
||||
if (!key || !hostname || !hostname_capacity) { errno = EINVAL; return -1; }
|
||||
lookup.key = key; lookup.hostname = hostname; lookup.capacity = hostname_capacity; lookup.found = 0;
|
||||
if (dat_dns_name_foreach(find_name_for_key, &lookup) < 0) return -1;
|
||||
if (!lookup.found) { errno = ENOENT; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct name_resolve_context {
|
||||
const char *hostname;
|
||||
uint8_t *key;
|
||||
int found;
|
||||
} name_resolve_context_t;
|
||||
|
||||
static int find_key_for_name(const char *name, const uint8_t key[DAT_KEY_BYTES], void *context)
|
||||
{
|
||||
name_resolve_context_t *lookup = (name_resolve_context_t *)context;
|
||||
if (!strcasecmp(lookup->hostname, name)) {
|
||||
memcpy(lookup->key, key, DAT_KEY_BYTES);
|
||||
lookup->found = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_dns_name_resolve(const char *hostname, uint8_t key[DAT_KEY_BYTES])
|
||||
{
|
||||
char normalized[256];
|
||||
name_resolve_context_t lookup;
|
||||
if (!key || normalize_hostname(hostname, normalized)) { if (!key) errno = EINVAL; return -1; }
|
||||
lookup.hostname = normalized; lookup.key = key; lookup.found = 0;
|
||||
if (dat_dns_name_foreach(find_key_for_name, &lookup) < 0) return -1;
|
||||
if (!lookup.found) { errno = ENOENT; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int update_name(const char *hostname, const uint8_t *key, int remove_alias)
|
||||
{
|
||||
char normalized[256], path[1200], temporary[1250], line[400], old_name[256];
|
||||
uint8_t old_key[DAT_KEY_BYTES];
|
||||
FILE *input = NULL, *output = NULL;
|
||||
char hex[65];
|
||||
int found = 0, n, result = -1;
|
||||
if (normalize_hostname(hostname, normalized) || (!remove_alias && !key)) return -1;
|
||||
if (names_file(path, sizeof(path), 1)) return -1;
|
||||
n = snprintf(temporary, sizeof(temporary), "%s.tmp", path);
|
||||
if (n < 0 || (size_t)n >= sizeof(temporary)) { errno = ENAMETOOLONG; return -1; }
|
||||
input = fopen(path, "r");
|
||||
if (!input && errno != ENOENT) return -1;
|
||||
output = fopen(temporary, "w");
|
||||
if (!output) { if (input) fclose(input); return -1; }
|
||||
while (input && fgets(line, sizeof(line), input)) {
|
||||
if (!parse_name_line(line, old_name, old_key) && !strcasecmp(old_name, normalized)) {
|
||||
found = 1;
|
||||
if (!remove_alias) {
|
||||
if (dat_key_to_hex(key, hex) || fprintf(output, "%s %s\n", normalized, hex) < 0)
|
||||
goto done;
|
||||
}
|
||||
} else if (fputs(line, output) == EOF) goto done;
|
||||
}
|
||||
if (input && ferror(input)) goto done;
|
||||
if (!remove_alias && !found) {
|
||||
if (dat_key_to_hex(key, hex) || fprintf(output, "%s %s\n", normalized, hex) < 0) goto done;
|
||||
}
|
||||
if (fclose(output)) { output = NULL; goto done; }
|
||||
output = NULL;
|
||||
if (input && fclose(input)) { input = NULL; goto done; }
|
||||
input = NULL;
|
||||
if (remove_alias && !found) { errno = ENOENT; goto done; }
|
||||
if (dat_replace_file(temporary, path)) goto done;
|
||||
result = 0;
|
||||
done:
|
||||
if (input) fclose(input);
|
||||
if (output) fclose(output);
|
||||
if (result) unlink(temporary);
|
||||
return result;
|
||||
}
|
||||
|
||||
int dat_dns_name_set(const char *hostname, const uint8_t key[DAT_KEY_BYTES])
|
||||
{
|
||||
return update_name(hostname, key, 0);
|
||||
}
|
||||
|
||||
int dat_dns_name_remove(const char *hostname)
|
||||
{
|
||||
return update_name(hostname, NULL, 1);
|
||||
}
|
||||
|
||||
static int parse_datkey(const char *txt, size_t length, uint8_t key[32])
|
||||
{
|
||||
size_t i;
|
||||
const char *value = NULL;
|
||||
for (i = 0; i + 7 <= length; i++) {
|
||||
if ((txt[i] == 'd' || txt[i] == 'D') &&
|
||||
(txt[i + 1] == 'a' || txt[i + 1] == 'A') &&
|
||||
(txt[i + 2] == 't' || txt[i + 2] == 'T') &&
|
||||
(txt[i + 3] == 'k' || txt[i + 3] == 'K') &&
|
||||
(txt[i + 4] == 'e' || txt[i + 4] == 'E') &&
|
||||
(txt[i + 5] == 'y' || txt[i + 5] == 'Y') && txt[i + 6] == '=') {
|
||||
value = txt + i + 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!value || (size_t)(txt + length - value) < 64) return 0;
|
||||
for (i = 0; i < 32; i++) {
|
||||
int hi = hex_value((unsigned char)value[i * 2]);
|
||||
int lo = hex_value((unsigned char)value[i * 2 + 1]);
|
||||
if (hi < 0 || lo < 0) return 0;
|
||||
key[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
if ((size_t)(txt + length - value) > 64 && value[64] != '&' && value[64] != ';' &&
|
||||
value[64] != ' ' && value[64] != '"' && value[64] != '\'')
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dat_dns_parse_answer(const uint8_t *packet, unsigned packet_len,
|
||||
uint8_t key[DAT_KEY_BYTES], uint32_t *ttl_seconds)
|
||||
{
|
||||
unsigned offset = 12, questions, answers, record_index;
|
||||
if (!packet || packet_len < 12 || !key) { errno = EINVAL; return -1; }
|
||||
questions = ((unsigned)packet[4] << 8) | packet[5];
|
||||
answers = ((unsigned)packet[6] << 8) | packet[7];
|
||||
for (record_index = 0; record_index < questions; record_index++) {
|
||||
unsigned hops = 0, cursor = offset;
|
||||
int jumped = 0;
|
||||
for (;;) {
|
||||
unsigned label;
|
||||
if (cursor >= packet_len || ++hops > packet_len) { errno = EPROTO; return -1; }
|
||||
label = packet[cursor++];
|
||||
if ((label & 0xc0u) == 0xc0u) {
|
||||
if (cursor >= packet_len) { errno = EPROTO; return -1; }
|
||||
if (!jumped) offset = cursor + 1;
|
||||
cursor = ((label & 0x3fu) << 8) | packet[cursor]; jumped = 1; continue;
|
||||
}
|
||||
if (label & 0xc0u) { errno = EPROTO; return -1; }
|
||||
if (!label) { if (!jumped) offset = cursor; break; }
|
||||
if (label > 63 || label > packet_len - cursor) { errno = EPROTO; return -1; }
|
||||
cursor += label;
|
||||
}
|
||||
if (packet_len - offset < 4) { errno = EPROTO; return -1; }
|
||||
offset += 4;
|
||||
}
|
||||
for (record_index = 0; record_index < answers; record_index++) {
|
||||
unsigned hops = 0, cursor = offset, type, data_length, end;
|
||||
int jumped = 0;
|
||||
uint32_t ttl;
|
||||
for (;;) {
|
||||
unsigned label;
|
||||
if (cursor >= packet_len || ++hops > packet_len) { errno = EPROTO; return -1; }
|
||||
label = packet[cursor++];
|
||||
if ((label & 0xc0u) == 0xc0u) {
|
||||
if (cursor >= packet_len) { errno = EPROTO; return -1; }
|
||||
if (!jumped) offset = cursor + 1;
|
||||
cursor = ((label & 0x3fu) << 8) | packet[cursor]; jumped = 1; continue;
|
||||
}
|
||||
if (label & 0xc0u) { errno = EPROTO; return -1; }
|
||||
if (!label) { if (!jumped) offset = cursor; break; }
|
||||
if (label > 63 || label > packet_len - cursor) { errno = EPROTO; return -1; }
|
||||
cursor += label;
|
||||
}
|
||||
if (packet_len - offset < 10) { errno = EPROTO; return -1; }
|
||||
type = ((unsigned)packet[offset] << 8) | packet[offset + 1];
|
||||
ttl = ((uint32_t)packet[offset + 4] << 24) | ((uint32_t)packet[offset + 5] << 16) |
|
||||
((uint32_t)packet[offset + 6] << 8) | packet[offset + 7];
|
||||
data_length = ((unsigned)packet[offset + 8] << 8) | packet[offset + 9];
|
||||
offset += 10;
|
||||
if (data_length > packet_len - offset) { errno = EPROTO; return -1; }
|
||||
end = offset + data_length;
|
||||
if (type == 16) {
|
||||
char text[DAT_DNS_TXT_MAX];
|
||||
size_t used = 0;
|
||||
while (offset < end) {
|
||||
unsigned part = packet[offset++];
|
||||
if (part > end - offset || used + part >= sizeof(text)) { errno = EPROTO; return -1; }
|
||||
memcpy(text + used, packet + offset, part); used += part; offset += part;
|
||||
}
|
||||
if (parse_datkey(text, used, key)) { if (ttl_seconds) *ttl_seconds = ttl; return 0; }
|
||||
} else offset = end;
|
||||
}
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dat_dns_resolve(const char *hostname, uint8_t key[DAT_KEY_BYTES],
|
||||
uint32_t *ttl_seconds)
|
||||
{
|
||||
size_t i;
|
||||
uint64_t now;
|
||||
uint32_t ttl = 0;
|
||||
char normalized[256], request_url[320];
|
||||
if (!hostname || !*hostname || !key) { errno = EINVAL; return -1; }
|
||||
if (normalize_hostname(hostname, normalized)) return -1;
|
||||
if (dat_dns_name_resolve(normalized, key) == 0) {
|
||||
if (ttl_seconds) *ttl_seconds = 0;
|
||||
return 0;
|
||||
}
|
||||
now = (uint64_t)time(NULL);
|
||||
for (i = 0; i < DAT_DNS_CACHE_SIZE; i++) {
|
||||
if (dns_cache[i].active && dns_cache[i].expires_at <= now)
|
||||
dns_cache[i].active = 0;
|
||||
if (dns_cache[i].active && !strcasecmp(normalized, dns_cache[i].hostname)) {
|
||||
memcpy(key, dns_cache[i].key, DAT_KEY_BYTES);
|
||||
if (ttl_seconds) *ttl_seconds = (uint32_t)(dns_cache[i].expires_at - now);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
{
|
||||
CURL *curl;
|
||||
CURLcode result;
|
||||
long status = 0;
|
||||
dat_dns_body_t body;
|
||||
int url_length = snprintf(request_url, sizeof(request_url), "https://%s/.well-known/dat", normalized);
|
||||
if (url_length < 0 || (size_t)url_length >= sizeof(request_url)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (!curl_initialized) { if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { errno = EIO; return -1; } curl_initialized = 1; }
|
||||
memset(&body, 0, sizeof(body));
|
||||
curl = curl_easy_init();
|
||||
if (!curl) { errno = ENOMEM; return -1; }
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request_url);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive_dat_dns_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
|
||||
result = curl_easy_perform(curl);
|
||||
if (result == CURLE_OK) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
curl_easy_cleanup(curl);
|
||||
if (result != CURLE_OK || status != 200 || body.overflow) { errno = EHOSTUNREACH; return -1; }
|
||||
if (parse_wellknown_record(body.text, body.length, key, &ttl)) return -1;
|
||||
}
|
||||
if (ttl_seconds) *ttl_seconds = ttl;
|
||||
if (ttl) {
|
||||
dat_dns_cache_entry_t *entry = &dns_cache[dns_cache_next++ % DAT_DNS_CACHE_SIZE];
|
||||
strcpy(entry->hostname, hostname);
|
||||
memcpy(entry->key, key, DAT_KEY_BYTES);
|
||||
entry->expires_at = now + ttl;
|
||||
entry->active = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "dat.h"
|
||||
#include "dat_dns.h"
|
||||
#include "dat_fetch.h"
|
||||
#include "dat_paths.h"
|
||||
#include "dat_dpi_icons.h"
|
||||
#include "s7.h"
|
||||
#include "sleep_feed.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <locale.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define TAG_CAP 8192
|
||||
#define DPI_ARCHIVES_MAX 128
|
||||
|
||||
typedef struct {
|
||||
char key[65];
|
||||
char root[1200];
|
||||
char title[256];
|
||||
char description[512];
|
||||
uint64_t version;
|
||||
int used;
|
||||
} dpi_archive_t;
|
||||
|
||||
static dpi_archive_t open_archives[DPI_ARCHIVES_MAX];
|
||||
static s7_scheme *metadata_scheme;
|
||||
static int quit_requested;
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t length;
|
||||
size_t capacity;
|
||||
} body_buffer_t;
|
||||
|
||||
static int body_appendf(body_buffer_t *body, const char *format, ...)
|
||||
{
|
||||
va_list args, copy;
|
||||
int needed;
|
||||
size_t capacity;
|
||||
char *data;
|
||||
va_start(args, format);
|
||||
va_copy(copy, args);
|
||||
needed = vsnprintf(NULL, 0, format, copy);
|
||||
va_end(copy);
|
||||
if (needed < 0) { va_end(args); return -1; }
|
||||
if ((size_t)needed > SIZE_MAX - body->length - 1) { va_end(args); errno = EOVERFLOW; return -1; }
|
||||
if (body->length + (size_t)needed + 1 > body->capacity) {
|
||||
capacity = body->capacity ? body->capacity : 512;
|
||||
while (capacity < body->length + (size_t)needed + 1) {
|
||||
if (capacity > SIZE_MAX / 2) { capacity = body->length + (size_t)needed + 1; break; }
|
||||
capacity *= 2;
|
||||
}
|
||||
data = realloc(body->data, capacity);
|
||||
if (!data) { va_end(args); return -1; }
|
||||
body->data = data;
|
||||
body->capacity = capacity;
|
||||
}
|
||||
vsnprintf(body->data + body->length, body->capacity - body->length, format, args);
|
||||
va_end(args);
|
||||
body->length += (size_t)needed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void send_page(const char *url, const char *mime, const void *body, size_t length)
|
||||
{
|
||||
const char *p;
|
||||
printf("<cmd='start_send_page' url='");
|
||||
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||||
printf("' '>\nContent-Type: %s\r\n\r\n", mime);
|
||||
if (length) fwrite(body, 1, length, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static int send_file(const char *url, const char *mime, const dat_archive_t *archive,
|
||||
const sleep_metadata_entry_t *entry)
|
||||
{
|
||||
sleep_feed_t feed;
|
||||
uint64_t block;
|
||||
size_t sent = 0;
|
||||
const char *p;
|
||||
if (entry->size > SIZE_MAX) { errno = EOVERFLOW; return -1; }
|
||||
if (sleep_feed_open(&feed, archive->dat_path, "content", NULL)) return -1;
|
||||
for (block = 0; block < entry->block_count; block++) {
|
||||
if (!sleep_feed_has(&feed, (size_t)entry->block_offset + (size_t)block)) {
|
||||
sleep_feed_close(&feed); errno = ENOENT; return -1;
|
||||
}
|
||||
}
|
||||
fputs("<cmd='start_send_page' url='", stdout);
|
||||
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||||
printf("' '>\nContent-Type: %s\r\n\r\n", mime);
|
||||
fflush(stdout);
|
||||
for (block = 0; block < entry->block_count; block++) {
|
||||
uint8_t *data = NULL;
|
||||
size_t length = 0;
|
||||
if (sleep_feed_read(&feed, (size_t)entry->block_offset + (size_t)block,
|
||||
&data, &length) || length > (size_t)entry->size - sent) {
|
||||
free(data); sleep_feed_close(&feed); errno = EIO; return -1;
|
||||
}
|
||||
if (length && fwrite(data, 1, length, stdout) != length) {
|
||||
free(data); sleep_feed_close(&feed); errno = EIO; return -1;
|
||||
}
|
||||
sent += length;
|
||||
free(data);
|
||||
}
|
||||
sleep_feed_close(&feed);
|
||||
fflush(stdout);
|
||||
if (sent != (size_t)entry->size) { errno = EPROTO; return -1; }
|
||||
return ferror(stdout) ? -1 : 0;
|
||||
}
|
||||
|
||||
static void send_status(const char *message)
|
||||
{
|
||||
const char *p;
|
||||
fputs("<cmd='send_status_message' msg='", stdout);
|
||||
for (p = message; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||||
fputs("' '>", stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void send_reload(const char *url)
|
||||
{
|
||||
const char *p;
|
||||
fputs("<cmd='reload_request' url='", stdout);
|
||||
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||||
fputs("' '>", stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static int html_append(body_buffer_t *body, const char *text)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)(text ? text : "");
|
||||
for (; *p; p++) {
|
||||
const char *escape = NULL;
|
||||
switch (*p) {
|
||||
case '&': escape = "&"; break;
|
||||
case '<': escape = "<"; break;
|
||||
case '>': escape = ">"; break;
|
||||
case '\"': escape = """; break;
|
||||
case '\'': escape = "'"; break;
|
||||
default: break;
|
||||
}
|
||||
if (escape) { if (body_appendf(body, "%s", escape)) return -1; }
|
||||
else if (body_appendf(body, "%c", *p)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int url_append_path(body_buffer_t *body, const char *path)
|
||||
{
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
const unsigned char *p = (const unsigned char *)path;
|
||||
for (; *p; p++) {
|
||||
if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
|
||||
(*p >= '0' && *p <= '9') || strchr("/-_.~", *p)) {
|
||||
if (body_appendf(body, "%c", *p)) return -1;
|
||||
} else if (body_appendf(body, "%%%c%c", hex[*p >> 4], hex[*p & 15])) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Dillo DPIP tags end in space, quote, greater-than. */
|
||||
static int read_tag(FILE *in, char *tag, size_t cap)
|
||||
{
|
||||
int c, quote = 0;
|
||||
size_t n = 0;
|
||||
while ((c = fgetc(in)) != EOF) {
|
||||
if (!n && isspace((unsigned char)c)) continue;
|
||||
if (n + 1 >= cap) { errno = EOVERFLOW; return -1; }
|
||||
tag[n++] = (char)c;
|
||||
if (c == '\'') {
|
||||
if (quote) {
|
||||
c = fgetc(in);
|
||||
if (c == '\'') {
|
||||
if (n + 1 >= cap) { errno = EOVERFLOW; return -1; }
|
||||
tag[n++] = (char)c;
|
||||
continue;
|
||||
}
|
||||
if (c != EOF) ungetc(c, in);
|
||||
}
|
||||
quote = !quote;
|
||||
}
|
||||
if (n >= 2 && tag[n - 2] == '\'' && tag[n - 1] == '>') {
|
||||
tag[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return n ? -1 : 0;
|
||||
}
|
||||
|
||||
static char *attribute(const char *tag, const char *name)
|
||||
{
|
||||
char needle[64];
|
||||
const char *p, *start, *end;
|
||||
char *out;
|
||||
size_t n, used = 0;
|
||||
if (snprintf(needle, sizeof(needle), " %s='", name) >= (int)sizeof(needle)) return NULL;
|
||||
p = strstr(tag, needle);
|
||||
if (p) start = p + strlen(needle);
|
||||
else if (tag[0] == '<' && !strncmp(tag + 1, needle + 1, strlen(needle) - 1))
|
||||
start = tag + strlen(needle);
|
||||
else return NULL;
|
||||
end = start;
|
||||
while (*end) {
|
||||
if (*end == '\'' && end[1] != '\'') break;
|
||||
if (*end == '\'' && end[1] == '\'') end += 2;
|
||||
else end++;
|
||||
}
|
||||
if (!*end) return NULL;
|
||||
n = (size_t)(end - start);
|
||||
out = malloc(n + 1);
|
||||
if (!out) return NULL;
|
||||
while (start < end) {
|
||||
out[used++] = *start;
|
||||
if (*start == '\'' && start + 1 < end && start[1] == '\'') start++;
|
||||
start++;
|
||||
}
|
||||
out[used] = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
static int authenticate(const char *tag)
|
||||
{
|
||||
char path[1024], line[512], secret[128];
|
||||
FILE *f;
|
||||
char *msg;
|
||||
int ok = 0;
|
||||
const char *home = dat_home_directory();
|
||||
if (!home || snprintf(path, sizeof(path), "%s/.dillo/dpid_comm_keys", home) >= (int)sizeof(path)) return 0;
|
||||
f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
if (fgets(line, sizeof(line), f) && sscanf(line, "%*u %127s", secret) == 1) {
|
||||
msg = attribute(tag, "msg");
|
||||
if (msg && strlen(msg) == strlen(secret)) ok = memcmp(msg, secret, strlen(secret)) == 0;
|
||||
free(msg);
|
||||
}
|
||||
fclose(f);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static s7_pointer manifest_field(s7_pointer record, const char *name)
|
||||
{
|
||||
s7_pointer fields = record;
|
||||
while (s7_is_pair(fields)) {
|
||||
s7_pointer field = s7_car(fields), key, values;
|
||||
if (s7_is_pair(field)) {
|
||||
key = s7_car(field);
|
||||
values = s7_cdr(field);
|
||||
if (s7_is_symbol(key) && !strcmp(s7_symbol_name(key), name) &&
|
||||
s7_is_pair(values)) return s7_car(values);
|
||||
}
|
||||
fields = s7_cdr(fields);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void read_manifest(const dat_archive_t *archive, uint64_t version,
|
||||
dpi_archive_t *record)
|
||||
{
|
||||
dat_path_info_t info;
|
||||
int listing;
|
||||
uint8_t *data = NULL;
|
||||
size_t length = 0;
|
||||
if (!metadata_scheme) metadata_scheme = s7_init();
|
||||
if (metadata_scheme &&
|
||||
dat_archive_resolve_at(archive, version, "/dat.scm", 0, &info, &listing) == 0 &&
|
||||
!listing && info.entry.type == SLEEP_ENTRY_FILE &&
|
||||
dat_archive_read_file(archive, &info.entry, &data, &length) == 0 &&
|
||||
length < 1024u * 1024u && !memchr(data, 0, length)) {
|
||||
char *source = malloc(length + 1);
|
||||
if (source) {
|
||||
s7_pointer port, parsed, title, description;
|
||||
memcpy(source, data, length); source[length] = '\0';
|
||||
port = s7_open_input_string(metadata_scheme, source);
|
||||
parsed = port ? s7_read(metadata_scheme, port) : NULL;
|
||||
if (port) s7_close_input_port(metadata_scheme, port);
|
||||
title = parsed ? manifest_field(parsed, "title") : NULL;
|
||||
description = parsed ? manifest_field(parsed, "description") : NULL;
|
||||
if (title && s7_is_string(title)) snprintf(record->title, sizeof(record->title), "%s", s7_string(title));
|
||||
if (description && s7_is_string(description)) snprintf(record->description, sizeof(record->description), "%s", s7_string(description));
|
||||
free(source);
|
||||
}
|
||||
}
|
||||
free(data);
|
||||
if (!record->title[0] || !record->description[0]) {
|
||||
uint8_t *cname = NULL;
|
||||
size_t cname_length = 0;
|
||||
if (dat_archive_resolve_at(archive, version, "/CNAME", 0, &info, &listing) == 0 &&
|
||||
!listing && info.entry.type == SLEEP_ENTRY_FILE &&
|
||||
dat_archive_read_file(archive, &info.entry, &cname, &cname_length) == 0 &&
|
||||
cname_length && cname_length < sizeof(record->title)) {
|
||||
while (cname_length && (cname[cname_length - 1] == '\n' || cname[cname_length - 1] == '\r')) cname_length--;
|
||||
if (!record->title[0]) { memcpy(record->title, cname, cname_length); record->title[cname_length] = '\0'; }
|
||||
}
|
||||
free(cname);
|
||||
}
|
||||
}
|
||||
|
||||
static dpi_archive_t *remember_archive(const dat_archive_t *archive, const char *keyhex,
|
||||
const char *root, uint64_t version)
|
||||
{
|
||||
size_t i;
|
||||
dpi_archive_t *record = NULL;
|
||||
for (i = 0; i < DPI_ARCHIVES_MAX; i++) {
|
||||
if (open_archives[i].used && !strcmp(open_archives[i].key, keyhex)) {
|
||||
record = &open_archives[i]; break;
|
||||
}
|
||||
if (!open_archives[i].used && !record) record = &open_archives[i];
|
||||
}
|
||||
if (!record) return NULL;
|
||||
if (!record->used) {
|
||||
memset(record, 0, sizeof(*record));
|
||||
snprintf(record->key, sizeof(record->key), "%s", keyhex);
|
||||
snprintf(record->root, sizeof(record->root), "%s", root);
|
||||
snprintf(record->title, sizeof(record->title), "%s", keyhex);
|
||||
record->used = 1;
|
||||
}
|
||||
record->version = version;
|
||||
read_manifest(archive, version, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
static int parse_query_value(const char *url, const char *name, char *out, size_t capacity)
|
||||
{
|
||||
const char *query = strchr(url, '?');
|
||||
size_t name_length = strlen(name);
|
||||
if (!query) return 0;
|
||||
query++;
|
||||
while (*query) {
|
||||
const char *end = strchr(query, '&');
|
||||
size_t part_length = end ? (size_t)(end - query) : strlen(query);
|
||||
const char *equals = memchr(query, '=', part_length);
|
||||
if (equals && (size_t)(equals - query) == name_length && !memcmp(query, name, name_length)) {
|
||||
size_t value_length = part_length - name_length - 1;
|
||||
if (value_length >= capacity) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(out, equals + 1, value_length); out[value_length] = '\0';
|
||||
return 1;
|
||||
}
|
||||
if (!end) break;
|
||||
query = end + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int error_page(const char *url, int status, const char *message)
|
||||
{
|
||||
const char *p;
|
||||
(void)status;
|
||||
printf("<cmd='start_send_page' url='");
|
||||
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||||
printf("' '>\nContent-Type: text/plain\r\n\r\n%s\n", message);
|
||||
fflush(stdout);
|
||||
return ferror(stdout) ? -1 : 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
sleep_metadata_entry_t entry;
|
||||
char name[1024];
|
||||
int type;
|
||||
} listing_context_t;
|
||||
|
||||
typedef struct {
|
||||
listing_context_t *entries;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
} listing_accumulator_t;
|
||||
|
||||
static int list_entry(const sleep_metadata_entry_t *entry, const char *name, void *context)
|
||||
{
|
||||
listing_accumulator_t *list = context;
|
||||
listing_context_t *next;
|
||||
if (list->count == list->capacity) {
|
||||
size_t capacity = list->capacity ? list->capacity * 2 : 16;
|
||||
next = realloc(list->entries, capacity * sizeof(*next));
|
||||
if (!next) return -1;
|
||||
list->entries = next; list->capacity = capacity;
|
||||
}
|
||||
next = &list->entries[list->count++];
|
||||
memset(next, 0, sizeof(*next));
|
||||
next->entry = *entry;
|
||||
next->type = entry->type == SLEEP_ENTRY_DIRECTORY ? 3 : 2;
|
||||
if (strlen(name) >= sizeof(next->name)) { errno = ENAMETOOLONG; return -1; }
|
||||
strcpy(next->name, name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compare_listing(const void *left, const void *right)
|
||||
{
|
||||
const listing_context_t *a = left, *b = right;
|
||||
if (a->type != b->type) return b->type - a->type;
|
||||
return -strcoll(a->name, b->name);
|
||||
}
|
||||
|
||||
static int append_listing_row(body_buffer_t *body, const dat_url_t *url,
|
||||
const listing_context_t *entry, const char *link_path)
|
||||
{
|
||||
const char *icon = entry->type == 1 ? dat_dpi_icon_updir :
|
||||
entry->type == 3 ? dat_dpi_icon_directory : dat_dpi_icon_file;
|
||||
char date[64] = "";
|
||||
if (entry->entry.mtime) {
|
||||
time_t seconds = (time_t)(entry->entry.mtime / 1000u);
|
||||
struct tm *tm = gmtime(&seconds);
|
||||
if (tm) strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", tm);
|
||||
}
|
||||
if (body_appendf(body, "<tr><td><img alt=\"%s\" src=\"data:image/png;base64,%s\"></td><td><a href=\"dat://",
|
||||
entry->type == 1 ? "up" : entry->type == 2 ? "directory" : "file", icon)) return -1;
|
||||
if (html_append(body, url->host)) return -1;
|
||||
if (url->has_version && body_appendf(body, "+%llu", (unsigned long long)url->version)) return -1;
|
||||
if (url_append_path(body, link_path) || body_appendf(body, "\">") ||
|
||||
html_append(body, entry->name)) return -1;
|
||||
if (entry->type == 3 && body_appendf(body, "/")) return -1;
|
||||
if (body_appendf(body, "</a></td><td>")) return -1;
|
||||
if (*date && html_append(body, date)) return -1;
|
||||
if (body_appendf(body, "</td><td align=\"right\">")) return -1;
|
||||
if (entry->type == 2 && body_appendf(body, "%llu", (unsigned long long)entry->entry.size)) return -1;
|
||||
return body_appendf(body, "</td></tr>\n");
|
||||
}
|
||||
|
||||
static const char *dpi_content_type(const char *pathname)
|
||||
{
|
||||
const char *dot = strrchr(pathname, '.');
|
||||
if (!dot) return "text/plain";
|
||||
dot++;
|
||||
if (!strcmp(dot, "htm") || !strcmp(dot, "html")) return "text/html";
|
||||
if (!strcmp(dot, "png")) return "image/png";
|
||||
if (!strcmp(dot, "gif")) return "image/gif";
|
||||
if (!strcmp(dot, "jpg")) return "image/jpeg";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
static int serve_not_found(const char *url)
|
||||
{
|
||||
static const char page[] = "<h3>Not found</h3>";
|
||||
send_page(url, "text/html", page, sizeof(page) - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int serve_timeout(const char *url)
|
||||
{
|
||||
static const char page[] = "<h3>Timed out</h3>";
|
||||
send_page(url, "text/html", page, sizeof(page) - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int serve_directory(const char *url_text, const dat_url_t *url,
|
||||
const dat_archive_t *archive, uint64_t version)
|
||||
{
|
||||
listing_accumulator_t entries = {0};
|
||||
listing_context_t parent;
|
||||
body_buffer_t body = {0};
|
||||
char parent_path[2048];
|
||||
size_t i, n;
|
||||
int failed = 0;
|
||||
if (dat_archive_readdir_at(archive, version, url->path, list_entry, &entries)) {
|
||||
free(entries.entries);
|
||||
return serve_not_found(url_text);
|
||||
}
|
||||
qsort(entries.entries, entries.count, sizeof(*entries.entries), compare_listing);
|
||||
if (body_appendf(&body, "<!doctype html><html><head><title>")) failed = 1;
|
||||
if (!failed && html_append(&body, url->path)) failed = 1;
|
||||
if (!failed && body_appendf(&body, "</title></head><body><h4>")) failed = 1;
|
||||
if (!failed && html_append(&body, url->path)) failed = 1;
|
||||
if (!failed && body_appendf(&body, "</h4><table>")) failed = 1;
|
||||
if (strcmp(url->path, "/")) {
|
||||
memset(&parent, 0, sizeof(parent));
|
||||
parent.type = 1;
|
||||
strcpy(parent.name, "..");
|
||||
strcpy(parent_path, url->path);
|
||||
n = strlen(parent_path);
|
||||
while (n && parent_path[n - 1] == '/') parent_path[--n] = '\0';
|
||||
while (n && parent_path[n - 1] != '/') parent_path[--n] = '\0';
|
||||
if (!n) strcpy(parent_path, "/");
|
||||
else parent_path[n] = '\0';
|
||||
if (!failed && append_listing_row(&body, url, &parent, parent_path)) failed = 1;
|
||||
}
|
||||
for (i = 0; !failed && i < entries.count; i++) {
|
||||
body_buffer_t href = {0};
|
||||
const listing_context_t *entry = &entries.entries[i];
|
||||
if (body_appendf(&href, "%s", url->path) ||
|
||||
(href.length && href.data[href.length - 1] != '/' && body_appendf(&href, "/")) ||
|
||||
body_appendf(&href, "%s%s", entry->name, entry->type == 3 ? "/" : "") ||
|
||||
append_listing_row(&body, url, entry, href.data)) failed = 1;
|
||||
free(href.data);
|
||||
}
|
||||
if (!failed && body_appendf(&body, "</table></body></html>")) failed = 1;
|
||||
if (failed) {
|
||||
free(body.data); free(entries.entries);
|
||||
return error_page(url_text, 500, "Unable to build directory listing");
|
||||
}
|
||||
send_page(url_text, "text/html", body.data, body.length);
|
||||
free(body.data); free(entries.entries);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_append_string(body_buffer_t *body, const char *text)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)(text ? text : "");
|
||||
if (body_appendf(body, "\"")) return -1;
|
||||
for (; *p; p++) {
|
||||
if (*p == '\"' || *p == '\\') { if (body_appendf(body, "\\%c", *p)) return -1; }
|
||||
else if (*p < 0x20) { if (body_appendf(body, "\\u%04x", *p)) return -1; }
|
||||
else if (body_appendf(body, "%c", *p)) return -1;
|
||||
}
|
||||
return body_appendf(body, "\"");
|
||||
}
|
||||
|
||||
static int serve(const char *url_text)
|
||||
{
|
||||
dat_url_t url;
|
||||
uint8_t key[DAT_KEY_BYTES];
|
||||
const char *home = dat_home_directory();
|
||||
char root[1200], keyhex[65], fetch_error[512];
|
||||
dat_archive_t archive;
|
||||
dat_path_info_t info;
|
||||
int listing = 0;
|
||||
uint64_t version;
|
||||
if (dat_url_parse(url_text, &url)) {
|
||||
error_page(url_text, 400, "Invalid dat:// URL"); return -1;
|
||||
}
|
||||
send_status("resolving name");
|
||||
if (dat_url_to_key(url.host, key) && dat_dns_resolve(url.host, key, NULL)) {
|
||||
error_page(url_text, 404, "Dat hostname has no usable datkey TXT record"); return -1;
|
||||
}
|
||||
if (!home) { error_page(url_text, 500, "User home directory is unavailable"); return -1; }
|
||||
if (dat_key_to_hex(key, keyhex)) { error_page(url_text, 500, "Unable to format archive key"); return -1; }
|
||||
if (snprintf(root, sizeof(root), "%s/.dillo/dat/archives/%.2s/%s",
|
||||
home, keyhex, keyhex + 2) >= (int)sizeof(root)) {
|
||||
error_page(url_text, 400, "Dillo archive cache path is too long"); return -1;
|
||||
}
|
||||
if (dat_archive_open(&archive, root)) {
|
||||
send_status("getting archive");
|
||||
if (dat_fetch_archive(root, key, 10000u)) {
|
||||
int saved = errno;
|
||||
if (saved == ETIMEDOUT) return serve_timeout(url_text);
|
||||
const char *reason = saved == ENETUNREACH ? "No Cesspool bootstrap is configured" :
|
||||
saved == EHOSTUNREACH ? "Cesspool bootstrap did not answer" :
|
||||
saved == ENOENT ? "No peer is announcing this archive key" :
|
||||
strerror(saved);
|
||||
snprintf(fetch_error, sizeof(fetch_error), "Cesspool archive fetch failed: %s", reason);
|
||||
return error_page(url_text, 404, fetch_error);
|
||||
}
|
||||
if (dat_archive_open(&archive, root)) {
|
||||
snprintf(fetch_error, sizeof(fetch_error), "Fetched archive cannot be opened: %s", strerror(errno));
|
||||
return error_page(url_text, 404, fetch_error);
|
||||
}
|
||||
}
|
||||
version = url.has_version ? url.version : dat_archive_version(&archive);
|
||||
if (version > dat_archive_version(&archive)) {
|
||||
dat_archive_close(&archive); return serve_not_found(url_text);
|
||||
}
|
||||
if (dat_archive_resolve_at(&archive, version, url.path,
|
||||
url.force_directory_listing, &info, &listing)) {
|
||||
int saved = errno;
|
||||
dat_archive_close(&archive);
|
||||
if (saved == ETIMEDOUT || saved == EAGAIN) return serve_timeout(url_text);
|
||||
return serve_not_found(url_text);
|
||||
}
|
||||
(void)remember_archive(&archive, keyhex, root, version);
|
||||
send_status("serving file");
|
||||
if (url.wants_stat) {
|
||||
body_buffer_t stat = {0};
|
||||
if (body_appendf(&stat, "{\n \"path\": ") || json_append_string(&stat, url.path) ||
|
||||
body_appendf(&stat, ",\n \"directory\": %s,\n \"size\": %llu,\n \"mtime\": %llu\n}\n",
|
||||
info.is_directory ? "true" : "false",
|
||||
(unsigned long long)info.entry.size, (unsigned long long)info.entry.mtime)) {
|
||||
free(stat.data); dat_archive_close(&archive); return error_page(url_text, 500, "Stat response is too large");
|
||||
}
|
||||
send_page(url_text, "text/plain", stat.data, stat.length);
|
||||
free(stat.data);
|
||||
} else if (listing) {
|
||||
serve_directory(url_text, &url, &archive, version);
|
||||
} else {
|
||||
if (info.entry.type != SLEEP_ENTRY_FILE) {
|
||||
dat_archive_close(&archive); return serve_not_found(url_text);
|
||||
}
|
||||
if (send_file(url_text, dpi_content_type(info.entry.path), &archive, &info.entry)) {
|
||||
dat_archive_close(&archive); return error_page(url_text, 500, "File content unavailable");
|
||||
}
|
||||
}
|
||||
dat_archive_close(&archive);
|
||||
fflush(stdout);
|
||||
return ferror(stdout) ? -1 : 0;
|
||||
}
|
||||
|
||||
static int serve_dashboard(const char *url)
|
||||
{
|
||||
char action[32], key[80], reload[2048];
|
||||
int has_action, has_key, has_reload;
|
||||
body_buffer_t body = {0};
|
||||
size_t i, count = 0;
|
||||
if (parse_query_value(url, "unswarm", action, sizeof(action)) > 0) {
|
||||
has_key = parse_query_value(url, "key", key, sizeof(key));
|
||||
has_reload = parse_query_value(url, "reload", reload, sizeof(reload));
|
||||
if (has_key > 0) {
|
||||
for (i = 0; i < DPI_ARCHIVES_MAX; i++)
|
||||
if (open_archives[i].used && !strcmp(open_archives[i].key, key))
|
||||
memset(&open_archives[i], 0, sizeof(open_archives[i]));
|
||||
send_status("unswarming archive");
|
||||
send_reload(has_reload > 0 ? reload : "dpi:/dat/");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
has_action = parse_query_value(url, "quit", action, sizeof(action));
|
||||
if (has_action > 0) {
|
||||
static const char quitting[] = "Quitting";
|
||||
send_page(url, "text/plain", quitting, sizeof(quitting) - 1);
|
||||
quit_requested = 1;
|
||||
return 0;
|
||||
}
|
||||
if (body_appendf(&body, "<!doctype html><html><head><title>Dat</title></head><body>")) goto fail;
|
||||
if (body_appendf(&body, "<form action=\"dpi:/dat/\"><input type=\"submit\" name=\"quit\" value=\"Quit\"></form><h3>Archives</h3>")) goto fail;
|
||||
for (i = 0; i < DPI_ARCHIVES_MAX; i++) if (open_archives[i].used) count++;
|
||||
if (!count && body_appendf(&body, "<p>None</p>")) goto fail;
|
||||
for (i = 0; i < DPI_ARCHIVES_MAX; i++) {
|
||||
dpi_archive_t *archive = &open_archives[i];
|
||||
if (!archive->used) continue;
|
||||
if (body_appendf(&body, "<p><a href=\"dat://%s\">", archive->key) ||
|
||||
html_append(&body, archive->title[0] ? archive->title : archive->key) ||
|
||||
body_appendf(&body, "</a>")) goto fail;
|
||||
if (archive->description[0] &&
|
||||
(body_appendf(&body, " - ") || html_append(&body, archive->description))) goto fail;
|
||||
if (body_appendf(&body, "<br>version <a href=\"dat://%s+%llu\">%llu</a></p>",
|
||||
archive->key, (unsigned long long)archive->version,
|
||||
(unsigned long long)archive->version) ||
|
||||
body_appendf(&body, "<blockquote><h3>Peers</h3>None")) goto fail;
|
||||
if (body_appendf(&body, "<form action=\"dpi:/dat/\"><input type=\"hidden\" name=\"key\" value=\"%s\"><input type=\"hidden\" name=\"reload\" value=\"dpi:/dat/\"><input type=\"submit\" name=\"unswarm\" value=\"unswarm\"></form></blockquote>", archive->key)) goto fail;
|
||||
}
|
||||
if (body_appendf(&body, "</body></html>")) goto fail;
|
||||
send_page(url, "text/html", body.data, body.length);
|
||||
free(body.data);
|
||||
return 0;
|
||||
fail:
|
||||
free(body.data);
|
||||
return error_page(url, 500, "Unable to build Dat dashboard");
|
||||
}
|
||||
|
||||
static int serve_internal(const char *url)
|
||||
{
|
||||
const char *path = strstr(url, "://");
|
||||
const char *query;
|
||||
char decoded[2048];
|
||||
size_t n;
|
||||
if (!strncmp(url, "dpi:/", 5)) path = url + 4;
|
||||
else if (path) path = strchr(path + 3, '/');
|
||||
if (!path) path = "/";
|
||||
query = strchr(path, '?');
|
||||
n = query ? (size_t)(query - path) : strlen(path);
|
||||
if (n >= sizeof(decoded)) return error_page(url, 400, "Internal DPI path is too long");
|
||||
memcpy(decoded, path, n); decoded[n] = '\0';
|
||||
if (!strcmp(decoded, "/dat/")) return serve_dashboard(url);
|
||||
return serve_not_found(url);
|
||||
}
|
||||
|
||||
static int serve_stdio_requests(void)
|
||||
{
|
||||
char tag[TAG_CAP];
|
||||
int result = read_tag(stdin, tag, sizeof(tag));
|
||||
if (result <= 0 || !authenticate(tag)) return 1;
|
||||
while (!quit_requested) {
|
||||
char *cmd, *url;
|
||||
result = read_tag(stdin, tag, sizeof(tag));
|
||||
if (result <= 0) break;
|
||||
cmd = attribute(tag, "cmd");
|
||||
url = attribute(tag, "url");
|
||||
if (cmd && !strcmp(cmd, "DpiBye")) { free(cmd); free(url); break; }
|
||||
if (cmd && !strcmp(cmd, "open_url") && url) {
|
||||
if (!strncmp(url, "dat:", 4)) serve(url);
|
||||
else if (!strncmp(url, "dpi:/", 5)) serve_internal(url);
|
||||
else error_page(url, 404, "Not found");
|
||||
}
|
||||
free(cmd);
|
||||
free(url);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int serve_socket_requests(void)
|
||||
{
|
||||
int listener = dup(STDIN_FILENO), stopped = 0;
|
||||
if (listener < 0) return -1;
|
||||
while (!stopped) {
|
||||
int client = accept(listener, NULL, NULL);
|
||||
int saved_in, saved_out, stop_after_request = 0;
|
||||
char tag[TAG_CAP];
|
||||
if (client < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(listener);
|
||||
return -1;
|
||||
}
|
||||
saved_in = dup(STDIN_FILENO);
|
||||
saved_out = dup(STDOUT_FILENO);
|
||||
if (saved_in < 0 || saved_out < 0 || dup2(client, STDIN_FILENO) < 0 ||
|
||||
dup2(client, STDOUT_FILENO) < 0) {
|
||||
if (saved_in >= 0) close(saved_in);
|
||||
if (saved_out >= 0) close(saved_out);
|
||||
close(client);
|
||||
close(listener);
|
||||
return -1;
|
||||
}
|
||||
clearerr(stdin);
|
||||
clearerr(stdout);
|
||||
if (read_tag(stdin, tag, sizeof(tag)) > 0 && authenticate(tag)) {
|
||||
while (!stop_after_request) {
|
||||
char *cmd, *url;
|
||||
int result = read_tag(stdin, tag, sizeof(tag));
|
||||
if (result <= 0) break;
|
||||
cmd = attribute(tag, "cmd");
|
||||
url = attribute(tag, "url");
|
||||
if (cmd && !strcmp(cmd, "DpiBye")) {
|
||||
stopped = 1;
|
||||
stop_after_request = 1;
|
||||
} else if (cmd && !strcmp(cmd, "open_url") && url) {
|
||||
if (!strncmp(url, "dat:", 4)) serve(url);
|
||||
else if (!strncmp(url, "dpi:/", 5)) serve_internal(url);
|
||||
else error_page(url, 404, "Not found");
|
||||
stop_after_request = 1;
|
||||
}
|
||||
free(cmd);
|
||||
free(url);
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
/* The historical dillo-dat plugin ends the accepted socket per page. */
|
||||
dup2(saved_in, STDIN_FILENO);
|
||||
dup2(saved_out, STDOUT_FILENO);
|
||||
close(saved_in);
|
||||
close(saved_out);
|
||||
close(client);
|
||||
clearerr(stdin);
|
||||
clearerr(stdout);
|
||||
}
|
||||
close(listener);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int result;
|
||||
(void)setlocale(LC_COLLATE, "");
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
metadata_scheme = s7_init();
|
||||
if (argc == 2 && !strcmp(argv[1], "--stdio-test"))
|
||||
result = serve_stdio_requests();
|
||||
else
|
||||
result = serve_socket_requests();
|
||||
if (metadata_scheme) s7_free(metadata_scheme);
|
||||
return result < 0 ? 1 : result;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef DAT_DPI_ICONS_H
|
||||
#define DAT_DPI_ICONS_H
|
||||
/* PNG data URIs copied from the bundled historical dillo-dat DPI. */
|
||||
static const char dat_dpi_icon_updir[] =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAKxJ"
|
||||
"REFUeNpi/P//PwMlgImBQjAMDGBBF2BkZISz09LSwCE8a9YsuCBGoIMEkDEMJCUl/b90+QoYg9i41LNgc1ZycvL/hMQkhgcPH4H5"
|
||||
"iUnJIJf9nzt3LiNBL2RkZPwPj4hk4BMUYuDh44MEFDMLQ0xsHAMrKyvIJYyEwuDLiuXLeP7+/Qv3EihcmJmZGZiYmL5gqEcPFKBi"
|
||||
"AyDFjCPQ/wLVX8BrwGhSJh0ABBgAsetR5KBfw9EAAAAASUVORK5CYII="
|
||||
;
|
||||
static const char dat_dpi_icon_directory[] =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0"
|
||||
"RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAXdEVYdEF1dGhvcgBMYXBvIENhbGFtYW5kcmVp35EaKgAAACl0RVh0"
|
||||
"RGVzY3JpcHRpb24AQmFzZWQgb2YgSmFrdWIgU3RlaW5lciBkZXNpZ26ghAVzAAABbElEQVQ4jaWQO0tDQRCFz2x2A8YHQoogaKFW"
|
||||
"2qSysbATsdAIWgrWlhIFBRvLoFhZW/gb0vgPRBAStEgExZA2VR7X3Nw7MxY3BhUjCU6zMOz5zrcL/HPo/HDzREFnZMj1tgoI1FPm"
|
||||
"/ePL/M2fgNxRxltaXh8xxkCEoSIQYQQdH6XHO6/T8ZePL/PFfgBLCifCqJQfesswDNBoNhAEnQQRFXLZjV+qAefiRQsAba/e27MI"
|
||||
"Wl4Ta1t7SE3N9lVXEVxfnaYtyJjS0z04DCMlF8fK6jaSyRQatUpfwFhypvsEUrOze4CxiUmoAlBF4LfwXq/1DUcG3UJhRmJ0HI1a"
|
||||
"9c/AzxGOAAYApEsbCiBfAMrDA5T5nwb8zYCHN/j8RABQFYAINGgYgEhUamPGKLOQiyciCFH3NABRdFsFqhoVqUJV4bebiBmjNmZd"
|
||||
"8eW5kJ6bXxhUAADw9lpWY12BLrKZRWNjt0EYTA8DsM5Vw7a/9gEhN65EVGzVRQAAAABJRU5ErkJggg="
|
||||
;
|
||||
static const char dat_dpi_icon_file[] =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0"
|
||||
"RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAASdEVYdFRpdGxlAFBhcGVyIFNoZWV0c7mvkfkAAAAXdEVYdEF1dGhv"
|
||||
"cgBMYXBvIENhbGFtYW5kcmVp35EaKgAAACd0RVh0RGVzY3JpcHRpb24Ad2l0aCBhIEhVR0UgaGVscCBmcm9tIEpha3VihlQHswAA"
|
||||
"AhNJREFUOI11kstqU1EUhr91ctI2A2uTNsRaOxDEkeILiIgTL6CCAx+iUnTSgQPBRxAFSxWhA8XiBQst7aQjUV+kMWlzOaeJVZvs"
|
||||
"y3JwctK0wQWLvQabb/3/v7eoKuubqzdFZMk5PwuKqqIKoAB/Qba8d8/v3b2/xfFSVVbXPpWbUUO990Pd7Xa0Uv2paxurf1Y+vnuc"
|
||||
"wA87AOh0OjP5iQL7v/dptWOacZ1ao0plZ5vdepV2q8Wt67dzxanik7fvlxcGBQQAxlgAqpUK5e0KO5Ua9d2IuNlmL/pFuVwhCAKu"
|
||||
"XrmWGx0Ze/pm+dXlFBAmAANAYSqPcy5p73DO4pwjE8OHzyuMZXNcvHAp9/3H1wXgWx9gjQGURi3CWjuU01S+xMkTBbxYgiCQg4OD"
|
||||
"Gy9ePsvMzz1yfQUKTBTGcc7iVVHv8T5V4hhhFJExzp09z8bmesarzwIpINkaN1s454YUpCWBkC706gcysEkG+clxnPNo7y/0PsMh"
|
||||
"QHoAa1CvwyFCQBAoipBcFY4eyWCtxTt/FCBAHO3h7P8tZMIMpeI0xlh8z+pABkLpVBG0J1UGVKQKVBARrDH9rAaeERq1iG63298Y"
|
||||
"hiFnZmf63rWXiTEGd9wCwOmZaUTkaA8ooJfpEEBEqnEcTRcKk//1n1a73QIkMtZ0EluqzD98cCfMhoum2y2pgpI84fEZlGx2pG6M"
|
||||
"mVtafP0F4B+wR1eZMTEGTgAAAABJRU5ErkJggg=="
|
||||
;
|
||||
#endif
|
||||
@@ -0,0 +1,617 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "dat_fetch.h"
|
||||
#include "dat.h"
|
||||
#include "dat_paths.h"
|
||||
#include "cesspool.h"
|
||||
#include "dat_session.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include "sleep_utp.h"
|
||||
|
||||
#include <sodium.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#define mkdir(path, mode) _mkdir(path)
|
||||
#define chmod(path, mode) 0
|
||||
#endif
|
||||
|
||||
#define FETCH_HELLO_BYTES 132u
|
||||
#define FETCH_ACK_BYTES 100u
|
||||
#define FETCH_RECORD_LIMIT (4u * 1024u * 1024u)
|
||||
|
||||
typedef struct fetch_client fetch_client_t;
|
||||
typedef struct fetch_transport {
|
||||
sleep_utp_t utp;
|
||||
fetch_client_t *owner;
|
||||
uint8_t peer_id[32];
|
||||
uint8_t local_nonce[32], remote_nonce[32];
|
||||
uint8_t auth[FETCH_HELLO_BYTES + FETCH_ACK_BYTES];
|
||||
size_t auth_length;
|
||||
uint8_t *tx, *rx;
|
||||
size_t tx_length, tx_offset, tx_capacity;
|
||||
size_t rx_length, rx_capacity;
|
||||
int used, authenticated, hello_sent, hello_received;
|
||||
int ack_sent, ack_received, waiting_incoming;
|
||||
} fetch_transport_t;
|
||||
|
||||
struct fetch_client {
|
||||
cesspool_t pool;
|
||||
sleep_keypair_t identity;
|
||||
sleep_feed_t metadata, content;
|
||||
dat_session_t session;
|
||||
uint8_t metadata_key[32], content_key[32], topic[32];
|
||||
fetch_transport_t transports[DAT_SESSION_MAX_PEERS];
|
||||
int pool_ready, session_ready, content_known, failed;
|
||||
int failure_errno;
|
||||
};
|
||||
|
||||
static uint64_t fetch_now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0;
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
static int ensure_dir(const char *path)
|
||||
{
|
||||
if (mkdir(path, 0700) == 0 || errno == EEXIST) return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int ensure_dir_tree(const char *path)
|
||||
{
|
||||
char copy[1200];
|
||||
size_t i, length;
|
||||
if (!path || !*path) { errno = EINVAL; return -1; }
|
||||
length = strlen(path);
|
||||
if (length >= sizeof(copy)) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(copy, path, length + 1);
|
||||
for (i = 0; i < length; i++) {
|
||||
if (copy[i] != '/' && copy[i] != '\\') continue;
|
||||
if (i == 0 || (i == 2 && copy[1] == ':')) continue;
|
||||
{
|
||||
char saved = copy[i];
|
||||
copy[i] = '\0';
|
||||
if (*copy && ensure_dir(copy)) return -1;
|
||||
copy[i] = saved;
|
||||
}
|
||||
}
|
||||
return ensure_dir(copy);
|
||||
}
|
||||
|
||||
/* Cached public keys identify the archive. Never replace a mismatched key
|
||||
* when a later request reuses this cache directory. */
|
||||
static int persist_pubkey(const char *path, const uint8_t key[32])
|
||||
{
|
||||
uint8_t existing[32];
|
||||
if (sleep_load_pubkey(path, existing) == 0) {
|
||||
if (memcmp(existing, key, sizeof(existing))) { errno = EACCES; return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (errno != ENOENT) return -1;
|
||||
return sleep_save_pubkey(path, key);
|
||||
}
|
||||
|
||||
static int load_identity(sleep_keypair_t *identity)
|
||||
{
|
||||
const char *home = dat_home_directory();
|
||||
char directory[1100], path[1200];
|
||||
int n;
|
||||
if (!home || !*home) { errno = ENOENT; return -1; }
|
||||
n = snprintf(directory, sizeof(directory), "%s/.dat", home);
|
||||
if (n < 0 || (size_t)n >= sizeof(directory) || ensure_dir(directory)) return -1;
|
||||
n = snprintf(path, sizeof(path), "%s/dillo-dat.network-key", directory);
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) { errno = ENAMETOOLONG; return -1; }
|
||||
if (sleep_load_keypair(path, identity) == 0) return 0;
|
||||
if (errno != ENOENT || sleep_gen_keypair(identity) ||
|
||||
sleep_save_keypair(path, identity) || chmod(path, 0600)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static fetch_transport_t *transport_by_id(fetch_client_t *client,
|
||||
const uint8_t peer_id[32])
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++)
|
||||
if (client->transports[i].used &&
|
||||
!memcmp(client->transports[i].peer_id, peer_id, 32))
|
||||
return &client->transports[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static fetch_transport_t *transport_create(fetch_client_t *client,
|
||||
const uint8_t peer_id[32])
|
||||
{
|
||||
size_t i;
|
||||
fetch_transport_t *transport = transport_by_id(client, peer_id);
|
||||
if (transport) return transport;
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++) {
|
||||
transport = &client->transports[i];
|
||||
if (transport->used) continue;
|
||||
memset(transport, 0, sizeof(*transport));
|
||||
if (sleep_utp_init(&transport->utp, client->pool.net.sock)) return NULL;
|
||||
transport->used = 1;
|
||||
transport->owner = client;
|
||||
memcpy(transport->peer_id, peer_id, 32);
|
||||
transport->utp.context = transport;
|
||||
return transport;
|
||||
}
|
||||
errno = ENOSPC;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int flush_transport(fetch_transport_t *transport)
|
||||
{
|
||||
while (transport->tx_offset < transport->tx_length &&
|
||||
transport->utp.state == SLEEP_UTP_CONNECTED) {
|
||||
size_t amount = transport->tx_length - transport->tx_offset;
|
||||
if (amount > SLEEP_UTP_MTU) amount = SLEEP_UTP_MTU;
|
||||
if (sleep_utp_send(&transport->utp, transport->tx + transport->tx_offset, amount)) {
|
||||
if (errno == EAGAIN || errno == ENOBUFS) return 0;
|
||||
return -1;
|
||||
}
|
||||
transport->tx_offset += amount;
|
||||
}
|
||||
if (transport->tx_offset == transport->tx_length)
|
||||
transport->tx_offset = transport->tx_length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int queue_transport(fetch_transport_t *transport, const uint8_t *bytes, size_t length)
|
||||
{
|
||||
size_t queued, needed, capacity;
|
||||
uint8_t *next;
|
||||
if (transport->tx_offset && transport->tx_length + length > transport->tx_capacity) {
|
||||
memmove(transport->tx, transport->tx + transport->tx_offset,
|
||||
transport->tx_length - transport->tx_offset);
|
||||
transport->tx_length -= transport->tx_offset;
|
||||
transport->tx_offset = 0;
|
||||
}
|
||||
queued = transport->tx_length - transport->tx_offset;
|
||||
if (length > FETCH_RECORD_LIMIT - queued) { errno = ENOBUFS; return -1; }
|
||||
needed = transport->tx_length + length;
|
||||
if (needed > transport->tx_capacity) {
|
||||
capacity = transport->tx_capacity ? transport->tx_capacity : 4096;
|
||||
while (capacity < needed) {
|
||||
if (capacity >= FETCH_RECORD_LIMIT / 2) { capacity = FETCH_RECORD_LIMIT; break; }
|
||||
capacity *= 2;
|
||||
}
|
||||
next = realloc(transport->tx, capacity);
|
||||
if (!next) return -1;
|
||||
transport->tx = next;
|
||||
transport->tx_capacity = capacity;
|
||||
}
|
||||
memcpy(transport->tx + transport->tx_length, bytes, length);
|
||||
transport->tx_length += length;
|
||||
return flush_transport(transport);
|
||||
}
|
||||
|
||||
static int send_session_frame(void *context, const uint8_t peer_id[32],
|
||||
const uint8_t *frame, size_t frame_len)
|
||||
{
|
||||
fetch_client_t *client = context;
|
||||
fetch_transport_t *transport = transport_by_id(client, peer_id);
|
||||
uint8_t *record;
|
||||
size_t prefix, written;
|
||||
int rc;
|
||||
if (!transport || !transport->authenticated) { errno = ENOTCONN; return -1; }
|
||||
if (frame_len > FETCH_RECORD_LIMIT) { errno = EMSGSIZE; return -1; }
|
||||
prefix = 1 + sleep_varint_size(frame_len);
|
||||
record = malloc(prefix + frame_len);
|
||||
if (!record) return -1;
|
||||
record[0] = 0;
|
||||
if (sleep_varint_encode(frame_len, record + 1, prefix - 1, &written)) { free(record); return -1; }
|
||||
memcpy(record + prefix, frame, frame_len);
|
||||
rc = queue_transport(transport, record, prefix + frame_len);
|
||||
free(record);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int send_hello(fetch_transport_t *transport)
|
||||
{
|
||||
uint8_t hello[FETCH_HELLO_BYTES];
|
||||
if (transport->hello_sent || transport->utp.state != SLEEP_UTP_CONNECTED) return 0;
|
||||
memcpy(hello, "THH0", 4);
|
||||
memcpy(hello + 4, transport->owner->identity.pubkey, 32);
|
||||
randombytes_buf(transport->local_nonce, sizeof(transport->local_nonce));
|
||||
memcpy(hello + 36, transport->local_nonce, 32);
|
||||
if (sleep_sign(hello + 68, hello, 68, &transport->owner->identity) ||
|
||||
sleep_utp_send(&transport->utp, hello, sizeof(hello))) return -1;
|
||||
transport->hello_sent = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_ack(fetch_transport_t *transport)
|
||||
{
|
||||
uint8_t ack[FETCH_ACK_BYTES], transcript[132];
|
||||
if (transport->ack_sent) return 0;
|
||||
memcpy(ack, "THA0", 4);
|
||||
memcpy(ack + 4, transport->remote_nonce, 32);
|
||||
memcpy(transcript, ack, 36);
|
||||
memcpy(transcript + 36, transport->local_nonce, 32);
|
||||
memcpy(transcript + 68, transport->owner->identity.pubkey, 32);
|
||||
memcpy(transcript + 100, transport->peer_id, 32);
|
||||
if (sleep_sign(ack + 36, transcript, sizeof(transcript), &transport->owner->identity) ||
|
||||
sleep_utp_send(&transport->utp, ack, sizeof(ack))) return -1;
|
||||
transport->ack_sent = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int activate_content(fetch_client_t *client)
|
||||
{
|
||||
sleep_metadata_header_t header;
|
||||
uint8_t *block = NULL;
|
||||
size_t length = 0;
|
||||
char key_path[1200];
|
||||
int n;
|
||||
if (client->content_known || !sleep_feed_has(&client->metadata, 0)) return 0;
|
||||
if (sleep_feed_read(&client->metadata, 0, &block, &length)) return -1;
|
||||
if (sleep_metadata_header_decode(&header, block, length) ||
|
||||
strcmp(header.type, "hyperdrive") || header.content_len != 32) {
|
||||
free(block); errno = EPROTO; return -1;
|
||||
}
|
||||
free(block);
|
||||
if (sleep_feed_load_remote(&client->content, header.content) ||
|
||||
dat_session_set_content_key(&client->session, header.content)) return -1;
|
||||
n = snprintf(key_path, sizeof(key_path), "%s/content.key", client->metadata.directory);
|
||||
if (n < 0 || (size_t)n >= sizeof(key_path) || persist_pubkey(key_path, header.content)) return -1;
|
||||
memcpy(client->content_key, header.content, 32);
|
||||
client->content_known = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int receive_records(fetch_transport_t *transport, const uint8_t *bytes, size_t length)
|
||||
{
|
||||
fetch_client_t *client = transport->owner;
|
||||
size_t need, capacity;
|
||||
uint8_t *next;
|
||||
if (length > FETCH_RECORD_LIMIT - transport->rx_length) { errno = ENOBUFS; return -1; }
|
||||
need = transport->rx_length + length;
|
||||
if (need > transport->rx_capacity) {
|
||||
capacity = transport->rx_capacity ? transport->rx_capacity : 4096;
|
||||
while (capacity < need) capacity = capacity > FETCH_RECORD_LIMIT / 2 ? FETCH_RECORD_LIMIT : capacity * 2;
|
||||
next = realloc(transport->rx, capacity);
|
||||
if (!next) return -1;
|
||||
transport->rx = next;
|
||||
transport->rx_capacity = capacity;
|
||||
}
|
||||
if (length) memcpy(transport->rx + transport->rx_length, bytes, length);
|
||||
transport->rx_length += length;
|
||||
for (;;) {
|
||||
uint64_t frame_len;
|
||||
size_t varint_len, prefix;
|
||||
int saved_errno;
|
||||
if (transport->rx_length < 2) return 0;
|
||||
if (transport->rx[0] != 0) { errno = EPROTO; return -1; }
|
||||
errno = 0;
|
||||
if (sleep_varint_decode(transport->rx + 1, transport->rx_length - 1,
|
||||
&frame_len, &varint_len)) {
|
||||
saved_errno = errno;
|
||||
if (saved_errno == EAGAIN) return 0;
|
||||
errno = saved_errno; return -1;
|
||||
}
|
||||
if (frame_len > FETCH_RECORD_LIMIT) { errno = EMSGSIZE; return -1; }
|
||||
prefix = 1 + varint_len;
|
||||
if (frame_len > transport->rx_length - prefix) return 0;
|
||||
if (dat_session_receive(&client->session, transport->peer_id,
|
||||
transport->rx + prefix, (size_t)frame_len) ||
|
||||
activate_content(client)) return -1;
|
||||
memmove(transport->rx, transport->rx + prefix + (size_t)frame_len,
|
||||
transport->rx_length - prefix - (size_t)frame_len);
|
||||
transport->rx_length -= prefix + (size_t)frame_len;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_utp_data(sleep_utp_t *utp, const uint8_t *bytes, size_t length)
|
||||
{
|
||||
fetch_transport_t *transport = utp ? utp->context : NULL;
|
||||
if (!transport || !transport->used) return;
|
||||
if (transport->authenticated) {
|
||||
if (receive_records(transport, bytes, length)) {
|
||||
transport->owner->failed = 1;
|
||||
transport->owner->failure_errno = errno ? errno : EPROTO;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (length > sizeof(transport->auth) - transport->auth_length) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = EMSGSIZE; return;
|
||||
}
|
||||
memcpy(transport->auth + transport->auth_length, bytes, length);
|
||||
transport->auth_length += length;
|
||||
for (;;) {
|
||||
if (!transport->hello_received && transport->auth_length >= FETCH_HELLO_BYTES) {
|
||||
const uint8_t *hello = transport->auth;
|
||||
if (memcmp(hello, "THH0", 4) || memcmp(hello + 4, transport->peer_id, 32) ||
|
||||
sleep_verify(hello + 68, hello, 68, transport->peer_id)) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = EACCES; return;
|
||||
}
|
||||
memcpy(transport->remote_nonce, hello + 36, 32);
|
||||
transport->hello_received = 1;
|
||||
memmove(transport->auth, transport->auth + FETCH_HELLO_BYTES,
|
||||
transport->auth_length - FETCH_HELLO_BYTES);
|
||||
transport->auth_length -= FETCH_HELLO_BYTES;
|
||||
if (send_ack(transport)) { transport->owner->failed = 1; transport->owner->failure_errno = errno; return; }
|
||||
continue;
|
||||
}
|
||||
if (transport->hello_received && !transport->ack_received &&
|
||||
transport->auth_length >= FETCH_ACK_BYTES) {
|
||||
const uint8_t *ack = transport->auth;
|
||||
uint8_t transcript[132];
|
||||
if (memcmp(ack, "THA0", 4) || memcmp(ack + 4, transport->local_nonce, 32)) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = EPROTO; return;
|
||||
}
|
||||
memcpy(transcript, ack, 36);
|
||||
memcpy(transcript + 36, transport->remote_nonce, 32);
|
||||
memcpy(transcript + 68, transport->peer_id, 32);
|
||||
memcpy(transcript + 100, transport->owner->identity.pubkey, 32);
|
||||
if (sleep_verify(ack + 36, transcript, sizeof(transcript), transport->peer_id)) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = EACCES; return;
|
||||
}
|
||||
transport->ack_received = 1;
|
||||
memmove(transport->auth, transport->auth + FETCH_ACK_BYTES,
|
||||
transport->auth_length - FETCH_ACK_BYTES);
|
||||
transport->auth_length -= FETCH_ACK_BYTES;
|
||||
transport->authenticated = 1;
|
||||
if (dat_session_peer_secure(&transport->owner->session, transport->peer_id, 1)) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = errno; return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (transport->authenticated && transport->auth_length) {
|
||||
if (receive_records(transport, transport->auth, transport->auth_length)) {
|
||||
transport->owner->failed = 1; transport->owner->failure_errno = errno; return;
|
||||
}
|
||||
transport->auth_length = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static int same_endpoint(const struct sockaddr_storage *a, const struct sockaddr_storage *b)
|
||||
{
|
||||
const struct sockaddr_in *aa = (const struct sockaddr_in *)a;
|
||||
const struct sockaddr_in *bb = (const struct sockaddr_in *)b;
|
||||
return aa->sin_family == AF_INET && bb->sin_family == AF_INET &&
|
||||
aa->sin_port == bb->sin_port && aa->sin_addr.s_addr == bb->sin_addr.s_addr;
|
||||
}
|
||||
|
||||
static void send_hello_if_ready(fetch_transport_t *transport)
|
||||
{
|
||||
if (transport->utp.state == SLEEP_UTP_CONNECTED && send_hello(transport)) {
|
||||
transport->owner->failed = 1;
|
||||
transport->owner->failure_errno = errno;
|
||||
}
|
||||
}
|
||||
|
||||
static void transport_packet(cesspool_t *pool, const dat_peer_t *source,
|
||||
const uint8_t *bytes, size_t length)
|
||||
{
|
||||
fetch_client_t *client = pool ? pool->transport_context : NULL;
|
||||
sleep_utp_packet_t packet;
|
||||
fetch_transport_t *transport = NULL;
|
||||
cesspool_peer_t *known = NULL;
|
||||
size_t i;
|
||||
if (!client || sleep_utp_packet_decode(&packet, bytes, length)) return;
|
||||
if (packet.type == SLEEP_UTP_SYN) {
|
||||
for (i = 0; i < pool->peer_count; i++)
|
||||
if (pool->peers[i].discovered &&
|
||||
!memcmp(pool->peers[i].discovery_topic, client->topic, 32) &&
|
||||
same_endpoint(&pool->peers[i].address.addr, &source->addr)) {
|
||||
known = &pool->peers[i]; break;
|
||||
}
|
||||
if (!known || memcmp(client->identity.pubkey, known->public_key, 32) < 0) return;
|
||||
transport = transport_by_id(client, known->public_key);
|
||||
if (!transport) transport = transport_create(client, known->public_key);
|
||||
if (!transport || transport->utp.state == SLEEP_UTP_CONNECTED) return;
|
||||
transport->waiting_incoming = 0;
|
||||
transport->utp.on_data = on_utp_data;
|
||||
if (sleep_utp_accept(&transport->utp, &packet,
|
||||
(const struct sockaddr *)&source->addr, source->addrlen)) return;
|
||||
send_hello_if_ready(transport);
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++) {
|
||||
fetch_transport_t *candidate = &client->transports[i];
|
||||
if (candidate->used && candidate->utp.receive_id == packet.connection_id &&
|
||||
same_endpoint(&candidate->utp.remote, &source->addr)) { transport = candidate; break; }
|
||||
}
|
||||
if (!transport) return;
|
||||
if (sleep_utp_input(&transport->utp, bytes, length)) return;
|
||||
send_hello_if_ready(transport);
|
||||
}
|
||||
|
||||
static void peer_discovered(void *context, const dat_session_peer_t *peer)
|
||||
{
|
||||
fetch_client_t *client = context;
|
||||
fetch_transport_t *transport = transport_create(client, peer->peer_id);
|
||||
if (!transport) return;
|
||||
transport->utp.on_data = on_utp_data;
|
||||
if (memcmp(client->identity.pubkey, peer->peer_id, 32) > 0) {
|
||||
transport->waiting_incoming = 1;
|
||||
return;
|
||||
}
|
||||
if (sleep_utp_connect(&transport->utp,
|
||||
(const struct sockaddr *)&peer->address.addr,
|
||||
peer->address.addrlen)) {
|
||||
transport->owner->failed = 1;
|
||||
transport->owner->failure_errno = errno;
|
||||
}
|
||||
}
|
||||
|
||||
static int open_remote_feed(sleep_feed_t *feed, const char *directory,
|
||||
const char *name, const uint8_t key[32])
|
||||
{
|
||||
if (sleep_feed_open(feed, directory, name, NULL) != 0) {
|
||||
if (errno != ENOENT) return -1;
|
||||
return sleep_feed_open_remote(feed, directory, name, key);
|
||||
}
|
||||
if (sleep_feed_load_remote(feed, key)) { sleep_feed_close(feed); return -1; }
|
||||
feed->persist_remote = 1;
|
||||
memcpy(feed->keypair.pubkey, key, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int feed_complete(const sleep_feed_t *feed, const sleep_replication_t *replication)
|
||||
{
|
||||
uint64_t i;
|
||||
if (!replication->remote_length || replication->remote_length > (uint64_t)SLEEP_BITFIELD_DATA_BYTES * 8u) return 0;
|
||||
for (i = 0; i < replication->remote_length; i++) if (!sleep_feed_has(feed, (size_t)i)) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int archive_complete(const fetch_client_t *client)
|
||||
{
|
||||
size_t p;
|
||||
for (p = 0; p < client->session.peer_count; p++) {
|
||||
const dat_session_peer_t *peer = &client->session.peers[p];
|
||||
if (peer->state != DAT_SESSION_PEER_READY) continue;
|
||||
if (!feed_complete(&client->metadata, &peer->replication[DAT_SESSION_METADATA])) continue;
|
||||
if (!client->content_known ||
|
||||
!feed_complete(&client->content, &peer->replication[DAT_SESSION_CONTENT])) continue;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cleanup(fetch_client_t *client)
|
||||
{
|
||||
size_t i;
|
||||
if (client->session_ready) (void)dat_session_close(&client->session);
|
||||
if (client->pool_ready) (void)cesspool_destroy(&client->pool);
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++) {
|
||||
fetch_transport_t *transport = &client->transports[i];
|
||||
if (transport->used) (void)sleep_utp_close(&transport->utp);
|
||||
free(transport->tx); free(transport->rx);
|
||||
}
|
||||
sleep_feed_close(&client->metadata);
|
||||
sleep_feed_close(&client->content);
|
||||
}
|
||||
|
||||
int dat_fetch_archive(const char *root, const uint8_t metadata_key[32],
|
||||
unsigned timeout_ms)
|
||||
{
|
||||
fetch_client_t *client;
|
||||
char datdir[1200], path[1400];
|
||||
uint8_t unknown_content[32] = {0};
|
||||
uint16_t port = DATNET_PORT;
|
||||
uint64_t deadline;
|
||||
int n, rc = -1, bootstrap_count;
|
||||
if (!root || !metadata_key) { errno = EINVAL; return -1; }
|
||||
client = calloc(1, sizeof(*client));
|
||||
if (!client) return -1;
|
||||
memcpy(client->metadata_key, metadata_key, 32);
|
||||
if (sleep_crypto_init() || load_identity(&client->identity)) goto done;
|
||||
if (ensure_dir_tree(root)) goto done;
|
||||
n = snprintf(datdir, sizeof(datdir), "%s/.dat", root);
|
||||
if (n < 0 || (size_t)n >= sizeof(datdir) || ensure_dir(datdir)) goto done;
|
||||
n = snprintf(path, sizeof(path), "%s/metadata.key", datdir);
|
||||
if (n < 0 || (size_t)n >= sizeof(path) || persist_pubkey(path, metadata_key)) goto done;
|
||||
if (open_remote_feed(&client->metadata, datdir, "metadata", metadata_key) ||
|
||||
sleep_feed_open_remote(&client->content, datdir, "content", unknown_content)) goto done;
|
||||
n = snprintf(path, sizeof(path), "%s/content.key", datdir);
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) { errno = ENAMETOOLONG; goto done; }
|
||||
if (sleep_load_pubkey(path, client->content_key) == 0) {
|
||||
if (sleep_feed_load_remote(&client->content, client->content_key)) goto done;
|
||||
client->content.persist_remote = 1;
|
||||
memcpy(client->content.keypair.pubkey, client->content_key, 32);
|
||||
client->content_known = 1;
|
||||
} else if (errno != ENOENT) goto done;
|
||||
if (cesspool_init(&client->pool, client->identity.pubkey)) goto done;
|
||||
client->pool_ready = 1;
|
||||
client->pool.on_transport_packet = transport_packet;
|
||||
client->pool.transport_context = client;
|
||||
if (cesspool_listen(&client->pool, "0.0.0.0", port)) {
|
||||
if (errno != EADDRINUSE || cesspool_listen(&client->pool, "0.0.0.0", 0)) goto done;
|
||||
}
|
||||
bootstrap_count = cesspool_configure_bootstraps(&client->pool);
|
||||
if (bootstrap_count < 0) goto done;
|
||||
if (cesspool_discovery_key(metadata_key, client->topic) ||
|
||||
dat_session_init(&client->session, &client->pool, metadata_key,
|
||||
client->identity.pubkey, client->pool.port,
|
||||
&client->metadata, metadata_key, &client->content,
|
||||
client->content_known ? client->content_key : unknown_content,
|
||||
send_session_frame, client)) goto done;
|
||||
client->session_ready = 1;
|
||||
if (!client->content_known && dat_session_expect_content_key(&client->session)) goto done;
|
||||
if (dat_session_set_peer_callback(&client->session, peer_discovered, client) ||
|
||||
dat_session_start(&client->session, 1, 1)) goto done;
|
||||
deadline = fetch_now_ms() + (timeout_ms ? timeout_ms : 30000u);
|
||||
while (fetch_now_ms() < deadline) {
|
||||
size_t i;
|
||||
if (client->failed) { errno = client->failure_errno ? client->failure_errno : EIO; goto done; }
|
||||
if (dat_session_tick(&client->session, fetch_now_ms()) < 0 && errno != EINTR) goto done;
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++) {
|
||||
fetch_transport_t *transport = &client->transports[i];
|
||||
if (!transport->used) continue;
|
||||
if (transport->waiting_incoming && transport->utp.state == SLEEP_UTP_CLOSED) continue;
|
||||
if (sleep_utp_tick(&transport->utp, fetch_now_ms()) != 0 ||
|
||||
transport->utp.state == SLEEP_UTP_RESET ||
|
||||
transport->utp.state == SLEEP_UTP_CLOSED) {
|
||||
transport->used = 0;
|
||||
continue;
|
||||
}
|
||||
if (flush_transport(transport)) goto done;
|
||||
}
|
||||
if (archive_complete(client)) { rc = 0; goto done; }
|
||||
{
|
||||
struct timespec pause = {0, 20000000};
|
||||
nanosleep(&pause, NULL);
|
||||
}
|
||||
}
|
||||
if (!bootstrap_count && !client->session.peer_count) errno = ENETUNREACH;
|
||||
else if (bootstrap_count && !client->pool.bootstrap_reachable) errno = EHOSTUNREACH;
|
||||
else if (!client->session.peer_count) errno = ENOENT;
|
||||
else {
|
||||
size_t i, secure = 0, ready = 0, active_utp = 0, auth = 0;
|
||||
for (i = 0; i < client->session.peer_count; i++) {
|
||||
if (client->session.peers[i].state >= DAT_SESSION_PEER_SECURE &&
|
||||
client->session.peers[i].state != DAT_SESSION_PEER_CLOSED) secure++;
|
||||
if (client->session.peers[i].state == DAT_SESSION_PEER_READY) ready++;
|
||||
}
|
||||
for (i = 0; i < DAT_SESSION_MAX_PEERS; i++) if (client->transports[i].used) {
|
||||
active_utp += client->transports[i].utp.state == SLEEP_UTP_CONNECTED;
|
||||
auth += client->transports[i].authenticated;
|
||||
}
|
||||
fprintf(stderr, "dat-fetch: bootstrap reachable; peers=%lu secure=%lu ready=%lu utp=%lu authenticated=%lu\n",
|
||||
(unsigned long)client->session.peer_count, (unsigned long)secure,
|
||||
(unsigned long)ready, (unsigned long)active_utp, (unsigned long)auth);
|
||||
for (i = 0; i < client->session.peer_count; i++) {
|
||||
const dat_session_peer_t *peer = &client->session.peers[i];
|
||||
const struct sockaddr_in *address = (const struct sockaddr_in *)&peer->address.addr;
|
||||
char host[INET_ADDRSTRLEN] = "?";
|
||||
char peer_hex[65];
|
||||
size_t j;
|
||||
fetch_transport_t *transport = transport_by_id(client, peer->peer_id);
|
||||
for (j = 0; j < 32; j++) snprintf(peer_hex + j * 2, 3, "%02x", peer->peer_id[j]);
|
||||
if (address->sin_family == AF_INET)
|
||||
(void)inet_ntop(AF_INET, &address->sin_addr, host, sizeof(host));
|
||||
fprintf(stderr, "dat-fetch: peer=%s state=%d endpoint=%s:%u local-id-order=%d waiting-incoming=%d utp-state=%d\n",
|
||||
peer_hex, peer->state, host, (unsigned)ntohs(address->sin_port),
|
||||
memcmp(client->identity.pubkey, peer->peer_id, 32),
|
||||
transport ? transport->waiting_incoming : 0,
|
||||
transport ? transport->utp.state : SLEEP_UTP_CLOSED);
|
||||
}
|
||||
errno = ETIMEDOUT;
|
||||
}
|
||||
done:
|
||||
if (rc != 0 && !errno) errno = EIO;
|
||||
{
|
||||
int saved = errno;
|
||||
cleanup(client);
|
||||
free(client);
|
||||
errno = saved;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
#include "dat_net.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#ifndef SIO_UDP_CONNRESET
|
||||
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
|
||||
#endif
|
||||
static int datnet_winsock_ready;
|
||||
#else
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <poll.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
static int datnet_winsock_errno(int error)
|
||||
{
|
||||
switch (error) {
|
||||
case WSAEINTR: return EINTR;
|
||||
case WSAEACCES: return EACCES;
|
||||
case WSAEADDRINUSE: return EADDRINUSE;
|
||||
case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL;
|
||||
case WSAENETDOWN: return ENETDOWN;
|
||||
case WSAENETUNREACH: return ENETUNREACH;
|
||||
case WSAEHOSTUNREACH: return EHOSTUNREACH;
|
||||
case WSAECONNRESET: return ECONNRESET;
|
||||
case WSAECONNREFUSED: return ECONNREFUSED;
|
||||
case WSAETIMEDOUT: return ETIMEDOUT;
|
||||
case WSAEMSGSIZE: return EMSGSIZE;
|
||||
case WSAENOBUFS: return ENOBUFS;
|
||||
case WSAEWOULDBLOCK: return EAGAIN;
|
||||
default: return EIO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int datnet_init(datnet_t *net, const uint8_t topic[32], const uint8_t peerid[32]) {
|
||||
if (!net || !topic || !peerid) { errno = EINVAL; return -1; }
|
||||
#ifdef _WIN32
|
||||
if (!datnet_winsock_ready) { WSADATA data; if (WSAStartup(MAKEWORD(2,2), &data)) { errno = ENETDOWN; return -1; } datnet_winsock_ready = 1; }
|
||||
#endif
|
||||
memset(net, 0, sizeof(*net)); net->sock = DAT_INVALID_SOCKET; memcpy(net->topic, topic, 32); memcpy(net->peerid, peerid, 32); return 0;
|
||||
}
|
||||
int datnet_bind(datnet_t *net, const char *host, uint16_t port) {
|
||||
struct sockaddr_in address; int broadcast = 1;
|
||||
if (!net) { errno = EINVAL; return -1; }
|
||||
memset(&address, 0, sizeof(address)); address.sin_family = AF_INET; address.sin_port = htons(port ? port : DATNET_PORT);
|
||||
if (!host || !*host || !strcmp(host, "*")) address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
else {
|
||||
struct addrinfo hints, *resolved = NULL;
|
||||
memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM;
|
||||
if (getaddrinfo(host, NULL, &hints, &resolved) != 0 || !resolved) {
|
||||
errno = EADDRNOTAVAIL; return -1;
|
||||
}
|
||||
address.sin_addr = ((struct sockaddr_in *)resolved->ai_addr)->sin_addr;
|
||||
freeaddrinfo(resolved);
|
||||
}
|
||||
net->sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (net->sock == DAT_INVALID_SOCKET) {
|
||||
#ifdef _WIN32
|
||||
errno = ENETDOWN;
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (setsockopt(net->sock, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast))) {
|
||||
int error = WSAGetLastError();
|
||||
closesocket(net->sock); net->sock = DAT_INVALID_SOCKET;
|
||||
errno = datnet_winsock_errno(error);
|
||||
return -1;
|
||||
}
|
||||
/* A peer that is not listening can send an ICMP port-unreachable response.
|
||||
* Winsock reports that asynchronously as WSAECONNRESET on a UDP socket;
|
||||
* that is a peer/network condition, not a dead local socket. */
|
||||
{
|
||||
BOOL disable_udp_connreset = FALSE;
|
||||
DWORD returned = 0;
|
||||
(void)WSAIoctl(net->sock, SIO_UDP_CONNRESET, &disable_udp_connreset,
|
||||
sizeof(disable_udp_connreset), NULL, 0, &returned, NULL, NULL);
|
||||
}
|
||||
if (bind(net->sock, (struct sockaddr *)&address, sizeof(address))) {
|
||||
int error = WSAGetLastError();
|
||||
closesocket(net->sock); net->sock = DAT_INVALID_SOCKET;
|
||||
errno = error == WSAEADDRINUSE ? EADDRINUSE :
|
||||
error == WSAEACCES ? EACCES : EIO;
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
if (setsockopt(net->sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast))) {
|
||||
int saved = errno;
|
||||
close(net->sock); net->sock = DAT_INVALID_SOCKET; errno = saved;
|
||||
return -1;
|
||||
}
|
||||
if (bind(net->sock, (struct sockaddr *)&address, sizeof(address))) { close(net->sock); net->sock = DAT_INVALID_SOCKET; return -1; }
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
int datnet_poll(datnet_t *net, int timeout_ms) {
|
||||
uint8_t packet[DATNET_MAX_PACKET], peer_id[DATNET_PEERID_BYTES]; struct sockaddr_storage source; socklen_t source_len = sizeof(source); ssize_t n; dat_peer_t peer;
|
||||
if (!net || net->sock == DAT_INVALID_SOCKET) { errno = EINVAL; return -1; }
|
||||
memcpy(peer_id, net->peerid, sizeof(peer_id));
|
||||
#ifdef _WIN32
|
||||
{ WSAPOLLFD pfd; int ready; pfd.fd = net->sock; pfd.events = POLLRDNORM; pfd.revents = 0;
|
||||
ready = WSAPoll(&pfd, 1, timeout_ms);
|
||||
if (ready == SOCKET_ERROR) { errno = datnet_winsock_errno(WSAGetLastError()); return -1; }
|
||||
if (ready == 0) return 0;
|
||||
if (pfd.revents & (POLLHUP | POLLNVAL)) { errno = EIO; return -1; }
|
||||
if (pfd.revents & POLLERR) {
|
||||
int socket_error = 0, error_length = sizeof(socket_error);
|
||||
if (getsockopt(net->sock, SOL_SOCKET, SO_ERROR, (char *)&socket_error,
|
||||
&error_length) == SOCKET_ERROR) {
|
||||
errno = datnet_winsock_errno(WSAGetLastError());
|
||||
return -1;
|
||||
}
|
||||
if (socket_error == WSAECONNRESET || socket_error == WSAECONNREFUSED ||
|
||||
socket_error == WSAEHOSTUNREACH || socket_error == WSAENETUNREACH)
|
||||
return 0;
|
||||
if (socket_error) { errno = datnet_winsock_errno(socket_error); return -1; }
|
||||
}
|
||||
if (!(pfd.revents & POLLRDNORM)) return 0; }
|
||||
#else
|
||||
{ struct pollfd pfd; int ready; pfd.fd = net->sock; pfd.events = POLLIN; pfd.revents = 0;
|
||||
ready = poll(&pfd, 1, timeout_ms);
|
||||
if (ready < 0) return -1;
|
||||
if (ready == 0) return 0;
|
||||
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { errno = EIO; return -1; }
|
||||
if (!(pfd.revents & POLLIN)) return 0; }
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
{ int source_length = (int)source_len; n = recvfrom(net->sock, (char *)packet, sizeof(packet), 0, (struct sockaddr *)&source, &source_length); source_len = (socklen_t)source_length;
|
||||
if (n < 0) { errno = datnet_winsock_errno(WSAGetLastError()); return -1; } }
|
||||
#else
|
||||
n = recvfrom(net->sock, packet, sizeof(packet), 0, (struct sockaddr *)&source, &source_len); if (n < 0) return -1;
|
||||
#endif
|
||||
if (n < 0) return -1;
|
||||
memset(&peer, 0, sizeof(peer)); peer.fd = net->sock; peer.addr = source; peer.addrlen = source_len;
|
||||
/* Cesspool decodes discovery records here; other datagrams are passed to
|
||||
* its authenticated peer-transport hook. */
|
||||
if (net->on_message) net->on_message(net, &peer, packet, (size_t)n);
|
||||
/* The identity bound to this UDP socket is immutable. Keep it intact even
|
||||
* if a protocol callback uses an aliased view while handling the packet. */
|
||||
memcpy(net->peerid, peer_id, sizeof(peer_id));
|
||||
return 1;
|
||||
}
|
||||
int datnet_send(datnet_t *net, const dat_peer_t *peer, const uint8_t *message, size_t length) {
|
||||
ssize_t n; if (!net || !peer || net->sock == DAT_INVALID_SOCKET || (!message && length) || length > DATNET_MAX_PACKET) { errno = EINVAL; return -1; }
|
||||
#ifdef _WIN32
|
||||
n = sendto(net->sock, (const char *)message, (int)length, 0, (const struct sockaddr *)&peer->addr, (int)peer->addrlen);
|
||||
if (n == SOCKET_ERROR) { errno = datnet_winsock_errno(WSAGetLastError()); return -1; }
|
||||
#else
|
||||
n = sendto(net->sock, message, length, 0, (const struct sockaddr *)&peer->addr, peer->addrlen);
|
||||
if (n < 0) return -1;
|
||||
#endif
|
||||
return n == (ssize_t)length ? 0 : -1;
|
||||
}
|
||||
int datnet_close(datnet_t *net) { int rc = 0; if (!net) { errno = EINVAL; return -1; } if (net->sock != DAT_INVALID_SOCKET) {
|
||||
#ifdef _WIN32
|
||||
rc = closesocket(net->sock);
|
||||
#else
|
||||
rc = close(net->sock);
|
||||
#endif
|
||||
} net->sock = DAT_INVALID_SOCKET; return rc; }
|
||||
@@ -0,0 +1,884 @@
|
||||
#include "dat_session.h"
|
||||
#include "errno_compat.h"
|
||||
|
||||
#include "sleep.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include "sleep_proof.h"
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
#include <sodium.h>
|
||||
#endif
|
||||
|
||||
#define DAT_SESSION_FRAME_BYTES (128u * 1024u)
|
||||
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
static const uint64_t feed_channels[DAT_SESSION_FEED_COUNT] = {0, 1};
|
||||
#else
|
||||
static const uint64_t feed_channels[DAT_SESSION_FEED_COUNT] = {1, 2};
|
||||
#endif
|
||||
|
||||
static int valid_feed(enum dat_session_feed feed)
|
||||
{
|
||||
return feed >= DAT_SESSION_METADATA && feed < DAT_SESSION_FEED_COUNT;
|
||||
}
|
||||
|
||||
static int feed_for_channel(uint64_t channel, enum dat_session_feed *feed)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < DAT_SESSION_FEED_COUNT; i++) if (feed_channels[i] == channel) {
|
||||
*feed = (enum dat_session_feed)i;
|
||||
return 0;
|
||||
}
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int discover_known_peers(dat_session_t *session)
|
||||
{
|
||||
cesspool_peer_t *peers[CESSPOOL_MAX_PEERS];
|
||||
size_t count, i;
|
||||
count = cesspool_topic_peers(session->cesspool, session->discovery_topic,
|
||||
peers, CESSPOOL_MAX_PEERS);
|
||||
for (i = 0; i < count; i++)
|
||||
if (dat_session_peer_discovered(session, peers[i])) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
dat_session_peer_t *dat_session_find_peer(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES])
|
||||
{
|
||||
size_t i;
|
||||
if (!session || !peer_id) return NULL;
|
||||
for (i = 0; i < session->peer_count; i++)
|
||||
if (!memcmp(session->peers[i].peer_id, peer_id, DAT_PEER_ID_BYTES))
|
||||
return &session->peers[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int dat_session_init(dat_session_t *session, cesspool_t *pool,
|
||||
const uint8_t archive_key[DAT_KEY_BYTES],
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES], uint16_t port,
|
||||
sleep_feed_t *metadata_feed,
|
||||
const uint8_t metadata_key[DAT_KEY_BYTES],
|
||||
sleep_feed_t *content_feed,
|
||||
const uint8_t content_key[DAT_KEY_BYTES],
|
||||
dat_session_send_fn send, void *context)
|
||||
{
|
||||
if (!session || !pool || !archive_key || !peer_id || !metadata_feed ||
|
||||
!metadata_key || !content_feed || !content_key || !send) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
memset(session, 0, sizeof(*session));
|
||||
memcpy(session->archive_key, archive_key, DAT_KEY_BYTES);
|
||||
memcpy(session->peer_id, peer_id, DAT_PEER_ID_BYTES);
|
||||
memcpy(session->feed_keys[DAT_SESSION_METADATA], metadata_key, DAT_KEY_BYTES);
|
||||
memcpy(session->feed_keys[DAT_SESSION_CONTENT], content_key, DAT_KEY_BYTES);
|
||||
session->content_key_known = 1;
|
||||
if (cesspool_discovery_key(archive_key, session->discovery_topic)) return -1;
|
||||
session->cesspool = pool;
|
||||
session->feeds[DAT_SESSION_METADATA] = metadata_feed;
|
||||
session->feeds[DAT_SESSION_CONTENT] = content_feed;
|
||||
session->send = send;
|
||||
session->context = context;
|
||||
session->port = port;
|
||||
if (session->port == 0) session->port = pool->port;
|
||||
session->state = DAT_SESSION_OPEN;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_start(dat_session_t *session, int announce, int lookup)
|
||||
{
|
||||
if (!session || session->state != DAT_SESSION_OPEN || (!announce && !lookup)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
session->announcing = !!announce;
|
||||
session->looking_up = !!lookup;
|
||||
if (cesspool_join(session->cesspool, session->discovery_topic,
|
||||
session->port, announce, lookup)) {
|
||||
(void)cesspool_leave(session->cesspool, session->discovery_topic);
|
||||
session->state = DAT_SESSION_FAILED;
|
||||
return -1;
|
||||
}
|
||||
session->state = DAT_SESSION_DISCOVERING;
|
||||
return discover_known_peers(session);
|
||||
}
|
||||
|
||||
int dat_session_peer_discovered(dat_session_t *session,
|
||||
const cesspool_peer_t *discovered_peer)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
if (!session || !discovered_peer || (session->state != DAT_SESSION_DISCOVERING &&
|
||||
session->state != DAT_SESSION_ACTIVE)) { errno = EINVAL; return -1; }
|
||||
peer = dat_session_find_peer(session, discovered_peer->public_key);
|
||||
if (peer) {
|
||||
int retry = peer->state == DAT_SESSION_PEER_CLOSED;
|
||||
peer->address = discovered_peer->address;
|
||||
if (retry) {
|
||||
peer->state = DAT_SESSION_PEER_DISCOVERED;
|
||||
peer->transport_authenticated = 0;
|
||||
free(peer->rx_buffer);
|
||||
peer->rx_buffer = NULL;
|
||||
peer->rx_buffer_len = peer->rx_buffer_capacity = 0;
|
||||
peer->tx_offset = peer->rx_offset = 0;
|
||||
peer->remote_feed_mask = 0;
|
||||
peer->local_feed_mask = 0;
|
||||
peer->remote_content_pending = 0;
|
||||
memset(peer->remote_content_discovery, 0,
|
||||
sizeof(peer->remote_content_discovery));
|
||||
peer->tx_stream_encrypted = peer->rx_stream_encrypted = 0;
|
||||
if (session->on_peer) session->on_peer(session->peer_context, peer);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (session->peer_count >= DAT_SESSION_MAX_PEERS) { errno = ENOSPC; return -1; }
|
||||
peer = &session->peers[session->peer_count];
|
||||
memset(peer, 0, sizeof(*peer));
|
||||
memcpy(peer->peer_id, discovered_peer->public_key, DAT_PEER_ID_BYTES);
|
||||
peer->address = discovered_peer->address;
|
||||
peer->state = DAT_SESSION_PEER_DISCOVERED;
|
||||
if (sleep_replication_init(&peer->replication[DAT_SESSION_METADATA],
|
||||
session->feeds[DAT_SESSION_METADATA],
|
||||
feed_channels[DAT_SESSION_METADATA]) ||
|
||||
sleep_replication_init(&peer->replication[DAT_SESSION_CONTENT],
|
||||
session->feeds[DAT_SESSION_CONTENT],
|
||||
feed_channels[DAT_SESSION_CONTENT])) return -1;
|
||||
session->peer_count++;
|
||||
if (session->on_peer) session->on_peer(session->peer_context, peer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_set_peer_callback(dat_session_t *session,
|
||||
dat_session_peer_fn callback, void *context)
|
||||
{
|
||||
size_t i;
|
||||
if (!session || session->state == DAT_SESSION_CLOSED ||
|
||||
session->state == DAT_SESSION_CLOSING) { errno = EINVAL; return -1; }
|
||||
session->on_peer = callback;
|
||||
session->peer_context = context;
|
||||
if (callback)
|
||||
for (i = 0; i < session->peer_count; i++)
|
||||
if (session->peers[i].state == DAT_SESSION_PEER_DISCOVERED)
|
||||
callback(context, &session->peers[i]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_set_transport_poll(dat_session_t *session,
|
||||
dat_session_poll_fn poll, void *context)
|
||||
{
|
||||
if (!session || session->state == DAT_SESSION_CLOSED ||
|
||||
session->state == DAT_SESSION_CLOSING) { errno = EINVAL; return -1; }
|
||||
session->poll_transport = poll;
|
||||
session->poll_context = context;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_frame(dat_session_t *session, dat_session_peer_t *peer,
|
||||
uint64_t channel, uint64_t type,
|
||||
const uint8_t *payload, size_t payload_len)
|
||||
{
|
||||
dat_wire_message_t message;
|
||||
uint8_t frame[DAT_SESSION_FRAME_BYTES];
|
||||
size_t frame_len;
|
||||
int send_result;
|
||||
message.channel = channel;
|
||||
message.type = type;
|
||||
message.payload = payload;
|
||||
message.payload_len = payload_len;
|
||||
if (dat_wire_encode(&message, frame, sizeof(frame), &frame_len)) return -1;
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
if (peer->tx_stream_encrypted) {
|
||||
if (sleep_stream_xsalsa20_xor(frame, frame, frame_len,
|
||||
peer->tx_nonce,
|
||||
session->feed_keys[DAT_SESSION_METADATA],
|
||||
peer->tx_offset)) return -1;
|
||||
if (peer->tx_offset > UINT64_MAX - frame_len) { errno = EOVERFLOW; return -1; }
|
||||
}
|
||||
#endif
|
||||
send_result = session->send(session->context, peer->peer_id, frame, frame_len);
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
if (!send_result && peer->tx_stream_encrypted) peer->tx_offset += frame_len;
|
||||
#endif
|
||||
return send_result;
|
||||
}
|
||||
|
||||
static int send_have_range(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed, uint64_t start,
|
||||
uint64_t length)
|
||||
{
|
||||
dat_range_t range = {start, length};
|
||||
uint8_t payload[32];
|
||||
size_t payload_len;
|
||||
if (!range.length) return 0;
|
||||
if (dat_range_encode(&range, payload, sizeof(payload), &payload_len)) return -1;
|
||||
return send_frame(session, peer, feed_channels[feed], DAT_MSG_HAVE,
|
||||
payload, payload_len);
|
||||
}
|
||||
|
||||
static int send_have(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed)
|
||||
{
|
||||
size_t index, length = sleep_feed_length(session->feeds[feed]);
|
||||
size_t run_start = 0;
|
||||
int in_run = 0;
|
||||
if (length > SLEEP_BITFIELD_DATA_BYTES * 8u)
|
||||
length = SLEEP_BITFIELD_DATA_BYTES * 8u;
|
||||
for (index = 0; index < length; index++) {
|
||||
int have = sleep_feed_has(session->feeds[feed], index);
|
||||
if (have && !in_run) { run_start = index; in_run = 1; }
|
||||
if (!have && in_run) {
|
||||
if (send_have_range(session, peer, feed, run_start, index - run_start)) return -1;
|
||||
in_run = 0;
|
||||
}
|
||||
}
|
||||
if (in_run) return send_have_range(session, peer, feed, run_start, length - run_start);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int answer_want(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed, const dat_range_t *range)
|
||||
{
|
||||
uint64_t i, end, run_start = 0;
|
||||
int in_run = 0;
|
||||
uint64_t feed_length = sleep_feed_length(session->feeds[feed]);
|
||||
if (range->start > UINT64_MAX - range->length) {
|
||||
errno = EOVERFLOW;
|
||||
return -1;
|
||||
}
|
||||
end = range->start + range->length;
|
||||
if (end > feed_length) end = feed_length;
|
||||
if (end > SLEEP_BITFIELD_DATA_BYTES * 8u) end = SLEEP_BITFIELD_DATA_BYTES * 8u;
|
||||
for (i = range->start; i < end; i++) {
|
||||
int have = i <= SIZE_MAX && sleep_feed_has(session->feeds[feed], (size_t)i);
|
||||
if (have && !in_run) { run_start = i; in_run = 1; }
|
||||
if (!have && in_run) {
|
||||
if (send_have_range(session, peer, feed, run_start, i - run_start)) return -1;
|
||||
in_run = 0;
|
||||
}
|
||||
}
|
||||
if (in_run && send_have_range(session, peer, feed, run_start, end - run_start)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int announce_received_block(dat_session_t *session,
|
||||
enum dat_session_feed feed, uint64_t index)
|
||||
{
|
||||
dat_range_t range = {index, 1};
|
||||
uint8_t payload[32];
|
||||
size_t payload_len, i;
|
||||
if (dat_range_encode(&range, payload, sizeof(payload), &payload_len)) return -1;
|
||||
for (i = 0; i < session->peer_count; i++) {
|
||||
dat_session_peer_t *peer = &session->peers[i];
|
||||
if (peer->state == DAT_SESSION_PEER_READY &&
|
||||
(peer->remote_feed_mask & (uint8_t)(1u << feed)) &&
|
||||
send_frame(session, peer, feed_channels[feed], DAT_MSG_HAVE,
|
||||
payload, payload_len)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pump_replication(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed)
|
||||
{
|
||||
sleep_replication_t *replication = &peer->replication[feed];
|
||||
size_t index, limit;
|
||||
if (peer->state != DAT_SESSION_PEER_READY || peer->paused || !replication->uploading) return 0;
|
||||
limit = replication->remote_length > SLEEP_BITFIELD_DATA_BYTES * 8u ?
|
||||
SLEEP_BITFIELD_DATA_BYTES * 8u : (size_t)replication->remote_length;
|
||||
for (index = 0; index < limit; index++) {
|
||||
int requested, available;
|
||||
if (sleep_feed_has(session->feeds[feed], index)) continue;
|
||||
available = sleep_bitfield_get_data(&replication->remote_bitfield, index);
|
||||
if (available <= 0) continue;
|
||||
requested = sleep_replication_is_requested(replication, index);
|
||||
if (requested < 0) return -1;
|
||||
if (requested) continue;
|
||||
if (dat_session_request_block(session, peer->peer_id, feed, index, 0)) {
|
||||
if (errno == ENOSPC) return 0;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_peer_secure(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES], int authenticated)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
dat_handshake_t handshake;
|
||||
dat_feed_open_t feed_open;
|
||||
uint8_t payload[128];
|
||||
size_t payload_len;
|
||||
if (!session || !peer_id || !authenticated) { errno = EACCES; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer || peer->state != DAT_SESSION_PEER_DISCOVERED) { errno = EINVAL; return -1; }
|
||||
peer->transport_authenticated = 1;
|
||||
peer->state = DAT_SESSION_PEER_SECURE;
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
memset(&feed_open, 0, sizeof(feed_open));
|
||||
if (sleep_crypto_init()) goto fail;
|
||||
if (sleep_compute_discovery_key(session->feed_keys[DAT_SESSION_METADATA],
|
||||
feed_open.discovery_key)) goto fail;
|
||||
feed_open.has_nonce = 1;
|
||||
randombytes_buf(feed_open.nonce, sizeof(feed_open.nonce));
|
||||
memcpy(peer->tx_nonce, feed_open.nonce, sizeof(peer->tx_nonce));
|
||||
if (dat_feed_open_encode(&feed_open, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[DAT_SESSION_METADATA], DAT_MSG_FEED,
|
||||
payload, payload_len)) goto fail;
|
||||
peer->local_feed_mask |= (uint8_t)(1u << DAT_SESSION_METADATA);
|
||||
peer->tx_stream_encrypted = 1;
|
||||
if (dat_handshake_init(&handshake, session->peer_id,
|
||||
session->content_key_known ? 0 : DAT_SESSION_CAP_METADATA_ONLY) ||
|
||||
dat_handshake_encode(&handshake, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, 0, DAT_MSG_HANDSHAKE, payload, payload_len)) goto fail;
|
||||
memset(&feed_open, 0, sizeof(feed_open));
|
||||
if (session->content_key_known &&
|
||||
(sleep_compute_discovery_key(session->feed_keys[DAT_SESSION_CONTENT],
|
||||
feed_open.discovery_key) ||
|
||||
dat_feed_open_encode(&feed_open, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[DAT_SESSION_CONTENT], DAT_MSG_FEED,
|
||||
payload, payload_len))) goto fail;
|
||||
if (session->content_key_known) peer->local_feed_mask |= (uint8_t)(1u << DAT_SESSION_CONTENT);
|
||||
return 0;
|
||||
#else
|
||||
memset(&feed_open, 0, sizeof(feed_open));
|
||||
if (sleep_compute_discovery_key(session->feed_keys[DAT_SESSION_METADATA],
|
||||
feed_open.discovery_key) ||
|
||||
dat_feed_open_encode(&feed_open, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[DAT_SESSION_METADATA],
|
||||
DAT_MSG_FEED, payload, payload_len)) goto fail;
|
||||
peer->local_feed_mask |= (uint8_t)(1u << DAT_SESSION_METADATA);
|
||||
if (session->content_key_known &&
|
||||
(sleep_compute_discovery_key(session->feed_keys[DAT_SESSION_CONTENT],
|
||||
feed_open.discovery_key) ||
|
||||
dat_feed_open_encode(&feed_open, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[DAT_SESSION_CONTENT],
|
||||
DAT_MSG_FEED, payload, payload_len))) goto fail;
|
||||
if (session->content_key_known) peer->local_feed_mask |= (uint8_t)(1u << DAT_SESSION_CONTENT);
|
||||
if (dat_handshake_init(&handshake, session->peer_id,
|
||||
session->content_key_known ? 0 : DAT_SESSION_CAP_METADATA_ONLY) ||
|
||||
dat_handshake_encode(&handshake, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, 0, DAT_MSG_HANDSHAKE, payload, payload_len)) goto fail;
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
fail:
|
||||
peer->state = DAT_SESSION_PEER_CLOSED;
|
||||
peer->transport_authenticated = 0;
|
||||
peer->tx_stream_encrypted = peer->rx_stream_encrypted = 0;
|
||||
peer->tx_offset = peer->rx_offset = 0;
|
||||
free(peer->rx_buffer);
|
||||
peer->rx_buffer = NULL;
|
||||
peer->rx_buffer_len = peer->rx_buffer_capacity = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dat_session_expect_content_key(dat_session_t *session)
|
||||
{
|
||||
if (!session || session->state != DAT_SESSION_OPEN) { errno = EINVAL; return -1; }
|
||||
memset(session->feed_keys[DAT_SESSION_CONTENT], 0, DAT_KEY_BYTES);
|
||||
session->content_key_known = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_set_content_key(dat_session_t *session,
|
||||
const uint8_t content_key[DAT_KEY_BYTES])
|
||||
{
|
||||
uint8_t expected[32];
|
||||
size_t i;
|
||||
if (!session || !content_key || session->state == DAT_SESSION_CLOSED ||
|
||||
session->state == DAT_SESSION_CLOSING) { errno = EINVAL; return -1; }
|
||||
if (session->content_key_known) {
|
||||
if (!memcmp(session->feed_keys[DAT_SESSION_CONTENT], content_key, DAT_KEY_BYTES)) return 0;
|
||||
errno = EKEYREJECTED;
|
||||
return -1;
|
||||
}
|
||||
if (sleep_compute_discovery_key(content_key, expected)) return -1;
|
||||
for (i = 0; i < session->peer_count; i++) {
|
||||
dat_session_peer_t *peer = &session->peers[i];
|
||||
if (peer->remote_content_pending &&
|
||||
memcmp(peer->remote_content_discovery, expected, sizeof(expected))) {
|
||||
errno = EKEYREJECTED;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
memcpy(session->feed_keys[DAT_SESSION_CONTENT], content_key, DAT_KEY_BYTES);
|
||||
memcpy(session->feeds[DAT_SESSION_CONTENT]->keypair.pubkey, content_key, DAT_KEY_BYTES);
|
||||
session->content_key_known = 1;
|
||||
for (i = 0; i < session->peer_count; i++) {
|
||||
dat_session_peer_t *peer = &session->peers[i];
|
||||
dat_feed_open_t feed_open;
|
||||
uint8_t payload[128];
|
||||
size_t payload_len;
|
||||
peer->remote_content_pending = 0;
|
||||
if (peer->state != DAT_SESSION_PEER_READY && peer->state != DAT_SESSION_PEER_SECURE) continue;
|
||||
if (!(peer->local_feed_mask & (uint8_t)(1u << DAT_SESSION_CONTENT))) {
|
||||
memset(&feed_open, 0, sizeof(feed_open));
|
||||
if (sleep_compute_discovery_key(content_key, feed_open.discovery_key) ||
|
||||
dat_feed_open_encode(&feed_open, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[DAT_SESSION_CONTENT], DAT_MSG_FEED,
|
||||
payload, payload_len)) return -1;
|
||||
peer->local_feed_mask |= (uint8_t)(1u << DAT_SESSION_CONTENT);
|
||||
}
|
||||
if (peer->state == DAT_SESSION_PEER_READY &&
|
||||
(peer->remote_feed_mask & (uint8_t)(1u << DAT_SESSION_CONTENT)) &&
|
||||
send_have(session, peer, DAT_SESSION_CONTENT)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int receive_data(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed, const uint8_t *payload,
|
||||
size_t payload_len)
|
||||
{
|
||||
dat_data_t data;
|
||||
sleep_proof_t proof;
|
||||
sleep_feed_t *local;
|
||||
uint8_t *old_data = NULL;
|
||||
uint8_t proof_cache[8192];
|
||||
const uint8_t *cached_proof;
|
||||
size_t old_len = 0;
|
||||
size_t cached_proof_len;
|
||||
size_t i;
|
||||
if (!valid_feed(feed) || dat_data_decode(&data, payload, payload_len)) return -1;
|
||||
local = session->feeds[feed];
|
||||
if (data.proof_len) {
|
||||
if (sleep_proof_decode(&proof, data.proof, data.proof_len)) return -1;
|
||||
} else {
|
||||
sleep_proof_node_t nodes[DAT_DATA_MAX_NODES];
|
||||
uint64_t remote_length = peer->replication[feed].remote_length;
|
||||
if (!data.has_signature || !remote_length || data.node_count > DAT_DATA_MAX_NODES) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < data.node_count; i++) {
|
||||
nodes[i].index = data.nodes[i].index;
|
||||
nodes[i].length = data.nodes[i].length;
|
||||
memcpy(nodes[i].hash, data.nodes[i].hash, 32);
|
||||
}
|
||||
if (sleep_proof_from_nodes(data.index, data.value, data.value_len,
|
||||
nodes, data.node_count, data.signature,
|
||||
remote_length, &proof)) return -1;
|
||||
}
|
||||
if (proof.block != data.index || !proof.has_signature ||
|
||||
sleep_proof_verify(&proof, data.value, data.value_len,
|
||||
session->feed_keys[feed])) {
|
||||
errno = EBADMSG;
|
||||
return -1;
|
||||
}
|
||||
cached_proof = data.proof;
|
||||
cached_proof_len = data.proof_len;
|
||||
if (!cached_proof_len) {
|
||||
if (sleep_proof_encode(&proof, proof_cache, sizeof(proof_cache),
|
||||
&cached_proof_len)) return -1;
|
||||
cached_proof = proof_cache;
|
||||
}
|
||||
if (data.index >= SLEEP_BITFIELD_DATA_BYTES * 8u ||
|
||||
(peer->replication[feed].remote_length &&
|
||||
data.index >= peer->replication[feed].remote_length)) {
|
||||
errno = ERANGE;
|
||||
return -1;
|
||||
}
|
||||
if (peer->paused)
|
||||
return sleep_replication_complete(&peer->replication[feed], data.index);
|
||||
if (sleep_feed_has(local, (size_t)data.index)) {
|
||||
if (sleep_feed_read(local, (size_t)data.index, &old_data, &old_len)) return -1;
|
||||
if (old_len != data.value_len || memcmp(old_data, data.value, old_len)) {
|
||||
free(old_data); errno = EBADMSG; return -1;
|
||||
}
|
||||
free(old_data);
|
||||
peer->payload_bytes_received += data.value_len;
|
||||
if (sleep_replication_complete(&peer->replication[feed], data.index)) return -1;
|
||||
return pump_replication(session, peer, feed);
|
||||
}
|
||||
if (sleep_feed_append_remote(local, (size_t)data.index, data.value,
|
||||
data.value_len, cached_proof,
|
||||
cached_proof_len)) return -1;
|
||||
peer->payload_bytes_received += data.value_len;
|
||||
if (sleep_replication_complete(&peer->replication[feed], data.index)) return -1;
|
||||
if (announce_received_block(session, feed, data.index)) return -1;
|
||||
return pump_replication(session, peer, feed);
|
||||
}
|
||||
|
||||
static int dispatch_request(dat_session_t *session, dat_session_peer_t *peer,
|
||||
enum dat_session_feed feed,
|
||||
const dat_request_t *request)
|
||||
{
|
||||
if (session->on_request)
|
||||
return session->on_request(session->context, peer->peer_id, feed, request);
|
||||
return dat_session_send_block(session, peer->peer_id, feed, request->index);
|
||||
}
|
||||
|
||||
static int dat_session_process_frame(dat_session_t *session,
|
||||
dat_session_peer_t *peer,
|
||||
const uint8_t *frame, size_t frame_len)
|
||||
{
|
||||
dat_wire_message_t message;
|
||||
size_t consumed;
|
||||
if (dat_wire_decode(&message, frame, frame_len, &consumed) || consumed != frame_len) return -1;
|
||||
if (message.type == DAT_MSG_FEED) {
|
||||
dat_feed_open_t remote_feed;
|
||||
enum dat_session_feed feed;
|
||||
uint8_t expected[32];
|
||||
if ((peer->state != DAT_SESSION_PEER_SECURE && peer->state != DAT_SESSION_PEER_READY) ||
|
||||
feed_for_channel(message.channel, &feed) ||
|
||||
(peer->remote_feed_mask & (uint8_t)(1u << feed)) ||
|
||||
dat_feed_open_decode(&remote_feed, message.payload, message.payload_len)) {
|
||||
errno = EKEYREJECTED;
|
||||
return -1;
|
||||
}
|
||||
if (feed == DAT_SESSION_CONTENT && !session->content_key_known) {
|
||||
memcpy(peer->remote_content_discovery, remote_feed.discovery_key, 32);
|
||||
peer->remote_content_pending = 1;
|
||||
} else if (sleep_compute_discovery_key(session->feed_keys[feed], expected) ||
|
||||
memcmp(remote_feed.discovery_key, expected, sizeof(expected))) {
|
||||
errno = EKEYREJECTED;
|
||||
return -1;
|
||||
}
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
if (feed == DAT_SESSION_METADATA && message.channel == 0) {
|
||||
if (!remote_feed.has_nonce || peer->rx_stream_encrypted) { errno = EPROTO; return -1; }
|
||||
memcpy(peer->rx_nonce, remote_feed.nonce, sizeof(peer->rx_nonce));
|
||||
peer->rx_offset = 0;
|
||||
peer->rx_stream_encrypted = 1;
|
||||
}
|
||||
#endif
|
||||
peer->remote_feed_mask |= (uint8_t)(1u << feed);
|
||||
if (peer->state == DAT_SESSION_PEER_READY)
|
||||
return send_have(session, peer, feed);
|
||||
return 0;
|
||||
}
|
||||
if (message.type == DAT_MSG_HANDSHAKE) {
|
||||
dat_handshake_t remote;
|
||||
if (message.channel != 0 || peer->state != DAT_SESSION_PEER_SECURE ||
|
||||
!(peer->remote_feed_mask & (uint8_t)(1u << DAT_SESSION_METADATA)) ||
|
||||
dat_handshake_decode(&remote, message.payload, message.payload_len) ||
|
||||
dat_handshake_protocol_matches(&remote) != 1
|
||||
#ifndef DAT_LEGACY_HYPERCORE
|
||||
|| (remote.has_peer_id && memcmp(remote.peer_id, peer->peer_id,
|
||||
DAT_PEER_ID_BYTES))
|
||||
#endif
|
||||
) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
#ifndef DAT_LEGACY_HYPERCORE
|
||||
if (!(remote.capabilities & DAT_SESSION_CAP_METADATA_ONLY) &&
|
||||
peer->remote_feed_mask != (uint8_t)((1u << DAT_SESSION_FEED_COUNT) - 1u)) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
if ((remote.capabilities & DAT_SESSION_CAP_METADATA_ONLY) &&
|
||||
peer->remote_feed_mask != (uint8_t)(1u << DAT_SESSION_METADATA)) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
peer->capabilities = remote.capabilities;
|
||||
peer->state = DAT_SESSION_PEER_READY;
|
||||
session->state = DAT_SESSION_ACTIVE;
|
||||
for (size_t feed = 0; feed < DAT_SESSION_FEED_COUNT; feed++)
|
||||
if ((peer->remote_feed_mask & (uint8_t)(1u << feed)) &&
|
||||
send_have(session, peer, (enum dat_session_feed)feed)) return -1;
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
enum dat_session_feed feed;
|
||||
if (peer->state != DAT_SESSION_PEER_READY || feed_for_channel(message.channel, &feed)) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
if (!(peer->remote_feed_mask & (uint8_t)(1u << feed))) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
if (message.type == DAT_MSG_INFO) {
|
||||
dat_info_t info;
|
||||
if (dat_info_decode(&info, message.payload, message.payload_len)) return -1;
|
||||
peer->replication[feed].uploading = info.uploading;
|
||||
peer->replication[feed].downloading = info.downloading;
|
||||
return pump_replication(session, peer, feed);
|
||||
}
|
||||
if (message.type == DAT_MSG_UNHAVE) {
|
||||
dat_range_t range;
|
||||
if (dat_range_decode(&range, message.payload, message.payload_len)) return -1;
|
||||
if (sleep_replication_unhave(&peer->replication[feed], range.start,
|
||||
range.length)) return -1;
|
||||
return sleep_replication_unwant(&peer->replication[feed], range.start,
|
||||
range.length);
|
||||
}
|
||||
if (message.type == DAT_MSG_HAVE || message.type == DAT_MSG_WANT) {
|
||||
dat_range_t range;
|
||||
if (message.type == DAT_MSG_HAVE) {
|
||||
if (dat_range_decode(&range, message.payload, message.payload_len)) return -1;
|
||||
if (sleep_replication_have(&peer->replication[feed],
|
||||
range.start, range.length)) return -1;
|
||||
return pump_replication(session, peer, feed);
|
||||
}
|
||||
if (dat_want_decode(&range, message.payload, message.payload_len,
|
||||
sleep_feed_length(session->feeds[feed]))) return -1;
|
||||
return answer_want(session, peer, feed, &range);
|
||||
}
|
||||
if (message.type == DAT_MSG_UNWANT) return 0;
|
||||
if (message.type == DAT_MSG_REQUEST) {
|
||||
dat_request_t request;
|
||||
if (dat_request_decode(&request, message.payload, message.payload_len)) return -1;
|
||||
return dispatch_request(session, peer, feed, &request);
|
||||
}
|
||||
if (message.type == DAT_MSG_DATA)
|
||||
return receive_data(session, peer, feed, message.payload, message.payload_len);
|
||||
if (message.type == DAT_MSG_CANCEL) return 0;
|
||||
errno = EPROTONOSUPPORT;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int dat_session_receive_buffered(dat_session_t *session,
|
||||
dat_session_peer_t *peer,
|
||||
const uint8_t *bytes, size_t length)
|
||||
{
|
||||
if (length > DAT_SESSION_FRAME_BYTES * 2u ||
|
||||
peer->rx_buffer_len > DAT_SESSION_FRAME_BYTES * 2u - length) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
if (peer->rx_buffer_capacity < peer->rx_buffer_len + length) {
|
||||
size_t capacity = peer->rx_buffer_len + length;
|
||||
uint8_t *next;
|
||||
if (capacity < 4096) capacity = 4096;
|
||||
next = realloc(peer->rx_buffer, capacity);
|
||||
if (!next) return -1;
|
||||
peer->rx_buffer = next;
|
||||
peer->rx_buffer_capacity = capacity;
|
||||
}
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
if (peer->rx_stream_encrypted) {
|
||||
if (sleep_stream_xsalsa20_xor(peer->rx_buffer + peer->rx_buffer_len,
|
||||
bytes, length, peer->rx_nonce,
|
||||
session->feed_keys[DAT_SESSION_METADATA],
|
||||
peer->rx_offset)) return -1;
|
||||
peer->rx_offset += length;
|
||||
} else if (length) {
|
||||
memcpy(peer->rx_buffer + peer->rx_buffer_len, bytes, length);
|
||||
}
|
||||
#else
|
||||
if (length) memcpy(peer->rx_buffer + peer->rx_buffer_len, bytes, length);
|
||||
#endif
|
||||
peer->rx_buffer_len += length;
|
||||
while (peer->rx_buffer_len) {
|
||||
uint64_t body_len;
|
||||
size_t prefix, total;
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
int was_encrypted = peer->rx_stream_encrypted;
|
||||
#endif
|
||||
if (sleep_varint_decode(peer->rx_buffer, peer->rx_buffer_len,
|
||||
&body_len, &prefix)) {
|
||||
if (errno == EAGAIN) return 0;
|
||||
return -1;
|
||||
}
|
||||
if (!body_len || body_len > DAT_SESSION_FRAME_BYTES ||
|
||||
body_len > SIZE_MAX - prefix) { errno = EMSGSIZE; return -1; }
|
||||
total = prefix + (size_t)body_len;
|
||||
if (peer->rx_buffer_len < total) return 0;
|
||||
if (dat_session_process_frame(session, peer, peer->rx_buffer, total)) return -1;
|
||||
peer->rx_buffer_len -= total;
|
||||
if (peer->rx_buffer_len)
|
||||
memmove(peer->rx_buffer, peer->rx_buffer + total, peer->rx_buffer_len);
|
||||
#ifdef DAT_LEGACY_HYPERCORE
|
||||
/* Feed(0) turns encryption on mid-read; cipher the buffered tail from offset zero. */
|
||||
if (!was_encrypted && peer->rx_stream_encrypted && peer->rx_buffer_len) {
|
||||
if (sleep_stream_xsalsa20_xor(peer->rx_buffer, peer->rx_buffer,
|
||||
peer->rx_buffer_len, peer->rx_nonce,
|
||||
session->feed_keys[DAT_SESSION_METADATA], 0)) return -1;
|
||||
peer->rx_offset = peer->rx_buffer_len;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_receive(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
const uint8_t *bytes, size_t length)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
if (!session || !peer_id || (!bytes && length)) { errno = EINVAL; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer || !peer->transport_authenticated) { errno = EACCES; return -1; }
|
||||
return dat_session_receive_buffered(session, peer, bytes, length);
|
||||
}
|
||||
|
||||
int dat_session_request_block(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
enum dat_session_feed feed, uint64_t index,
|
||||
uint64_t bytes)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
sleep_replication_request_t queued;
|
||||
dat_request_t request;
|
||||
uint8_t payload[64];
|
||||
size_t payload_len;
|
||||
int already_requested;
|
||||
if (!session || !peer_id || !valid_feed(feed)) { errno = EINVAL; return -1; }
|
||||
if (index >= SLEEP_BITFIELD_DATA_BYTES * 8u) { errno = ERANGE; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer || peer->state != DAT_SESSION_PEER_READY ||
|
||||
!(peer->remote_feed_mask & (uint8_t)(1u << feed))) { errno = ENOTCONN; return -1; }
|
||||
if (!peer->replication[feed].uploading) { errno = EACCES; return -1; }
|
||||
already_requested = sleep_replication_is_requested(&peer->replication[feed], index);
|
||||
if (already_requested < 0) return -1;
|
||||
if (already_requested) return 0;
|
||||
queued.index = index; queued.bytes = bytes; queued.hash = 1; queued.nodes = 1;
|
||||
if (sleep_replication_request(&peer->replication[feed], &queued)) return -1;
|
||||
request.index = index; request.bytes = bytes; request.want_hash = 1; request.want_nodes = 1;
|
||||
if (dat_request_encode(&request, payload, sizeof(payload), &payload_len) ||
|
||||
send_frame(session, peer, feed_channels[feed], DAT_MSG_REQUEST, payload, payload_len)) {
|
||||
(void)sleep_replication_complete(&peer->replication[feed], index);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_send_block(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES],
|
||||
enum dat_session_feed feed, uint64_t index)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
sleep_proof_t proof;
|
||||
dat_data_t data;
|
||||
uint8_t *block = NULL, *proof_bytes = NULL, *payload = NULL;
|
||||
size_t block_len = 0, proof_len = 0, payload_len = 0;
|
||||
int rc = -1;
|
||||
if (!session || !peer_id || !valid_feed(feed)) { errno = EINVAL; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer || peer->state != DAT_SESSION_PEER_READY ||
|
||||
!(peer->remote_feed_mask & (uint8_t)(1u << feed))) { errno = ENOTCONN; return -1; }
|
||||
if (!peer->replication[feed].downloading) { errno = EACCES; return -1; }
|
||||
if (sleep_feed_read(session->feeds[feed], (size_t)index, &block, &block_len) ||
|
||||
sleep_proof_create(session->feeds[feed], index, &proof)) goto done;
|
||||
proof_bytes = malloc(51 + (proof.node_count + proof.root_count) * 48 + 64);
|
||||
if (!proof_bytes) goto done;
|
||||
if (sleep_proof_encode(&proof, proof_bytes,
|
||||
51 + (proof.node_count + proof.root_count) * 48 + 64,
|
||||
&proof_len)) goto done;
|
||||
data.index = index;
|
||||
data.value = block;
|
||||
data.value_len = block_len;
|
||||
data.proof = proof_bytes;
|
||||
data.proof_len = proof_len;
|
||||
data.node_count = 0;
|
||||
data.has_signature = proof.has_signature;
|
||||
memcpy(data.signature, proof.signature, sizeof(data.signature));
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < proof.node_count + proof.root_count; i++) {
|
||||
const sleep_proof_node_t *node = i < proof.node_count ?
|
||||
&proof.nodes[i] : &proof.roots[i - proof.node_count];
|
||||
data.nodes[i].index = node->index;
|
||||
data.nodes[i].length = node->length;
|
||||
memcpy(data.nodes[i].hash, node->hash, 32);
|
||||
}
|
||||
data.node_count = proof.node_count + proof.root_count;
|
||||
}
|
||||
if (block_len > SIZE_MAX - proof_len - data.node_count * 64 - 256) {
|
||||
errno = EOVERFLOW;
|
||||
goto done;
|
||||
}
|
||||
payload = malloc(block_len + proof_len + data.node_count * 64 + 256);
|
||||
if (!payload) goto done;
|
||||
if (dat_data_encode(&data, payload,
|
||||
block_len + proof_len + data.node_count * 64 + 256,
|
||||
&payload_len)) goto done;
|
||||
rc = send_frame(session, peer, feed_channels[feed], DAT_MSG_DATA, payload, payload_len);
|
||||
if (!rc) peer->payload_bytes_sent += block_len;
|
||||
done:
|
||||
free(block); free(proof_bytes); free(payload);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int dat_session_peer_down(dat_session_t *session, const uint8_t peer_id[DAT_PEER_ID_BYTES])
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
size_t i;
|
||||
int active = 0;
|
||||
if (!session || !peer_id) { errno = EINVAL; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer) { errno = ENOENT; return -1; }
|
||||
peer->state = DAT_SESSION_PEER_CLOSED;
|
||||
peer->transport_authenticated = 0;
|
||||
free(peer->rx_buffer);
|
||||
peer->rx_buffer = NULL;
|
||||
peer->rx_buffer_len = peer->rx_buffer_capacity = 0;
|
||||
for (i = 0; i < DAT_SESSION_FEED_COUNT; i++)
|
||||
sleep_replication_clear_requests(&peer->replication[i]);
|
||||
for (i = 0; i < session->peer_count; i++)
|
||||
if (session->peers[i].state == DAT_SESSION_PEER_READY) active++;
|
||||
if (!active) session->state = DAT_SESSION_DISCOVERING;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_peer_pause(dat_session_t *session,
|
||||
const uint8_t peer_id[DAT_PEER_ID_BYTES], int paused)
|
||||
{
|
||||
dat_session_peer_t *peer;
|
||||
size_t feed;
|
||||
if (!session || !peer_id) { errno = EINVAL; return -1; }
|
||||
peer = dat_session_find_peer(session, peer_id);
|
||||
if (!peer || peer->state != DAT_SESSION_PEER_READY) { errno = ENOTCONN; return -1; }
|
||||
peer->paused = !!paused;
|
||||
if (peer->paused) {
|
||||
for (feed = 0; feed < DAT_SESSION_FEED_COUNT; feed++)
|
||||
sleep_replication_clear_requests(&peer->replication[feed]);
|
||||
return 0;
|
||||
}
|
||||
for (feed = 0; feed < DAT_SESSION_FEED_COUNT; feed++)
|
||||
if (pump_replication(session, peer, (enum dat_session_feed)feed)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_tick(dat_session_t *session, uint64_t now_ms)
|
||||
{
|
||||
return dat_session_poll(session, 0, now_ms);
|
||||
}
|
||||
|
||||
int dat_session_poll(dat_session_t *session, int timeout_ms, uint64_t now_ms)
|
||||
{
|
||||
int received, wait_ms;
|
||||
if (!session || (session->state != DAT_SESSION_DISCOVERING &&
|
||||
session->state != DAT_SESSION_ACTIVE) || timeout_ms < 0) { errno = EINVAL; return -1; }
|
||||
if (session->poll_transport &&
|
||||
session->poll_transport(session->poll_context, timeout_ms) < 0) return -1;
|
||||
wait_ms = session->poll_transport ? 0 : timeout_ms;
|
||||
received = cesspool_poll(session->cesspool, wait_ms);
|
||||
if (received < 0) return -1;
|
||||
while (received > 0) {
|
||||
received = cesspool_poll(session->cesspool, 0);
|
||||
if (received < 0) return -1;
|
||||
}
|
||||
if (cesspool_tick(session->cesspool, now_ms)) return -1;
|
||||
if (discover_known_peers(session)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_session_close(dat_session_t *session)
|
||||
{
|
||||
size_t i;
|
||||
int rc = 0;
|
||||
if (!session || session->state == DAT_SESSION_CLOSED) { errno = EINVAL; return -1; }
|
||||
session->state = DAT_SESSION_CLOSING;
|
||||
for (i = 0; i < session->peer_count; i++) {
|
||||
session->peers[i].state = DAT_SESSION_PEER_CLOSED;
|
||||
session->peers[i].transport_authenticated = 0;
|
||||
free(session->peers[i].rx_buffer);
|
||||
session->peers[i].rx_buffer = NULL;
|
||||
session->peers[i].rx_buffer_len = session->peers[i].rx_buffer_capacity = 0;
|
||||
}
|
||||
if (session->announcing || session->looking_up) {
|
||||
if (cesspool_leave(session->cesspool, session->discovery_topic)) rc = -1;
|
||||
}
|
||||
session->state = DAT_SESSION_CLOSED;
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "dat.h"
|
||||
#include "win_dirent.h"
|
||||
#include <errno.h>
|
||||
#include "win_fnmatch.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int join_path(char*out,size_t cap,const char*a,const char*b){int n=snprintf(out,cap,"%s%s%s",a,*a&&a[strlen(a)-1]!='/'?"/":"",b);if(n<0||(size_t)n>=cap){errno=ENAMETOOLONG;return-1;}return 0;}
|
||||
static int matches_archive_path(const char *path, const char *root);
|
||||
typedef struct sync_context { char patterns[128][256]; size_t pattern_count; } sync_context_t;
|
||||
static uint64_t stat_mtime_ms(const struct stat *st){
|
||||
#if defined(_WIN32)
|
||||
if(st->st_mtime<0)return 0;
|
||||
return (uint64_t)st->st_mtime*1000u;
|
||||
#elif defined(__APPLE__)
|
||||
if(st->st_mtimespec.tv_sec<0)return 0;
|
||||
return (uint64_t)st->st_mtimespec.tv_sec*1000u+(uint64_t)st->st_mtimespec.tv_nsec/1000000u;
|
||||
#else
|
||||
if(st->st_mtim.tv_sec<0)return 0;
|
||||
return (uint64_t)st->st_mtim.tv_sec*1000u+(uint64_t)st->st_mtim.tv_nsec/1000000u;
|
||||
#endif
|
||||
}
|
||||
static uint64_t now_ms(void){struct timespec ts;if(clock_gettime(CLOCK_REALTIME,&ts))return (uint64_t)time(NULL)*1000u;return (uint64_t)ts.tv_sec*1000u+(uint64_t)ts.tv_nsec/1000000u;}
|
||||
static int import_file(dat_archive_t*a,const char*disk,const char*name,const struct stat*st){FILE*f;uint8_t*data;size_t len=(size_t)st->st_size;if(st->st_size<0||(uint64_t)st->st_size>SIZE_MAX){errno=EOVERFLOW;return-1;}data=malloc(len?len:1);if(!data)return-1;f=fopen(disk,"rb");if(!f){free(data);return-1;}if(len&&fread(data,1,len,f)!=len){fclose(f);free(data);errno=EIO;return-1;}fclose(f);if(dat_archive_append_file(a,name,data,len,(uint32_t)st->st_mode,stat_mtime_ms(st))){free(data);return-1;}free(data);return 0;}
|
||||
static int import_one(dat_archive_t*a,const char*disk,const char*name){struct stat st;if(lstat(disk,&st))return-1;if(S_ISREG(st.st_mode))return import_file(a,disk,name,&st);if(S_ISLNK(st.st_mode)){char target[1024];ssize_t n=readlink(disk,target,sizeof(target)-1);if(n<0)return-1;target[n]=0;return dat_archive_append_symlink(a,name,target,stat_mtime_ms(&st));}if(S_ISDIR(st.st_mode)){DIR*dir;struct dirent*item;int rc=0;if(*name&&dat_archive_append_directory(a,name,(uint32_t)st.st_mode,stat_mtime_ms(&st)))return-1;dir=opendir(disk);if(!dir)return-1;while((item=readdir(dir))){char child_disk[2048],child_name[2048];if(!strcmp(item->d_name,".")||!strcmp(item->d_name,"..")||!strcmp(item->d_name,".dat"))continue;if(join_path(child_disk,sizeof(child_disk),disk,item->d_name)||join_path(child_name,sizeof(child_name),name,item->d_name)||import_one(a,child_disk,child_name)){rc=-1;break;}}closedir(dir);return rc;}return 0;}
|
||||
int dat_archive_import_path(dat_archive_t*a,const char*filesystem_path,const char*archive_path){if(!a||!filesystem_path||!archive_path){errno=EINVAL;return-1;}while(*archive_path=='/')archive_path++;return import_one(a,filesystem_path,archive_path);}
|
||||
|
||||
static int same_file(const dat_archive_t *a, const char *disk,
|
||||
const struct stat *st, const sleep_metadata_entry_t *entry)
|
||||
{
|
||||
FILE *f;
|
||||
uint8_t *old, buffer[8192];
|
||||
size_t old_len, n, off = 0;
|
||||
if (entry->type != SLEEP_ENTRY_FILE || entry->size != (uint64_t)st->st_size ||
|
||||
entry->mode != (uint32_t)st->st_mode || entry->mtime != stat_mtime_ms(st))
|
||||
return 0;
|
||||
if (dat_archive_read_file(a, entry, &old, &old_len)) return -1;
|
||||
f = fopen(disk, "rb");
|
||||
if (!f) { free(old); return -1; }
|
||||
while ((n = fread(buffer, 1, sizeof(buffer), f)) != 0) {
|
||||
if (n > old_len - off || memcmp(buffer, old + off, n)) {
|
||||
fclose(f); free(old); return 0;
|
||||
}
|
||||
off += n;
|
||||
}
|
||||
if (ferror(f)) { fclose(f); free(old); errno = EIO; return -1; }
|
||||
fclose(f);
|
||||
free(old);
|
||||
return off == old_len;
|
||||
}
|
||||
|
||||
static int ignored_path(const sync_context_t *context, const char *name, const char *basename)
|
||||
{
|
||||
size_t i;
|
||||
if (*name && basename[0] == '.') return 1;
|
||||
for (i = 0; i < context->pattern_count; i++) {
|
||||
const char *pattern = context->patterns[i];
|
||||
size_t n = strlen(pattern);
|
||||
if (pattern[0] == '/') pattern++;
|
||||
if (n && context->patterns[i][n - 1] == '/' && !strncmp(name, pattern, strlen(pattern))) return 1;
|
||||
if (!fnmatch(pattern, name, 0)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sync_one(dat_archive_t *a, const char *disk, const char *name, const sync_context_t *context)
|
||||
{
|
||||
struct stat st;
|
||||
sleep_metadata_entry_t previous;
|
||||
int found;
|
||||
if (lstat(disk, &st)) return -1;
|
||||
found = dat_archive_find(a, name, &previous) == 0;
|
||||
if (!found && errno != ENOENT) return -1;
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
int equal = found ? same_file(a, disk, &st, &previous) : 0;
|
||||
if (equal < 0) return -1;
|
||||
return equal ? 0 : import_file(a, disk, name, &st);
|
||||
}
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
char target[1024]; ssize_t n = readlink(disk, target, sizeof(target) - 1);
|
||||
if (n < 0) return -1;
|
||||
target[n] = 0;
|
||||
if (found && previous.type == SLEEP_ENTRY_SYMLINK && previous.mtime == stat_mtime_ms(&st) && !strcmp(previous.linkname, target)) return 0;
|
||||
return dat_archive_append_symlink(a, name, target, stat_mtime_ms(&st));
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
DIR *dir; struct dirent *item;
|
||||
if (*name && (!found || previous.type != SLEEP_ENTRY_DIRECTORY || previous.mode != (uint32_t)st.st_mode || previous.mtime != stat_mtime_ms(&st)) && dat_archive_append_directory(a, name, (uint32_t)st.st_mode, stat_mtime_ms(&st))) return -1;
|
||||
dir = opendir(disk);
|
||||
if (!dir) return -1;
|
||||
while ((item = readdir(dir))) {
|
||||
char child_disk[2048], child_name[2048];
|
||||
if (!strcmp(item->d_name, ".") || !strcmp(item->d_name, "..")) continue;
|
||||
if (join_path(child_disk, sizeof(child_disk), disk, item->d_name) || join_path(child_name, sizeof(child_name), name, item->d_name)) { closedir(dir); return -1; }
|
||||
if (ignored_path(context, child_name, item->d_name)) continue;
|
||||
if (sync_one(a, child_disk, child_name, context)) { closedir(dir); return -1; }
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_ignore_file(const char *root, sync_context_t *context)
|
||||
{
|
||||
char path[1200], line[256];
|
||||
FILE *f;
|
||||
if (join_path(path, sizeof(path), root, ".datignore")) return -1;
|
||||
f = fopen(path, "r");
|
||||
if (!f) return errno == ENOENT ? 0 : -1;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
size_t n = strcspn(line, "\r\n");
|
||||
line[n] = 0;
|
||||
if (!line[0] || line[0] == '#') continue;
|
||||
if (context->pattern_count >= sizeof(context->patterns) / sizeof(context->patterns[0])) { fclose(f); errno = EOVERFLOW; return -1; }
|
||||
strcpy(context->patterns[context->pattern_count++], line);
|
||||
}
|
||||
if (ferror(f)) { fclose(f); errno = EIO; return -1; }
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int path_missing(const char *filesystem_path, const char *archive_path, const char *entry_path)
|
||||
{
|
||||
char disk[4096]; const char *relative = entry_path; size_t prefix; struct stat st;
|
||||
while (*relative == '/') relative++;
|
||||
prefix = strlen(archive_path);
|
||||
if (prefix) { if (!strcmp(relative, archive_path)) relative = ""; else relative += prefix + 1; }
|
||||
if (join_path(disk, sizeof(disk), filesystem_path, relative)) return -1;
|
||||
if (!lstat(disk, &st)) return 0;
|
||||
return errno == ENOENT ? 1 : -1;
|
||||
}
|
||||
|
||||
int dat_archive_sync_path(dat_archive_t *a, const char *filesystem_path, const char *archive_path)
|
||||
{
|
||||
size_t count, i, j;
|
||||
sync_context_t context;
|
||||
if (!a || !filesystem_path || !archive_path) { errno = EINVAL; return -1; }
|
||||
while (*archive_path == '/') archive_path++;
|
||||
memset(&context, 0, sizeof(context));
|
||||
if (load_ignore_file(filesystem_path, &context) || sync_one(a, filesystem_path, archive_path, &context)) return -1;
|
||||
count = dat_archive_entry_count(a);
|
||||
for (i = 0; i < count; i++) {
|
||||
sleep_metadata_entry_t entry, later; int newest = 1, missing;
|
||||
if (dat_archive_read_entry(a, i, &entry)) return -1;
|
||||
if (!matches_archive_path(entry.path, archive_path)) continue;
|
||||
for (j = i + 1; j < count; j++) { if (dat_archive_read_entry(a, j, &later)) return -1; if (!strcmp(entry.path, later.path)) { newest = 0; break; } }
|
||||
if (!newest || entry.type == SLEEP_ENTRY_DELETE) continue;
|
||||
missing = path_missing(filesystem_path, archive_path, entry.path);
|
||||
if (missing < 0) return -1;
|
||||
if (missing && dat_archive_delete(a, entry.path, now_ms())) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int export_entry(const dat_archive_t*a,const sleep_metadata_entry_t*e,const char*path){if(e->type==SLEEP_ENTRY_DIRECTORY){if(mkdir(path,(mode_t)e->mode)&&errno!=EEXIST)return-1;return 0;}if(e->type==SLEEP_ENTRY_SYMLINK)return symlink(e->linkname,path);if(e->type==SLEEP_ENTRY_FILE){uint8_t*data;size_t len;FILE*f;if(dat_archive_read_file(a,e,&data,&len))return-1;f=fopen(path,"wb");if(!f){free(data);return-1;}if(len&&fwrite(data,1,len,f)!=len){fclose(f);free(data);errno=EIO;return-1;}free(data);return fclose(f);}return 0;}
|
||||
static int matches_archive_path(const char *path, const char *root)
|
||||
{
|
||||
size_t n;
|
||||
while (*path == '/') path++;
|
||||
while (*root == '/') root++;
|
||||
n = strlen(root);
|
||||
return !n || !strcmp(path, root) || (!strncmp(path, root, n) && path[n] == '/');
|
||||
}
|
||||
|
||||
int dat_archive_export_at(const dat_archive_t*a,uint64_t version,const char*archive_path,const char*filesystem_path)
|
||||
{
|
||||
sleep_metadata_entry_t e, later;
|
||||
size_t count,i,j,prefix;
|
||||
if(!a||!archive_path||!filesystem_path){errno=EINVAL;return-1;}
|
||||
while(*archive_path=='/')archive_path++;
|
||||
count=dat_archive_entry_count(a);
|
||||
if(version>count){errno=ERANGE;return-1;}
|
||||
prefix=strlen(archive_path);
|
||||
for(i=0;i<(size_t)version;i++){
|
||||
char suffix[2048],disk[4096];
|
||||
int current=1;
|
||||
if(dat_archive_read_entry(a,i,&e))return-1;
|
||||
if(!matches_archive_path(e.path,archive_path))continue;
|
||||
for(j=i+1;j<(size_t)version;j++){
|
||||
if(dat_archive_read_entry(a,j,&later))return-1;
|
||||
if(!strcmp(later.path,e.path)){current=0;break;}
|
||||
}
|
||||
if(!current||e.type==SLEEP_ENTRY_DELETE)continue;
|
||||
{
|
||||
const char *entry_path=e.path;
|
||||
while(*entry_path=='/')entry_path++;
|
||||
if(!prefix)sprintf(suffix,"%s",entry_path);
|
||||
else if(!strcmp(entry_path,archive_path))suffix[0]=0;
|
||||
else strcpy(suffix,entry_path+prefix+1);
|
||||
}
|
||||
if(*suffix){if(join_path(disk,sizeof(disk),filesystem_path,suffix))return-1;}
|
||||
else{if(strlen(filesystem_path)>=sizeof(disk)){errno=ENAMETOOLONG;return-1;}strcpy(disk,filesystem_path);}
|
||||
if(export_entry(a,&e,disk))return-1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_archive_export_path(const dat_archive_t*a,const char*archive_path,const char*filesystem_path)
|
||||
{
|
||||
if(!a){errno=EINVAL;return-1;}
|
||||
return dat_archive_export_at(a,dat_archive_version(a),archive_path,filesystem_path);
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
#include "dat_wire.h"
|
||||
#include "sleep_proto.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
static int put_varint(uint64_t value, uint8_t *out, size_t cap, size_t *off)
|
||||
{
|
||||
size_t n;
|
||||
if (*off > cap || sleep_varint_encode(value, out + *off, cap - *off, &n)) return -1;
|
||||
*off += n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int put_bytes(uint32_t field, const uint8_t *data, size_t len,
|
||||
uint8_t *out, size_t cap, size_t *off)
|
||||
{
|
||||
if ((!data && len) || put_varint(((uint64_t)field << 3) | 2, out, cap, off) ||
|
||||
put_varint(len, out, cap, off)) return -1;
|
||||
if (len > cap - *off) { errno = ENOSPC; return -1; }
|
||||
if (len) memcpy(out + *off, data, len);
|
||||
*off += len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int put_uint(uint32_t field, uint64_t value,
|
||||
uint8_t *out, size_t cap, size_t *off)
|
||||
{
|
||||
return put_varint((uint64_t)field << 3, out, cap, off) ||
|
||||
put_varint(value, out, cap, off) ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Returns one field. Unknown protobuf wire types are skipped by read_field. */
|
||||
typedef struct proto_field {
|
||||
uint32_t number;
|
||||
uint8_t wire;
|
||||
uint64_t value;
|
||||
const uint8_t *bytes;
|
||||
size_t length;
|
||||
} proto_field_t;
|
||||
|
||||
static int read_field(const uint8_t *in, size_t len, size_t *off,
|
||||
proto_field_t *field)
|
||||
{
|
||||
uint64_t tag, value;
|
||||
size_t n;
|
||||
if (*off >= len) return 0;
|
||||
if (sleep_varint_decode(in + *off, len - *off, &tag, &n)) return -1;
|
||||
*off += n;
|
||||
field->number = (uint32_t)(tag >> 3);
|
||||
field->wire = (uint8_t)(tag & 7);
|
||||
field->bytes = NULL;
|
||||
field->length = 0;
|
||||
field->value = 0;
|
||||
if (!field->number) { errno = EPROTO; return -1; }
|
||||
if (field->wire == 0) {
|
||||
if (sleep_varint_decode(in + *off, len - *off, &value, &n)) return -1;
|
||||
*off += n;
|
||||
field->value = value;
|
||||
} else if (field->wire == 2) {
|
||||
if (sleep_varint_decode(in + *off, len - *off, &value, &n)) return -1;
|
||||
*off += n;
|
||||
if (value > len - *off) { errno = EPROTO; return -1; }
|
||||
field->bytes = in + *off;
|
||||
field->length = (size_t)value;
|
||||
*off += (size_t)value;
|
||||
} else if (field->wire == 1 || field->wire == 5) {
|
||||
size_t skip = field->wire == 1 ? 8 : 4;
|
||||
if (skip > len - *off) { errno = EPROTO; return -1; }
|
||||
*off += skip;
|
||||
} else { errno = EPROTO; return -1; }
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dat_wire_encode(const dat_wire_message_t *m, uint8_t *out, size_t cap,
|
||||
size_t *written)
|
||||
{
|
||||
uint8_t header[10];
|
||||
size_t hlen, prefix, total;
|
||||
uint64_t packed;
|
||||
if (!m || !out || (!m->payload && m->payload_len) || m->type > 15 ||
|
||||
m->channel > (UINT64_MAX >> 4)) { errno = EINVAL; return -1; }
|
||||
packed = (m->channel << 4) | m->type;
|
||||
if (sleep_varint_encode(packed, header, sizeof(header), &hlen)) return -1;
|
||||
if (m->payload_len > SIZE_MAX - hlen) { errno = EOVERFLOW; return -1; }
|
||||
total = hlen + m->payload_len;
|
||||
if (sleep_varint_encode(total, out, cap, &prefix)) return -1;
|
||||
if (total > cap - prefix) { errno = ENOSPC; return -1; }
|
||||
memcpy(out + prefix, header, hlen);
|
||||
if (m->payload_len) memcpy(out + prefix + hlen, m->payload, m->payload_len);
|
||||
if (written) *written = prefix + total;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_wire_decode(dat_wire_message_t *m, const uint8_t *in, size_t len,
|
||||
size_t *consumed)
|
||||
{
|
||||
uint64_t frame, packed;
|
||||
size_t prefix, hlen;
|
||||
if (!m || !in) { errno = EINVAL; return -1; }
|
||||
if (sleep_varint_decode(in, len, &frame, &prefix)) return -1;
|
||||
if (frame > len - prefix || frame > SIZE_MAX) { errno = EAGAIN; return -1; }
|
||||
if (!frame || sleep_varint_decode(in + prefix, (size_t)frame, &packed, &hlen)) return -1;
|
||||
m->channel = packed >> 4;
|
||||
m->type = packed & 15;
|
||||
m->payload = in + prefix + hlen;
|
||||
m->payload_len = (size_t)frame - hlen;
|
||||
if (consumed) *consumed = prefix + (size_t)frame;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_handshake_init(dat_handshake_t *h, const uint8_t peer_id[32],
|
||||
uint64_t capabilities)
|
||||
{
|
||||
size_t n;
|
||||
if (!h || !peer_id) { errno = EINVAL; return -1; }
|
||||
n = strlen(DAT_NETWORK_PROTOCOL);
|
||||
if (!n || n > DAT_PROTOCOL_BYTES) { errno = EINVAL; return -1; }
|
||||
memset(h, 0, sizeof(*h));
|
||||
memcpy(h->protocol, DAT_NETWORK_PROTOCOL, n);
|
||||
memcpy(h->peer_id, peer_id, 32);
|
||||
h->capabilities = capabilities;
|
||||
h->has_peer_id = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_handshake_protocol_matches(const dat_handshake_t *h)
|
||||
{
|
||||
size_t n, i;
|
||||
if (!h) { errno = EINVAL; return -1; }
|
||||
n = strlen(DAT_NETWORK_PROTOCOL);
|
||||
if (!n || n > DAT_PROTOCOL_BYTES) { errno = EINVAL; return -1; }
|
||||
if (memcmp(h->protocol, DAT_NETWORK_PROTOCOL, n)) { errno = EPROTONOSUPPORT; return 0; }
|
||||
for (i = n; i < DAT_PROTOCOL_BYTES; i++) if (h->protocol[i]) {
|
||||
errno = EPROTONOSUPPORT;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dat_handshake_encode(const dat_handshake_t *h, uint8_t *out, size_t cap,
|
||||
size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
#ifndef DAT_LEGACY_HYPERCORE
|
||||
size_t protocol_len = strlen(DAT_NETWORK_PROTOCOL);
|
||||
#endif
|
||||
if (!h || !out || dat_handshake_protocol_matches(h) != 1) { errno = EINVAL; return -1; }
|
||||
if ((h->has_peer_id && put_bytes(1, h->peer_id, 32, out, cap, &off)) ||
|
||||
(h->capabilities && put_uint(2, h->capabilities, out, cap, &off))) return -1;
|
||||
#ifndef DAT_LEGACY_HYPERCORE
|
||||
if (put_bytes(3, (const uint8_t *)DAT_NETWORK_PROTOCOL, protocol_len,
|
||||
out, cap, &off)) return -1;
|
||||
#endif
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_handshake_decode(dat_handshake_t *h, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc;
|
||||
if (!h || !in) { errno = EINVAL; return -1; }
|
||||
memset(h, 0, sizeof(*h));
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) {
|
||||
if (f.number == 1 && f.wire == 2) {
|
||||
if (f.length == sizeof(h->peer_id)) {
|
||||
memcpy(h->peer_id, f.bytes, f.length);
|
||||
h->has_peer_id = 1;
|
||||
}
|
||||
} else if (f.number == 2 && f.wire == 0) h->capabilities = f.value;
|
||||
#ifndef DAT_LEGACY_HYPERCORE
|
||||
else if (f.number == 3 && f.wire == 2) {
|
||||
if (f.length > sizeof(h->protocol)) { errno = EPROTO; return -1; }
|
||||
memcpy(h->protocol, f.bytes, f.length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (rc < 0) { errno = EPROTO; return -1; }
|
||||
/* Hypercore v6 userData is application data, not a protocol identity. */
|
||||
memcpy(h->protocol, DAT_NETWORK_PROTOCOL, strlen(DAT_NETWORK_PROTOCOL));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_feed_open_encode(const dat_feed_open_t *feed, uint8_t *out,
|
||||
size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!feed || !out) { errno = EINVAL; return -1; }
|
||||
if (put_bytes(1, feed->discovery_key, 32, out, cap, &off) ||
|
||||
(feed->has_nonce && put_bytes(2, feed->nonce, sizeof(feed->nonce), out, cap, &off))) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_feed_open_decode(dat_feed_open_t *feed, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, found = 0;
|
||||
if (!feed || !in) { errno = EINVAL; return -1; }
|
||||
memset(feed, 0, sizeof(*feed));
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) {
|
||||
if ((f.number == 1 || f.number == 2) && f.wire == 2) {
|
||||
if ((f.number == 1 && f.length != 32) ||
|
||||
(f.number == 2 && f.length != sizeof(feed->nonce))) { errno = EPROTO; return -1; }
|
||||
if (f.number == 1) { memcpy(feed->discovery_key, f.bytes, 32); found = 1; }
|
||||
else { memcpy(feed->nonce, f.bytes, sizeof(feed->nonce)); feed->has_nonce = 1; }
|
||||
}
|
||||
}
|
||||
if (rc < 0 || !found) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_range_encode(const dat_range_t *r, uint8_t *out, size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!r || !out) { errno = EINVAL; return -1; }
|
||||
if (put_uint(1, r->start, out, cap, &off) || put_uint(2, r->length, out, cap, &off)) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_range_decode(dat_range_t *r, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, start = 0;
|
||||
if (!r || !in) { errno = EINVAL; return -1; }
|
||||
r->start = 0; r->length = 1;
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) if (f.wire == 0) {
|
||||
if (f.number == 1) { r->start = f.value; start = 1; }
|
||||
if (f.number == 2) r->length = f.value;
|
||||
}
|
||||
if (rc < 0 || !start) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_want_decode(dat_range_t *r, const uint8_t *in, size_t len,
|
||||
uint64_t feed_length)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, have_start = 0, have_length = 0;
|
||||
if (!r || !in) { errno = EINVAL; return -1; }
|
||||
r->start = 0;
|
||||
r->length = 0;
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) if (f.wire == 0) {
|
||||
if (f.number == 1) { r->start = f.value; have_start = 1; }
|
||||
else if (f.number == 2) { r->length = f.value; have_length = 1; }
|
||||
}
|
||||
if (rc < 0 || !have_start) { errno = EPROTO; return -1; }
|
||||
if (!have_length && r->start < feed_length) r->length = feed_length - r->start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_info_encode(const dat_info_t *info, uint8_t *out, size_t cap,
|
||||
size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!info || !out) { errno = EINVAL; return -1; }
|
||||
if ((!info->uploading && put_uint(1, 0, out, cap, &off)) ||
|
||||
(!info->downloading && put_uint(2, 0, out, cap, &off))) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_info_decode(dat_info_t *info, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc;
|
||||
if (!info || (!in && len)) { errno = EINVAL; return -1; }
|
||||
info->uploading = 1;
|
||||
info->downloading = 1;
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) if (f.wire == 0) {
|
||||
if (f.number == 1) info->uploading = !!f.value;
|
||||
else if (f.number == 2) info->downloading = !!f.value;
|
||||
}
|
||||
if (rc < 0) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_request_encode(const dat_request_t *r, uint8_t *out, size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!r || !out) { errno = EINVAL; return -1; }
|
||||
if (put_uint(1, r->index, out, cap, &off) ||
|
||||
(r->bytes && put_uint(2, r->bytes, out, cap, &off)) ||
|
||||
(r->want_hash && put_uint(3, 1, out, cap, &off)) ||
|
||||
(r->want_nodes && put_uint(4, 64, out, cap, &off))) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_request_decode(dat_request_t *r, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, found = 0;
|
||||
if (!r || !in) { errno = EINVAL; return -1; }
|
||||
memset(r, 0, sizeof(*r));
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) if (f.wire == 0) {
|
||||
if (f.number == 1) { r->index = f.value; found = 1; }
|
||||
else if (f.number == 2) r->bytes = f.value;
|
||||
else if (f.number == 3) r->want_hash = !!f.value;
|
||||
else if (f.number == 4) r->want_nodes = !!f.value;
|
||||
}
|
||||
if (rc < 0 || !found) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int encode_node(const dat_data_node_t *node, uint8_t *out,
|
||||
size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (put_uint(1, node->index, out, cap, &off) ||
|
||||
put_bytes(2, node->hash, 32, out, cap, &off) ||
|
||||
put_uint(3, node->length, out, cap, &off)) return -1;
|
||||
*written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_node(dat_data_node_t *node, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, have_index = 0, have_hash = 0, have_length = 0;
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) {
|
||||
if (f.number == 1 && f.wire == 0) { node->index = f.value; have_index = 1; }
|
||||
else if (f.number == 2 && f.wire == 2 && f.length == 32) { memcpy(node->hash, f.bytes, 32); have_hash = 1; }
|
||||
else if (f.number == 3 && f.wire == 0) { node->length = f.value; have_length = 1; }
|
||||
}
|
||||
if (rc < 0 || !have_index || !have_hash || !have_length) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_data_encode(const dat_data_t *d, uint8_t *out, size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0, i, node_len;
|
||||
uint8_t node[64];
|
||||
if (!d || !out || (!d->value && d->value_len) || (!d->proof && d->proof_len) ||
|
||||
d->node_count > DAT_DATA_MAX_NODES) { errno = EINVAL; return -1; }
|
||||
if (put_uint(1, d->index, out, cap, &off) ||
|
||||
put_bytes(2, d->value, d->value_len, out, cap, &off)) return -1;
|
||||
for (i = 0; i < d->node_count; i++) {
|
||||
if (encode_node(&d->nodes[i], node, sizeof(node), &node_len) ||
|
||||
put_bytes(3, node, node_len, out, cap, &off)) return -1;
|
||||
}
|
||||
if (d->has_signature && put_bytes(4, d->signature, sizeof(d->signature), out, cap, &off)) return -1;
|
||||
/* Field 5 carries C's complete multi-root proof; older protobuf readers ignore it. */
|
||||
if (d->proof_len && put_bytes(5, d->proof, d->proof_len, out, cap, &off)) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_data_decode(dat_data_t *d, const uint8_t *in, size_t len)
|
||||
{
|
||||
size_t off = 0;
|
||||
proto_field_t f;
|
||||
int rc, have_index = 0;
|
||||
if (!d || !in) { errno = EINVAL; return -1; }
|
||||
memset(d, 0, sizeof(*d));
|
||||
while ((rc = read_field(in, len, &off, &f)) > 0) {
|
||||
if (f.number == 1 && f.wire == 0) { d->index = f.value; have_index = 1; }
|
||||
else if (f.number == 2 && f.wire == 2) { d->value = f.bytes; d->value_len = f.length; }
|
||||
else if (f.number == 3 && f.wire == 2) {
|
||||
if (d->node_count >= DAT_DATA_MAX_NODES ||
|
||||
decode_node(&d->nodes[d->node_count], f.bytes, f.length)) return -1;
|
||||
d->node_count++;
|
||||
} else if (f.number == 4 && f.wire == 2) {
|
||||
if (f.length != 64) { errno = EPROTO; return -1; }
|
||||
memcpy(d->signature, f.bytes, 64); d->has_signature = 1;
|
||||
} else if (f.number == 5 && f.wire == 2) { d->proof = f.bytes; d->proof_len = f.length; }
|
||||
}
|
||||
if (rc < 0 || !have_index) { errno = EPROTO; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_cancel_encode(const dat_cancel_t *c, uint8_t *out, size_t cap, size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!c || !out) { errno = EINVAL; return -1; }
|
||||
if (put_uint(1, c->index, out, cap, &off)) return -1;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_cancel_decode(dat_cancel_t *c, const uint8_t *in, size_t len)
|
||||
{
|
||||
dat_request_t r;
|
||||
if (!c || dat_request_decode(&r, in, len)) return -1;
|
||||
c->index = r.index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_extension_encode(const dat_extension_t *e, uint8_t *out, size_t cap,
|
||||
size_t *written)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!e || !out || (!e->message && e->message_len)) { errno = EINVAL; return -1; }
|
||||
if (put_varint(e->id, out, cap, &off)) return -1;
|
||||
if (e->message_len > cap - off) { errno = ENOSPC; return -1; }
|
||||
if (e->message_len) memcpy(out + off, e->message, e->message_len);
|
||||
off += e->message_len;
|
||||
if (written) *written = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dat_extension_decode(dat_extension_t *e, const uint8_t *in, size_t len)
|
||||
{
|
||||
uint64_t id;
|
||||
size_t n, off = 0;
|
||||
if (!e || !in || sleep_varint_decode(in, len, &id, &n)) return -1;
|
||||
off = n;
|
||||
memset(e, 0, sizeof(*e));
|
||||
e->id = id;
|
||||
e->message = in + off;
|
||||
e->message_len = len - off;
|
||||
return 0;
|
||||
}
|
||||
+96503
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
#include "sleep.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#define DAT_MKDIR(path) _mkdir(path)
|
||||
#else
|
||||
#define DAT_MKDIR(path) mkdir(path, 0700)
|
||||
#endif
|
||||
|
||||
static int make_directory(const char *path) {
|
||||
if (DAT_MKDIR(path) == 0 || errno == EEXIST) return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sleep_init(const char *root) {
|
||||
char path[1024]; int n;
|
||||
if (!root || !*root) { errno = EINVAL; return -1; }
|
||||
n = snprintf(path, sizeof(path), "%s/.dat", root);
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) { errno = ENAMETOOLONG; return -1; }
|
||||
return make_directory(path);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "sleep_bitfield.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
static int bit_get(const uint8_t *bits, size_t bytes, size_t bit) {
|
||||
if (bit >= bytes * 8) { errno = ERANGE; return -1; }
|
||||
return (bits[bit >> 3] >> (7 - (bit & 7))) & 1;
|
||||
}
|
||||
static int bit_set(uint8_t *bits, size_t bytes, size_t bit, int value) {
|
||||
uint8_t mask; if (bit >= bytes * 8) { errno = ERANGE; return -1; }
|
||||
mask = (uint8_t)(1u << (7 - (bit & 7))); if (value) bits[bit >> 3] |= mask; else bits[bit >> 3] &= (uint8_t)~mask; return 0;
|
||||
}
|
||||
static unsigned byte_state(uint8_t v) { return v == 0 ? 0u : (v == 0xff ? 3u : 2u); }
|
||||
void sleep_bitfield_clear(sleep_bitfield_t *f) { if (f) memset(f, 0, sizeof(*f)); }
|
||||
int sleep_bitfield_get_data(const sleep_bitfield_t *f, size_t bit) { if (!f) { errno = EINVAL; return -1; } return bit_get(f->data, sizeof(f->data), bit); }
|
||||
int sleep_bitfield_get_tree(const sleep_bitfield_t *f, size_t bit) { if (!f) { errno = EINVAL; return -1; } return bit_get(f->tree, sizeof(f->tree), bit); }
|
||||
void sleep_bitfield_reindex(sleep_bitfield_t *f) {
|
||||
size_t pair; if (!f) return; memset(f->index, 0, sizeof(f->index));
|
||||
for (pair = 0; pair < 512; pair++) { unsigned tuple = byte_state(f->data[pair*2]) | (byte_state(f->data[pair*2+1]) << 2); size_t byte = pair >> 1;
|
||||
f->index[byte] |= (uint8_t)(tuple << ((pair & 1) ? 4 : 0)); }
|
||||
}
|
||||
int sleep_bitfield_set_data(sleep_bitfield_t *f, size_t bit, int present) { if (!f) { errno = EINVAL; return -1; } if (bit_set(f->data, sizeof(f->data), bit, present)) return -1; sleep_bitfield_reindex(f); return 0; }
|
||||
int sleep_bitfield_set_tree(sleep_bitfield_t *f, size_t bit, int present) { if (!f) { errno = EINVAL; return -1; } return bit_set(f->tree, sizeof(f->tree), bit, present); }
|
||||
size_t sleep_bitfield_next_data(const sleep_bitfield_t *f, size_t start, int present) {
|
||||
size_t bit; if (!f) return SIZE_MAX; for (bit = start; bit < sizeof(f->data)*8; bit++) if (bit_get(f->data, sizeof(f->data), bit) == !!present) return bit; return SIZE_MAX;
|
||||
}
|
||||
int sleep_bitfield_encode(const sleep_bitfield_t *f, uint8_t *out, size_t cap) {
|
||||
if (!f || !out) { errno = EINVAL; return -1; } if (cap < sizeof(*f)) { errno = ENOSPC; return -1; }
|
||||
memcpy(out, f->data, 1024); memcpy(out+1024, f->tree, 2048); memcpy(out+3072, f->index, 256); memcpy(out+3328, f->reserved, 256); return 0;
|
||||
}
|
||||
int sleep_bitfield_decode(sleep_bitfield_t *f, const uint8_t *in, size_t len) {
|
||||
if (!f || !in) { errno = EINVAL; return -1; } if (len < sizeof(*f)) { errno = EPROTO; return -1; }
|
||||
memcpy(f->data, in, 1024); memcpy(f->tree, in+1024, 2048); memcpy(f->index, in+3072, 256); memcpy(f->reserved, in+3328, 256); return 0;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "sleep_crypto.h"
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <sodium.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void put64be(uint8_t *p, uint64_t v) {
|
||||
unsigned int i;
|
||||
for (i = 0; i < 8; i++) p[i] = (uint8_t)(v >> (56 - i * 8));
|
||||
}
|
||||
|
||||
static int encode_roots(uint8_t **out, size_t *out_len, const uint8_t *roots,
|
||||
size_t nroots, const uint64_t *indices, const uint64_t *lengths) {
|
||||
size_t i, len;
|
||||
uint8_t *p;
|
||||
if (!out || !out_len || (nroots && (!roots || !indices || !lengths))) { errno = EINVAL; return -1; }
|
||||
if (nroots > (SIZE_MAX - 1) / SLEEP_ROOT_RECORD_BYTES) { errno = EOVERFLOW; return -1; }
|
||||
len = 1 + nroots * SLEEP_ROOT_RECORD_BYTES;
|
||||
p = malloc(len);
|
||||
if (!p) return -1;
|
||||
p[0] = 2; /* legacy SLEEP root domain */
|
||||
for (i = 0; i < nroots; i++) {
|
||||
memcpy(p + 1 + i * 48, roots + i * 32, 32);
|
||||
put64be(p + 33 + i * 48, indices[i]);
|
||||
put64be(p + 41 + i * 48, lengths[i]);
|
||||
}
|
||||
*out = p; *out_len = len; return 0;
|
||||
}
|
||||
|
||||
int sleep_crypto_init(void) { return sodium_init() < 0 ? -1 : 0; }
|
||||
|
||||
int sleep_stream_xsalsa20_xor(uint8_t *out, const uint8_t *in, size_t length,
|
||||
const uint8_t nonce[24], const uint8_t key[32],
|
||||
uint64_t stream_offset)
|
||||
{
|
||||
uint8_t *aligned;
|
||||
size_t skip = (size_t)(stream_offset & 63u), total;
|
||||
uint64_t counter = stream_offset >> 6;
|
||||
if ((!out && length) || (!in && length) || !nonce || !key ||
|
||||
length > SIZE_MAX - skip) { errno = EINVAL; return -1; }
|
||||
total = skip + length;
|
||||
if (!total) return 0;
|
||||
if (counter > UINT64_MAX - (total / 64u + !!(total % 64u))) { errno = EOVERFLOW; return -1; }
|
||||
aligned = calloc(total, 1);
|
||||
if (!aligned) return -1;
|
||||
if (length) memcpy(aligned + skip, in, length);
|
||||
if (crypto_stream_xsalsa20_xor_ic(aligned, aligned, total, nonce, counter, key) != 0) {
|
||||
free(aligned); errno = EIO; return -1;
|
||||
}
|
||||
if (length) memmove(out, aligned + skip, length);
|
||||
sodium_memzero(aligned, total);
|
||||
free(aligned);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sleep_compute_discovery_key(const uint8_t pubkey[32], uint8_t out[32])
|
||||
{
|
||||
static const uint8_t domain[] = "hypercore";
|
||||
if (!pubkey || !out) { errno = EINVAL; return -1; }
|
||||
/* Dat discovery keys are BLAKE2b(key=feed public key, message="hypercore"). */
|
||||
return crypto_generichash(out, 32, domain, sizeof(domain) - 1, pubkey, 32);
|
||||
}
|
||||
|
||||
int sleep_gen_keypair(sleep_keypair_t *kp) {
|
||||
if (!kp) { errno = EINVAL; return -1; }
|
||||
if (crypto_sign_keypair(kp->pubkey, kp->seckey) !=0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sleep_save_pubkey(const char *path, const uint8_t pubkey[SLEEP_PUB_KEY_BYTES]) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
size_t written;
|
||||
if (!f) return -1;
|
||||
written = fwrite(pubkey, 1, SLEEP_PUB_KEY_BYTES, f);
|
||||
fclose(f);
|
||||
return (written == SLEEP_PUB_KEY_BYTES) ? 0 : -1;
|
||||
}
|
||||
|
||||
int sleep_load_pubkey(const char *path, uint8_t pubkey[SLEEP_PUB_KEY_BYTES]) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
size_t read = fread(pubkey, 1, SLEEP_PUB_KEY_BYTES, f);
|
||||
fclose(f);
|
||||
return (read == SLEEP_PUB_KEY_BYTES) ? 0 : -1;
|
||||
}
|
||||
|
||||
int sleep_save_keypair(const char *path,const sleep_keypair_t*kp){FILE*f;size_t n;if(!path||!kp){errno=EINVAL;return-1;}f=fopen(path,"wb");if(!f)return-1;n=fwrite(kp,1,sizeof(*kp),f);if(fclose(f))return-1;return n==sizeof(*kp)?0:-1;}
|
||||
int sleep_load_keypair(const char *path,sleep_keypair_t*kp){FILE*f;size_t n;if(!path||!kp){errno=EINVAL;return-1;}f=fopen(path,"rb");if(!f)return-1;n=fread(kp,1,sizeof(*kp),f);fclose(f);return n==sizeof(*kp)?0:-1;}
|
||||
|
||||
int sleep_blake2b(uint8_t out[SLEEP_BLAKE2B_BYTES], const uint8_t *in, size_t inlen) {
|
||||
return crypto_generichash(out, SLEEP_BLAKE2B_BYTES, in, inlen, NULL, 0);
|
||||
}
|
||||
|
||||
int sleep_sign(uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *msg, size_t msglen, const sleep_keypair_t *kp) {
|
||||
return crypto_sign_detached(sig, NULL, msg, msglen, kp->seckey);
|
||||
}
|
||||
|
||||
int sleep_verify(const uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *msg, size_t msglen, const uint8_t pubkey[SLEEP_PUB_KEY_BYTES]) {
|
||||
return crypto_sign_verify_detached(sig, msg, msglen, pubkey);
|
||||
}
|
||||
|
||||
int sleep_sign_roots(uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *roots, size_t nroots, const uint64_t *indices, const uint64_t *lengths, const sleep_keypair_t *kp) {
|
||||
uint8_t *message, digest[32]; size_t len; int rc;
|
||||
if (encode_roots(&message, &len, roots, nroots, indices, lengths)) return -1;
|
||||
rc = sleep_blake2b(digest, message, len); free(message);
|
||||
return rc ? rc : sleep_sign(sig, digest, sizeof(digest), kp);
|
||||
}
|
||||
|
||||
int sleep_verify_roots(const uint8_t sig[SLEEP_SIGNATURE_BYTES], const uint8_t *roots, size_t nroots, const uint64_t *indices, const uint64_t *lengths, const uint8_t pub[SLEEP_PUB_KEY_BYTES]) {
|
||||
uint8_t *message, digest[32]; size_t len; int rc;
|
||||
if (encode_roots(&message, &len, roots, nroots, indices, lengths)) return -1;
|
||||
rc = sleep_blake2b(digest, message, len); free(message);
|
||||
return rc ? rc : sleep_verify(sig, digest, sizeof(digest), pub);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "sleep_feed.h"
|
||||
#include "sleep.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include "sleep_writer.h"
|
||||
#include "sleep_bitfield.h"
|
||||
#include "sleep_proof.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static uint64_t get64be(const uint8_t *p) { uint64_t v=0; unsigned i; for(i=0;i<8;i++) v=(v<<8)|p[i]; return v; }
|
||||
static uint32_t get32be(const uint8_t *p) { return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3]; }
|
||||
static void put64be(uint8_t *p,uint64_t v){unsigned i;for(i=0;i<8;i++)p[i]=(uint8_t)(v>>(56-i*8));}
|
||||
static void put32be(uint8_t *p,uint32_t v){p[0]=(uint8_t)(v>>24);p[1]=(uint8_t)(v>>16);p[2]=(uint8_t)(v>>8);p[3]=(uint8_t)v;}
|
||||
static int path_for(char *out, size_t cap, const sleep_feed_t *f, const char *suffix) { int n=snprintf(out,cap,"%s/%s.%s",f->directory,f->name,suffix); if(n<0||(size_t)n>=cap){errno=ENAMETOOLONG;return -1;}return 0; }
|
||||
static int read_file(const char *path, uint8_t **out, size_t *len) {
|
||||
FILE *fp=fopen(path,"rb"); long n; uint8_t *p; if(!fp) return -1;
|
||||
if(fseek(fp,0,SEEK_END)||(n=ftell(fp))<0||fseek(fp,0,SEEK_SET)){fclose(fp);return -1;}
|
||||
p=malloc(n?(size_t)n:1); if(!p){fclose(fp);return -1;} if(n&&fread(p,1,(size_t)n,fp)!=(size_t)n){free(p);fclose(fp);errno=EIO;return -1;}
|
||||
fclose(fp);*out=p;*len=(size_t)n;return 0;
|
||||
}
|
||||
static int reserve_feed(sleep_feed_t *f,size_t needed,int with_proofs){
|
||||
size_t capacity=f->capacity?f->capacity:8,old=f->capacity;
|
||||
uint8_t **blocks,**proofs;
|
||||
size_t *lengths,*proof_lengths;
|
||||
if(needed<=f->capacity&&(!with_proofs||f->proofs))return 0;
|
||||
while(capacity<needed){if(capacity>SIZE_MAX/2){errno=EOVERFLOW;return-1;}capacity*=2;}
|
||||
if(capacity>SIZE_MAX/sizeof(*f->blocks)||capacity>SIZE_MAX/sizeof(*f->lengths)){errno=EOVERFLOW;return-1;}
|
||||
blocks=realloc(f->blocks,capacity*sizeof(*blocks));if(!blocks)return-1;f->blocks=blocks;
|
||||
lengths=realloc(f->lengths,capacity*sizeof(*lengths));if(!lengths)return-1;f->lengths=lengths;
|
||||
memset(f->blocks+old,0,(capacity-old)*sizeof(*f->blocks));
|
||||
memset(f->lengths+old,0,(capacity-old)*sizeof(*f->lengths));
|
||||
if(with_proofs||f->proofs){
|
||||
proofs=realloc(f->proofs,capacity*sizeof(*proofs));if(!proofs)return-1;f->proofs=proofs;
|
||||
proof_lengths=realloc(f->proof_lengths,capacity*sizeof(*proof_lengths));if(!proof_lengths)return-1;f->proof_lengths=proof_lengths;
|
||||
memset(f->proofs+old,0,(capacity-old)*sizeof(*f->proofs));
|
||||
memset(f->proof_lengths+old,0,(capacity-old)*sizeof(*f->proof_lengths));
|
||||
}
|
||||
f->capacity=capacity;return 0;
|
||||
}
|
||||
static int append_remote_record(sleep_feed_t *f,size_t index,const uint8_t *data,size_t len,const uint8_t *proof,size_t proof_len){
|
||||
static const uint8_t magic[8]={'S','F','E','E','D','R','1',0};
|
||||
char path[1200];uint8_t header[40],record[16];FILE *fp;long end;
|
||||
if(len>UINT32_MAX||proof_len>UINT32_MAX||path_for(path,sizeof(path),f,"remote"))return-1;
|
||||
fp=fopen(path,"ab+");if(!fp)return-1;
|
||||
if(fseek(fp,0,SEEK_END)||(end=ftell(fp))<0){fclose(fp);return-1;}
|
||||
if(end==0){memcpy(header,magic,8);memcpy(header+8,f->keypair.pubkey,32);if(fwrite(header,1,sizeof(header),fp)!=sizeof(header)){fclose(fp);errno=EIO;return-1;}}
|
||||
put64be(record,(uint64_t)index);put32be(record+8,(uint32_t)len);put32be(record+12,(uint32_t)proof_len);
|
||||
if(fwrite(record,1,sizeof(record),fp)!=sizeof(record)||(len&&fwrite(data,1,len,fp)!=len)||(proof_len&&fwrite(proof,1,proof_len,fp)!=proof_len)||fflush(fp)||fsync(fileno(fp))){fclose(fp);errno=EIO;return-1;}
|
||||
return fclose(fp);
|
||||
}
|
||||
int sleep_feed_open(sleep_feed_t *f,const char *dir,const char *name,const sleep_keypair_t *kp) {
|
||||
char path[1200]; uint8_t *tree=NULL,*data=NULL; size_t tree_len=0,data_len=0,count,i,off=0;
|
||||
if(!f||!dir||!name){errno=EINVAL;return -1;} memset(f,0,sizeof(*f));
|
||||
if(strlen(dir)>=sizeof(f->directory)||strlen(name)>=sizeof(f->name)){errno=ENAMETOOLONG;return -1;} strcpy(f->directory,dir);strcpy(f->name,name);
|
||||
if(kp){f->keypair=*kp;f->writable=1;}
|
||||
if(path_for(path,sizeof(path),f,"tree")||read_file(path,&tree,&tree_len)) {
|
||||
if(errno==ENOENT&&kp)return 0;
|
||||
if(errno==ENOENT&&!kp){
|
||||
char key_path[1200]; uint8_t public_key[32]; int n;
|
||||
n=snprintf(key_path,sizeof(key_path),"%s/%s.key",dir,name);
|
||||
if(n<0||(size_t)n>=sizeof(key_path)){errno=ENAMETOOLONG;return-1;}
|
||||
if(sleep_load_pubkey(key_path,public_key))return-1;
|
||||
return sleep_feed_open_remote(f,dir,name,public_key);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(tree_len<32||(tree_len-32)%40){free(tree);errno=EPROTO;return -1;} count=((tree_len-32)/40+1)/2;
|
||||
if(path_for(path,sizeof(path),f,"data")||read_file(path,&data,&data_len)){free(tree);return -1;}
|
||||
f->blocks=calloc(count,sizeof(*f->blocks));f->lengths=calloc(count,sizeof(*f->lengths));f->capacity=count;if(count&&(!f->blocks||!f->lengths))goto fail;
|
||||
for(i=0;i<count;i++){uint64_t n=get64be(tree+32+i*80+32);if(n>SIZE_MAX||n>data_len-off){errno=EPROTO;goto fail;}f->lengths[i]=(size_t)n;f->blocks[i]=malloc(n?(size_t)n:1);if(!f->blocks[i])goto fail;if(n)memcpy(f->blocks[i],data+off,(size_t)n);off+=(size_t)n;f->length++;}
|
||||
free(tree);free(data);return 0;
|
||||
fail: free(tree);free(data);sleep_feed_close(f);return -1;
|
||||
}
|
||||
int sleep_feed_open_remote(sleep_feed_t *f,const char *dir,const char *name,const uint8_t public_key[32]) {
|
||||
static const uint8_t magic[8]={'S','F','E','E','D','R','1',0};
|
||||
char path[1200];uint8_t *bytes=NULL;size_t length=0,off=40;
|
||||
if(!f||!dir||!name||!public_key){errno=EINVAL;return-1;}
|
||||
if(strlen(dir)>=sizeof(f->directory)||strlen(name)>=sizeof(f->name)){errno=ENAMETOOLONG;return-1;}
|
||||
memset(f,0,sizeof(*f));strcpy(f->directory,dir);strcpy(f->name,name);
|
||||
memcpy(f->keypair.pubkey,public_key,32);f->remote_only=1;f->writable=0;f->persist_remote=1;
|
||||
if(path_for(path,sizeof(path),f,"remote")||read_file(path,&bytes,&length)){
|
||||
if(errno==ENOENT)return 0;
|
||||
sleep_feed_close(f);return-1;
|
||||
}
|
||||
if(length<40||memcmp(bytes,magic,8)||memcmp(bytes+8,public_key,32))goto corrupt;
|
||||
f->persist_remote=0;
|
||||
while(off<length){
|
||||
uint64_t index;uint32_t data_len,proof_len;sleep_proof_t proof;
|
||||
if(length-off<16)goto corrupt;
|
||||
index=get64be(bytes+off);data_len=get32be(bytes+off+8);proof_len=get32be(bytes+off+12);off+=16;
|
||||
if(index>=SLEEP_BITFIELD_DATA_BYTES*8u||!proof_len||data_len>length-off||proof_len>length-off-data_len)goto corrupt;
|
||||
if(sleep_proof_decode(&proof,bytes+off+data_len,proof_len)||proof.block!=index||
|
||||
sleep_proof_verify(&proof,bytes+off,data_len,public_key))goto corrupt;
|
||||
if(sleep_feed_append_remote(f,(size_t)index,bytes+off,data_len,
|
||||
bytes+off+data_len,proof_len))goto corrupt;
|
||||
off+=(size_t)data_len+proof_len;
|
||||
}
|
||||
f->persist_remote=1;free(bytes);return 0;
|
||||
corrupt:
|
||||
free(bytes);sleep_feed_close(f);errno=EBADMSG;return-1;
|
||||
}
|
||||
int sleep_feed_load_remote(sleep_feed_t *f,const uint8_t public_key[32]) {
|
||||
sleep_feed_t cached;
|
||||
int persist,rc=0;
|
||||
size_t i;
|
||||
if(!f||!public_key||f->writable){errno=EINVAL;return-1;}
|
||||
if(sleep_feed_open_remote(&cached,f->directory,f->name,public_key))return-1;
|
||||
persist=f->persist_remote;
|
||||
f->persist_remote=0;
|
||||
memcpy(f->keypair.pubkey,public_key,32);
|
||||
for(i=0;i<cached.length;i++)if(cached.blocks[i]&&!sleep_feed_has(f,i)){
|
||||
if(sleep_feed_append_remote(f,i,cached.blocks[i],cached.lengths[i],
|
||||
cached.proofs[i],cached.proof_lengths[i])){rc=-1;break;}
|
||||
}
|
||||
f->persist_remote=persist;
|
||||
sleep_feed_close(&cached);
|
||||
return rc;
|
||||
}
|
||||
size_t sleep_feed_length(const sleep_feed_t *f){return f?f->length:0;}
|
||||
int sleep_feed_read(const sleep_feed_t *f,size_t index,uint8_t **out,size_t *len){uint8_t *p;if(!f||!out||!len||index>=f->length){errno=EINVAL;return -1;}if(!f->blocks[index]){errno=ENODATA;return-1;}p=malloc(f->lengths[index]?f->lengths[index]:1);if(!p)return -1;if(f->lengths[index])memcpy(p,f->blocks[index],f->lengths[index]);*out=p;*len=f->lengths[index];return 0;}
|
||||
int sleep_feed_append(sleep_feed_t *f,const uint8_t *data,size_t len){uint8_t *copy;if(!f||!f->writable||(!data&&len)){errno=EINVAL;return-1;}if(reserve_feed(f,f->length+1,0))return-1;copy=malloc(len?len:1);if(!copy)return-1;if(len)memcpy(copy,data,len);f->blocks[f->length]=copy;f->lengths[f->length]=len;f->length++;f->dirty=1;return 0;}
|
||||
int sleep_feed_append_remote(sleep_feed_t *f,size_t index,const uint8_t *data,size_t len,const uint8_t *proof,size_t proof_len){uint8_t *copy,*proof_copy;if(!f||f->writable||index>=SLEEP_BITFIELD_DATA_BYTES*8u||(!data&&len)||!proof||!proof_len){errno=EINVAL;return-1;}if(index<f->length&&f->blocks[index]){errno=EEXIST;return-1;}if(reserve_feed(f,index+1,1))return-1;copy=malloc(len?len:1);proof_copy=malloc(proof_len);if(!copy||!proof_copy){free(copy);free(proof_copy);return-1;}if(len)memcpy(copy,data,len);memcpy(proof_copy,proof,proof_len);if(f->persist_remote&&append_remote_record(f,index,data,len,proof,proof_len)){free(copy);free(proof_copy);return-1;}f->blocks[index]=copy;f->lengths[index]=len;f->proofs[index]=proof_copy;f->proof_lengths[index]=proof_len;if(f->length<=index)f->length=index+1;f->remote_only=1;return 0;}
|
||||
int sleep_feed_truncate(sleep_feed_t*f,size_t length){size_t i;if(!f||!f->writable||length>f->length){errno=EINVAL;return-1;}for(i=length;i<f->length;i++)free(f->blocks[i]);f->length=length;f->dirty=1;return 0;}
|
||||
int sleep_feed_has(const sleep_feed_t*f,size_t index){return f&&index<f->length&&f->blocks[index]!=NULL;}
|
||||
int sleep_feed_byte_offset(const sleep_feed_t*f,size_t index,uint64_t*out){size_t i;uint64_t n=0;if(!f||!out||index>f->length){errno=EINVAL;return-1;}for(i=0;i<index;i++){if(!f->blocks[i]){errno=ENODATA;return-1;}if(UINT64_MAX-n<f->lengths[i]){errno=EOVERFLOW;return-1;}n+=f->lengths[i];}*out=n;return 0;}
|
||||
int sleep_feed_flush(sleep_feed_t *f){if(!f||!f->writable){errno=EINVAL;return-1;}if(!f->dirty)return 0;if(sleep_write_feed(f->directory,f->name,(const uint8_t *const*)f->blocks,f->lengths,f->length,&f->keypair))return-1;f->dirty=0;return 0;}
|
||||
void sleep_feed_close(sleep_feed_t *f){size_t i;if(!f)return;for(i=0;i<f->length;i++){free(f->blocks?f->blocks[i]:NULL);free(f->proofs?f->proofs[i]:NULL);}free(f->blocks);free(f->lengths);free(f->proofs);free(f->proof_lengths);memset(f,0,sizeof(*f));}
|
||||
Loaded 100 of 323 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user