mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
trying embedded caddy reverse proxy
This commit is contained in:
27
proxy/.gitignore
vendored
Normal file
27
proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Binaries
|
||||
bin/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Output of go coverage tool
|
||||
*.out
|
||||
|
||||
# Configuration files (keep example)
|
||||
config.json
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
661
proxy/LICENSE
Normal file
661
proxy/LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
83
proxy/Makefile
Normal file
83
proxy/Makefile
Normal file
@@ -0,0 +1,83 @@
|
||||
.PHONY: build clean run test help version proto
|
||||
|
||||
# Build variables
|
||||
BINARY_NAME=proxy
|
||||
BUILD_DIR=bin
|
||||
|
||||
# Version variables (can be overridden)
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
BUILD_DATE ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
|
||||
# Go linker flags for version injection
|
||||
LDFLAGS=-ldflags "-X github.com/netbirdio/netbird/proxy/pkg/version.Version=$(VERSION) \
|
||||
-X github.com/netbirdio/netbird/proxy/pkg/version.Commit=$(COMMIT) \
|
||||
-X github.com/netbirdio/netbird/proxy/pkg/version.BuildDate=$(BUILD_DATE)"
|
||||
|
||||
# Build the binary
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Commit: $(COMMIT)"
|
||||
@echo "BuildDate: $(BUILD_DATE)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOWORK=off go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
# Show version information
|
||||
version:
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Commit: $(COMMIT)"
|
||||
@echo "BuildDate: $(BUILD_DATE)"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@go clean
|
||||
@echo "Clean complete"
|
||||
|
||||
# Run the application (requires NB_PROXY_TARGET_URL to be set)
|
||||
run: build
|
||||
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
GOWORK=off go test -v ./...
|
||||
|
||||
# Install dependencies
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
GOWORK=off go mod download
|
||||
GOWORK=off go mod tidy
|
||||
@echo "Dependencies installed"
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
@echo "Formatting code..."
|
||||
@go fmt ./...
|
||||
@echo "Format complete"
|
||||
|
||||
# Lint code
|
||||
lint:
|
||||
@echo "Linting code..."
|
||||
@golangci-lint run
|
||||
@echo "Lint complete"
|
||||
|
||||
# Generate protobuf files
|
||||
proto:
|
||||
@echo "Generating protobuf files..."
|
||||
@./scripts/generate-proto.sh
|
||||
|
||||
# Show help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build the binary"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " run - Build and run the application"
|
||||
@echo " test - Run tests"
|
||||
@echo " proto - Generate protobuf files"
|
||||
@echo " deps - Install dependencies"
|
||||
@echo " fmt - Format code"
|
||||
@echo " lint - Lint code"
|
||||
@echo " help - Show this help message"
|
||||
177
proxy/README.md
Normal file
177
proxy/README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Netbird Reverse Proxy
|
||||
|
||||
A lightweight, configurable reverse proxy server with graceful shutdown support.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple reverse proxy with customizable headers
|
||||
- Configuration via environment variables or JSON file
|
||||
- Graceful shutdown with configurable timeout
|
||||
- Structured logging with logrus
|
||||
- Configurable timeouts (read, write, idle)
|
||||
- Health monitoring support
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build the binary
|
||||
GOWORK=off go build -o bin/proxy ./cmd/proxy
|
||||
|
||||
# Or use make if available
|
||||
make build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The proxy can be configured using either environment variables or a JSON configuration file. Environment variables take precedence over file-based configuration.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `NB_PROXY_LISTEN_ADDRESS` | Address to listen on | `:8080` |
|
||||
| `NB_PROXY_TARGET_URL` | Target URL to proxy requests to | **(required)** |
|
||||
| `NB_PROXY_READ_TIMEOUT` | Read timeout duration | `30s` |
|
||||
| `NB_PROXY_WRITE_TIMEOUT` | Write timeout duration | `30s` |
|
||||
| `NB_PROXY_IDLE_TIMEOUT` | Idle timeout duration | `60s` |
|
||||
| `NB_PROXY_SHUTDOWN_TIMEOUT` | Graceful shutdown timeout | `10s` |
|
||||
| `NB_PROXY_LOG_LEVEL` | Log level (debug, info, warn, error) | `info` |
|
||||
|
||||
### Configuration File
|
||||
|
||||
Create a JSON configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"listen_address": ":8080",
|
||||
"target_url": "http://localhost:3000",
|
||||
"read_timeout": "30s",
|
||||
"write_timeout": "30s",
|
||||
"idle_timeout": "60s",
|
||||
"shutdown_timeout": "10s",
|
||||
"log_level": "info"
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
```bash
|
||||
export NB_PROXY_TARGET_URL=http://localhost:3000
|
||||
export NB_PROXY_LOG_LEVEL=debug
|
||||
./bin/proxy
|
||||
```
|
||||
|
||||
### Using Configuration File
|
||||
|
||||
```bash
|
||||
./bin/proxy -config config.json
|
||||
```
|
||||
|
||||
### Combining Both
|
||||
|
||||
Environment variables override file configuration:
|
||||
|
||||
```bash
|
||||
export NB_PROXY_LOG_LEVEL=debug
|
||||
./bin/proxy -config config.json
|
||||
```
|
||||
|
||||
### Docker Example
|
||||
|
||||
```bash
|
||||
docker run -e NB_PROXY_TARGET_URL=http://backend:3000 \
|
||||
-e NB_PROXY_LISTEN_ADDRESS=:8080 \
|
||||
-p 8080:8080 \
|
||||
netbird-proxy
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The application follows a clean architecture with clear separation of concerns:
|
||||
|
||||
```
|
||||
proxy/
|
||||
├── cmd/
|
||||
│ └── proxy/
|
||||
│ └── main.go # Entry point, CLI handling, signal management
|
||||
├── config.go # Configuration loading and validation
|
||||
├── server.go # Server lifecycle (Start/Stop)
|
||||
├── go.mod # Module dependencies
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **config.go**: Handles configuration loading from environment variables and files using the `github.com/caarlos0/env/v11` library
|
||||
- **server.go**: Encapsulates the HTTP server and reverse proxy logic with proper lifecycle management
|
||||
- **cmd/proxy/main.go**: Entry point that orchestrates startup, graceful shutdown, and signal handling
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
The server handles SIGINT and SIGTERM signals for graceful shutdown:
|
||||
|
||||
1. Signal received (Ctrl+C or kill command)
|
||||
2. Server stops accepting new connections
|
||||
3. Existing connections are allowed to complete within the shutdown timeout
|
||||
4. Server exits cleanly
|
||||
|
||||
Press `Ctrl+C` to trigger graceful shutdown:
|
||||
|
||||
```bash
|
||||
^C2026-01-13 22:40:00 INFO Received signal: interrupt
|
||||
2026-01-13 22:40:00 INFO Shutting down server gracefully...
|
||||
2026-01-13 22:40:00 INFO Server stopped successfully
|
||||
2026-01-13 22:40:00 INFO Server exited successfully
|
||||
```
|
||||
|
||||
## Headers
|
||||
|
||||
The proxy automatically sets the following headers on proxied requests:
|
||||
|
||||
- `X-Forwarded-Host`: Original request host
|
||||
- `X-Origin-Host`: Target backend host
|
||||
- `X-Real-IP`: Client's remote address
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Invalid backend connections return `502 Bad Gateway`
|
||||
- All proxy errors are logged with details
|
||||
- Configuration errors are reported at startup
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.25 or higher
|
||||
- Access to `github.com/sirupsen/logrus`
|
||||
- Access to `github.com/caarlos0/env/v11`
|
||||
|
||||
### Testing Locally
|
||||
|
||||
Start a test backend:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start a simple backend
|
||||
python3 -m http.server 3000
|
||||
```
|
||||
|
||||
Start the proxy:
|
||||
|
||||
```bash
|
||||
# Terminal 2: Start the proxy
|
||||
export NB_PROXY_TARGET_URL=http://localhost:3000
|
||||
./bin/proxy
|
||||
```
|
||||
|
||||
Test the proxy:
|
||||
|
||||
```bash
|
||||
# Terminal 3: Make requests
|
||||
curl http://localhost:8080
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Part of the Netbird project.
|
||||
120
proxy/cmd/root.go
Normal file
120
proxy/cmd/root.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/pkg/proxy"
|
||||
"github.com/netbirdio/netbird/proxy/pkg/version"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile string
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "proxy",
|
||||
Short: "Netbird Reverse Proxy Server",
|
||||
Long: "A lightweight, configurable reverse proxy server.",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: run,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "path to JSON configuration file (optional, can use env vars instead)")
|
||||
|
||||
// Set version information
|
||||
rootCmd.Version = version.Short()
|
||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||
}
|
||||
|
||||
// Execute runs the root command
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
// Load configuration from file or environment variables
|
||||
config, err := proxy.LoadFromFileOrEnv(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set log level
|
||||
setupLogging(config.LogLevel)
|
||||
|
||||
log.Infof("Starting Netbird Proxy - %s", version.Short())
|
||||
log.Debugf("Full version info: %s", version.String())
|
||||
log.Info("Configuration loaded successfully")
|
||||
log.Infof("Listen Address: %s", config.ListenAddress)
|
||||
log.Infof("Log Level: %s", config.LogLevel)
|
||||
|
||||
// Create server instance
|
||||
server, err := proxy.NewServer(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start server in a goroutine
|
||||
serverErrors := make(chan error, 1)
|
||||
go func() {
|
||||
if err := server.Start(); err != nil {
|
||||
serverErrors <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up signal handler for graceful shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Wait for either an error or shutdown signal
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
log.Fatalf("Server error: %v", err)
|
||||
return err
|
||||
case sig := <-quit:
|
||||
log.Infof("Received signal: %v", sig)
|
||||
}
|
||||
|
||||
// Create shutdown context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Gracefully stop the server
|
||||
if err := server.Stop(ctx); err != nil {
|
||||
log.Fatalf("Failed to stop server gracefully: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Server exited successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupLogging(level string) {
|
||||
// Set log format
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
})
|
||||
|
||||
// Set log level
|
||||
switch level {
|
||||
case "debug":
|
||||
log.SetLevel(log.DebugLevel)
|
||||
case "info":
|
||||
log.SetLevel(log.InfoLevel)
|
||||
case "warn":
|
||||
log.SetLevel(log.WarnLevel)
|
||||
case "error":
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
default:
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
}
|
||||
9
proxy/config.example.json
Normal file
9
proxy/config.example.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"listen_address": ":8080",
|
||||
"target_url": "http://localhost:3000",
|
||||
"read_timeout": "30s",
|
||||
"write_timeout": "30s",
|
||||
"idle_timeout": "60s",
|
||||
"shutdown_timeout": "10s",
|
||||
"log_level": "info"
|
||||
}
|
||||
135
proxy/go.mod
Normal file
135
proxy/go.mod
Normal file
@@ -0,0 +1,135 @@
|
||||
module github.com/netbirdio/netbird/proxy
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/caddyserver/caddy/v2 v2.10.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.10.2
|
||||
go.uber.org/zap v1.27.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.24.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/ccoveille/go-safecast v1.6.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/libdns/libdns v1.1.0 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/slackhq/nebula v1.9.5 // indirect
|
||||
github.com/smallstep/certificates v0.28.4 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.1 // indirect
|
||||
github.com/smallstep/linkedca v0.23.0 // indirect
|
||||
github.com/smallstep/nosql v0.7.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect
|
||||
github.com/smallstep/truststore v0.13.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.step.sm/crypto v0.67.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/api v0.240.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
641
proxy/go.sum
Normal file
641
proxy/go.sum
Normal file
@@ -0,0 +1,641 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
|
||||
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
|
||||
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
|
||||
github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0=
|
||||
github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
|
||||
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
|
||||
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
|
||||
github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
|
||||
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
||||
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
|
||||
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY=
|
||||
github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw=
|
||||
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
|
||||
github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
|
||||
github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
|
||||
github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8=
|
||||
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
||||
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc=
|
||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4=
|
||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y=
|
||||
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
|
||||
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU=
|
||||
go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU=
|
||||
google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
109
proxy/internal/health/health.go
Normal file
109
proxy/internal/health/health.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Status represents the health status of the application
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusHealthy Status = "healthy"
|
||||
StatusUnhealthy Status = "unhealthy"
|
||||
StatusDegraded Status = "degraded"
|
||||
)
|
||||
|
||||
// Check represents a health check
|
||||
type Check struct {
|
||||
Name string `json:"name"`
|
||||
Status Status `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Response represents the health check response
|
||||
type Response struct {
|
||||
Status Status `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Uptime time.Duration `json:"uptime_seconds"`
|
||||
Checks map[string]Check `json:"checks,omitempty"`
|
||||
}
|
||||
|
||||
// Checker is the interface for health checks
|
||||
type Checker interface {
|
||||
Check() Check
|
||||
}
|
||||
|
||||
// Handler manages health checks
|
||||
type Handler struct {
|
||||
mu sync.RWMutex
|
||||
checkers map[string]Checker
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewHandler creates a new health check handler
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
checkers: make(map[string]Checker),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChecker registers a health checker
|
||||
func (h *Handler) RegisterChecker(name string, checker Checker) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.checkers[name] = checker
|
||||
}
|
||||
|
||||
// ServeHTTP handles health check requests
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
response := Response{
|
||||
Status: StatusHealthy,
|
||||
Timestamp: time.Now(),
|
||||
Uptime: time.Since(h.startTime),
|
||||
Checks: make(map[string]Check),
|
||||
}
|
||||
|
||||
// Run all health checks
|
||||
for name, checker := range h.checkers {
|
||||
check := checker.Check()
|
||||
response.Checks[name] = check
|
||||
|
||||
// Update overall status
|
||||
if check.Status == StatusUnhealthy {
|
||||
response.Status = StatusUnhealthy
|
||||
} else if check.Status == StatusDegraded && response.Status != StatusUnhealthy {
|
||||
response.Status = StatusDegraded
|
||||
}
|
||||
}
|
||||
|
||||
// Set HTTP status code based on health
|
||||
statusCode := http.StatusOK
|
||||
if response.Status == StatusUnhealthy {
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
} else if response.Status == StatusDegraded {
|
||||
statusCode = http.StatusOK // Still return 200 for degraded
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ReadinessHandler returns a simple readiness probe handler
|
||||
func ReadinessHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ready"))
|
||||
}
|
||||
|
||||
// LivenessHandler returns a simple liveness probe handler
|
||||
func LivenessHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("alive"))
|
||||
}
|
||||
12
proxy/internal/middleware/chain.go
Normal file
12
proxy/internal/middleware/chain.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Chain creates a middleware chain
|
||||
func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
|
||||
// Apply middlewares in reverse order so they execute in the order provided
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
handler = middlewares[i](handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
55
proxy/internal/middleware/logging.go
Normal file
55
proxy/internal/middleware/logging.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// responseWriter wraps http.ResponseWriter to capture status code
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
written int64
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
n, err := rw.ResponseWriter.Write(b)
|
||||
rw.written += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Logging middleware logs HTTP requests with details
|
||||
func Logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Wrap the response writer
|
||||
wrapped := &responseWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// Call the next handler
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
// Log request details
|
||||
duration := time.Since(start)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"status": wrapped.statusCode,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"bytes": wrapped.written,
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"user_agent": r.UserAgent(),
|
||||
}).Info("HTTP request")
|
||||
})
|
||||
}
|
||||
33
proxy/internal/middleware/recovery.go
Normal file
33
proxy/internal/middleware/recovery.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Recovery middleware recovers from panics and logs the error
|
||||
func Recovery(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Log the panic with stack trace
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"stack": string(debug.Stack()),
|
||||
"remote_addr": r.RemoteAddr,
|
||||
}).Error("Panic recovered")
|
||||
|
||||
// Return 500 Internal Server Error
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Internal Server Error")
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
626
proxy/internal/reverseproxy/caddy.go
Normal file
626
proxy/internal/reverseproxy/caddy.go
Normal file
@@ -0,0 +1,626 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/caddyserver/caddy/v2/modules/logging"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// CaddyProxy wraps Caddy's reverse proxy functionality
|
||||
type CaddyProxy struct {
|
||||
config Config
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
routes map[string]*RouteConfig // key is route ID
|
||||
requestCallback RequestDataCallback
|
||||
// customHandlers stores handlers with custom transports that can't be JSON-serialized
|
||||
// key is "routeID:path" to uniquely identify each handler
|
||||
customHandlers map[string]*reverseproxy.Handler
|
||||
}
|
||||
|
||||
// Config holds the reverse proxy configuration
|
||||
type Config struct {
|
||||
// ListenAddress is the address to listen on
|
||||
ListenAddress string
|
||||
|
||||
// EnableHTTPS enables automatic HTTPS with Let's Encrypt
|
||||
EnableHTTPS bool
|
||||
|
||||
// TLSEmail is the email for Let's Encrypt registration
|
||||
TLSEmail string
|
||||
|
||||
// RequestDataCallback is called for each proxied request with metrics
|
||||
RequestDataCallback RequestDataCallback
|
||||
}
|
||||
|
||||
// RouteConfig defines a routing configuration
|
||||
type RouteConfig struct {
|
||||
// ID is a unique identifier for this route
|
||||
ID string
|
||||
|
||||
// Domain is the domain to listen on (e.g., "example.com" or "*" for all)
|
||||
Domain string
|
||||
|
||||
// PathMappings defines paths that should be forwarded to specific ports
|
||||
// Key is the path prefix (e.g., "/", "/api", "/admin")
|
||||
// Value is the target IP:port (e.g., "192.168.1.100:3000")
|
||||
// Must have at least one entry. Use "/" or "" for the default/catch-all route.
|
||||
PathMappings map[string]string
|
||||
|
||||
// Conn is an optional existing network connection to use for this route
|
||||
// This allows routing through specific tunnels (e.g., WireGuard) per route
|
||||
// If set, this connection will be reused for all requests to this route
|
||||
Conn net.Conn
|
||||
|
||||
// CustomDialer is an optional custom dialer for this specific route
|
||||
// This is used if Conn is not set. It allows using different network connections per route
|
||||
CustomDialer func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// New creates a new Caddy-based reverse proxy
|
||||
func New(config Config) (*CaddyProxy, error) {
|
||||
// Default to port 443 if not specified
|
||||
if config.ListenAddress == "" {
|
||||
config.ListenAddress = ":443"
|
||||
}
|
||||
|
||||
cp := &CaddyProxy{
|
||||
config: config,
|
||||
isRunning: false,
|
||||
routes: make(map[string]*RouteConfig),
|
||||
requestCallback: config.RequestDataCallback,
|
||||
customHandlers: make(map[string]*reverseproxy.Handler),
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// Start starts the Caddy reverse proxy server
|
||||
func (cp *CaddyProxy) Start() error {
|
||||
cp.mu.Lock()
|
||||
if cp.isRunning {
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy already running")
|
||||
}
|
||||
cp.isRunning = true
|
||||
cp.mu.Unlock()
|
||||
|
||||
// Build Caddy configuration
|
||||
cfg, err := cp.buildCaddyConfig()
|
||||
if err != nil {
|
||||
cp.mu.Lock()
|
||||
cp.isRunning = false
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("failed to build Caddy config: %w", err)
|
||||
}
|
||||
|
||||
// Run Caddy with the configuration
|
||||
err = caddy.Run(cfg)
|
||||
if err != nil {
|
||||
cp.mu.Lock()
|
||||
cp.isRunning = false
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("failed to run Caddy: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Caddy reverse proxy started on %s", cp.config.ListenAddress)
|
||||
log.Infof("Configured %d route(s)", len(cp.routes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the Caddy reverse proxy
|
||||
func (cp *CaddyProxy) Stop(ctx context.Context) error {
|
||||
cp.mu.Lock()
|
||||
if !cp.isRunning {
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy not running")
|
||||
}
|
||||
cp.mu.Unlock()
|
||||
|
||||
log.Info("Stopping Caddy reverse proxy...")
|
||||
|
||||
// Stop Caddy
|
||||
if err := caddy.Stop(); err != nil {
|
||||
return fmt.Errorf("failed to stop Caddy: %w", err)
|
||||
}
|
||||
|
||||
cp.mu.Lock()
|
||||
cp.isRunning = false
|
||||
cp.mu.Unlock()
|
||||
|
||||
log.Info("Caddy reverse proxy stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCaddyConfig builds the Caddy configuration
|
||||
func (cp *CaddyProxy) buildCaddyConfig() (*caddy.Config, error) {
|
||||
cp.mu.RLock()
|
||||
defer cp.mu.RUnlock()
|
||||
|
||||
if len(cp.routes) == 0 {
|
||||
// Create a default empty server that returns 404
|
||||
httpServer := &caddyhttp.Server{
|
||||
Listen: []string{cp.config.ListenAddress},
|
||||
Routes: caddyhttp.RouteList{},
|
||||
}
|
||||
|
||||
httpApp := &caddyhttp.App{
|
||||
Servers: map[string]*caddyhttp.Server{
|
||||
"proxy": httpServer,
|
||||
},
|
||||
}
|
||||
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{
|
||||
Disabled: true,
|
||||
},
|
||||
AppsRaw: caddy.ModuleMap{
|
||||
"http": caddyconfig.JSON(httpApp, nil),
|
||||
},
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Build routes grouped by domain
|
||||
domainRoutes := make(map[string][]caddyhttp.Route)
|
||||
// Track unique service IDs for logger configuration
|
||||
serviceIDs := make(map[string]bool)
|
||||
|
||||
for _, routeConfig := range cp.routes {
|
||||
domain := routeConfig.Domain
|
||||
if domain == "" {
|
||||
domain = "*" // wildcard for all domains
|
||||
}
|
||||
|
||||
// Register callback for this service ID
|
||||
if cp.requestCallback != nil {
|
||||
RegisterCallback(routeConfig.ID, cp.requestCallback)
|
||||
serviceIDs[routeConfig.ID] = true
|
||||
}
|
||||
|
||||
// Sort path mappings by path length (longest first) for proper matching
|
||||
// This ensures more specific paths match before catch-all paths
|
||||
paths := make([]string, 0, len(routeConfig.PathMappings))
|
||||
for path := range routeConfig.PathMappings {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Slice(paths, func(i, j int) bool {
|
||||
// Sort by length descending, but put empty string last (catch-all)
|
||||
if paths[i] == "" || paths[i] == "/" {
|
||||
return false
|
||||
}
|
||||
if paths[j] == "" || paths[j] == "/" {
|
||||
return true
|
||||
}
|
||||
return len(paths[i]) > len(paths[j])
|
||||
})
|
||||
|
||||
// Create routes for each path mapping
|
||||
for _, path := range paths {
|
||||
target := routeConfig.PathMappings[path]
|
||||
route := cp.createRoute(routeConfig, path, target)
|
||||
domainRoutes[domain] = append(domainRoutes[domain], route)
|
||||
}
|
||||
}
|
||||
|
||||
// Build Caddy routes
|
||||
var caddyRoutes caddyhttp.RouteList
|
||||
for domain, routes := range domainRoutes {
|
||||
if domain != "*" {
|
||||
// Add host matcher for specific domains
|
||||
for i := range routes {
|
||||
routes[i].MatcherSetsRaw = []caddy.ModuleMap{
|
||||
{
|
||||
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
caddyRoutes = append(caddyRoutes, routes...)
|
||||
}
|
||||
|
||||
// Create HTTP server with access logging if callback is set
|
||||
httpServer := &caddyhttp.Server{
|
||||
Listen: []string{cp.config.ListenAddress},
|
||||
Routes: caddyRoutes,
|
||||
}
|
||||
|
||||
// Configure server logging if callback is set
|
||||
if cp.requestCallback != nil {
|
||||
httpServer.Logs = &caddyhttp.ServerLogConfig{
|
||||
// Use our custom logger for access logs
|
||||
LoggerNames: map[string]caddyhttp.StringArray{
|
||||
"http.log.access": {"http_access"},
|
||||
},
|
||||
// Disable default access logging (only use custom logger)
|
||||
ShouldLogCredentials: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Disable automatic HTTPS if not enabled
|
||||
if !cp.config.EnableHTTPS {
|
||||
// Explicitly disable automatic HTTPS for the server
|
||||
httpServer.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{
|
||||
Disabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Build HTTP app
|
||||
httpApp := &caddyhttp.App{
|
||||
Servers: map[string]*caddyhttp.Server{
|
||||
"proxy": httpServer,
|
||||
},
|
||||
}
|
||||
|
||||
// Provision the HTTP app to set up handlers from JSON
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
|
||||
if err := httpApp.Provision(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to provision HTTP app: %w", err)
|
||||
}
|
||||
|
||||
// After provisioning, inject custom transports into handlers
|
||||
// This is done post-provisioning so the Transport field is preserved
|
||||
if err := cp.injectCustomTransports(httpApp); err != nil {
|
||||
return nil, fmt.Errorf("failed to inject custom transports: %w", err)
|
||||
}
|
||||
|
||||
// Create Caddy config with the provisioned app
|
||||
// IMPORTANT: We pass the already-provisioned app, not JSON
|
||||
// This preserves the Transport fields we set
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{
|
||||
Disabled: true,
|
||||
},
|
||||
// Apps field takes already-provisioned apps
|
||||
Apps: map[string]caddy.App{
|
||||
"http": httpApp,
|
||||
},
|
||||
}
|
||||
|
||||
// Configure logging if callback is set
|
||||
if cp.requestCallback != nil {
|
||||
// Register the callback for the proxy service ID
|
||||
RegisterCallback("proxy", cp.requestCallback)
|
||||
|
||||
// Build logging config with proper module names
|
||||
cfg.Logging = &caddy.Logging{
|
||||
Logs: map[string]*caddy.CustomLog{
|
||||
"http_access": {
|
||||
BaseLog: caddy.BaseLog{
|
||||
WriterRaw: caddyconfig.JSONModuleObject(&CallbackWriter{ServiceID: "proxy"}, "output", "callback", nil),
|
||||
EncoderRaw: caddyconfig.JSONModuleObject(&logging.JSONEncoder{}, "format", "json", nil),
|
||||
Level: "INFO",
|
||||
},
|
||||
Include: []string{"http.log.access"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
log.Infof("Configured custom logging with callback writer for service: proxy")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// createRoute creates a Caddy route for a path and target with service ID tracking
|
||||
func (cp *CaddyProxy) createRoute(routeConfig *RouteConfig, path, target string) caddyhttp.Route {
|
||||
// Check if this route needs a custom transport
|
||||
hasCustomTransport := routeConfig.Conn != nil || routeConfig.CustomDialer != nil
|
||||
|
||||
if hasCustomTransport {
|
||||
// For routes with custom transports, store them separately
|
||||
// and configure the upstream to use a special dial address that we'll intercept
|
||||
handlerKey := fmt.Sprintf("%s:%s", routeConfig.ID, path)
|
||||
|
||||
// Create upstream with custom dial configuration
|
||||
upstream := &reverseproxy.Upstream{
|
||||
Dial: target,
|
||||
}
|
||||
|
||||
// Create the reverse proxy handler with custom transport
|
||||
handler := &reverseproxy.Handler{
|
||||
Upstreams: reverseproxy.UpstreamPool{upstream},
|
||||
}
|
||||
|
||||
// Configure the custom transport
|
||||
if routeConfig.Conn != nil {
|
||||
// Use the provided connection directly
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
log.Debugf("Reusing existing connection for route %s to %s", routeConfig.ID, address)
|
||||
return routeConfig.Conn, nil
|
||||
},
|
||||
MaxIdleConns: 1,
|
||||
MaxIdleConnsPerHost: 1,
|
||||
IdleConnTimeout: 0,
|
||||
DisableKeepAlives: false,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
handler.Transport = transport
|
||||
log.Infof("Configured net.Conn transport for route %s (path: %s)", routeConfig.ID, path)
|
||||
} else if routeConfig.CustomDialer != nil {
|
||||
// Use the custom dialer function
|
||||
transport := &http.Transport{
|
||||
DialContext: routeConfig.CustomDialer,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
handler.Transport = transport
|
||||
log.Infof("Configured custom dialer transport for route %s (path: %s)", routeConfig.ID, path)
|
||||
}
|
||||
|
||||
// Store the handler for later injection
|
||||
cp.customHandlers[handlerKey] = handler
|
||||
|
||||
// Create route using HandlersRaw with a placeholder that will be replaced
|
||||
// We'll use JSON serialization here, but inject the real handler after Caddy loads
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil),
|
||||
},
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
route.MatcherSetsRaw = []caddy.ModuleMap{
|
||||
{
|
||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
// Standard route without custom transport
|
||||
upstream := &reverseproxy.Upstream{
|
||||
Dial: target,
|
||||
}
|
||||
|
||||
handler := &reverseproxy.Handler{
|
||||
Upstreams: reverseproxy.UpstreamPool{upstream},
|
||||
}
|
||||
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil),
|
||||
},
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
route.MatcherSetsRaw = []caddy.ModuleMap{
|
||||
{
|
||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
// IsRunning returns whether the proxy is running
|
||||
func (cp *CaddyProxy) IsRunning() bool {
|
||||
cp.mu.RLock()
|
||||
defer cp.mu.RUnlock()
|
||||
return cp.isRunning
|
||||
}
|
||||
|
||||
// GetConfig returns the proxy configuration
|
||||
func (cp *CaddyProxy) GetConfig() Config {
|
||||
return cp.config
|
||||
}
|
||||
|
||||
// AddRoute adds a new route configuration to the proxy
|
||||
// If the proxy is running, it will reload the configuration
|
||||
func (cp *CaddyProxy) AddRoute(route *RouteConfig) error {
|
||||
if route == nil {
|
||||
return fmt.Errorf("route cannot be nil")
|
||||
}
|
||||
if route.ID == "" {
|
||||
return fmt.Errorf("route ID is required")
|
||||
}
|
||||
if len(route.PathMappings) == 0 {
|
||||
return fmt.Errorf("route must have at least one path mapping")
|
||||
}
|
||||
|
||||
cp.mu.Lock()
|
||||
// Check if route already exists
|
||||
if _, exists := cp.routes[route.ID]; exists {
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("route with ID %s already exists", route.ID)
|
||||
}
|
||||
|
||||
// Add new route
|
||||
cp.routes[route.ID] = route
|
||||
isRunning := cp.isRunning
|
||||
cp.mu.Unlock()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"route_id": route.ID,
|
||||
"domain": route.Domain,
|
||||
"paths": len(route.PathMappings),
|
||||
}).Info("Added route")
|
||||
|
||||
// Reload configuration if proxy is running
|
||||
if isRunning {
|
||||
if err := cp.reloadConfig(); err != nil {
|
||||
// Rollback: remove the route
|
||||
cp.mu.Lock()
|
||||
delete(cp.routes, route.ID)
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("failed to reload config after adding route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRoute removes a route from the proxy
|
||||
// If the proxy is running, it will reload the configuration
|
||||
func (cp *CaddyProxy) RemoveRoute(routeID string) error {
|
||||
cp.mu.Lock()
|
||||
// Check if route exists
|
||||
route, exists := cp.routes[routeID]
|
||||
if !exists {
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("route %s not found", routeID)
|
||||
}
|
||||
|
||||
// Remove route
|
||||
delete(cp.routes, routeID)
|
||||
isRunning := cp.isRunning
|
||||
cp.mu.Unlock()
|
||||
|
||||
log.Infof("Removed route: %s", routeID)
|
||||
|
||||
// Reload configuration if proxy is running
|
||||
if isRunning {
|
||||
if err := cp.reloadConfig(); err != nil {
|
||||
// Rollback: add the route back
|
||||
cp.mu.Lock()
|
||||
cp.routes[routeID] = route
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("failed to reload config after removing route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRoute updates an existing route configuration
|
||||
// If the proxy is running, it will reload the configuration
|
||||
func (cp *CaddyProxy) UpdateRoute(route *RouteConfig) error {
|
||||
if route == nil {
|
||||
return fmt.Errorf("route cannot be nil")
|
||||
}
|
||||
if route.ID == "" {
|
||||
return fmt.Errorf("route ID is required")
|
||||
}
|
||||
|
||||
cp.mu.Lock()
|
||||
// Check if route exists
|
||||
oldRoute, exists := cp.routes[route.ID]
|
||||
if !exists {
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("route %s not found", route.ID)
|
||||
}
|
||||
|
||||
// Update route
|
||||
cp.routes[route.ID] = route
|
||||
isRunning := cp.isRunning
|
||||
cp.mu.Unlock()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"route_id": route.ID,
|
||||
"domain": route.Domain,
|
||||
"paths": len(route.PathMappings),
|
||||
}).Info("Updated route")
|
||||
|
||||
// Reload configuration if proxy is running
|
||||
if isRunning {
|
||||
if err := cp.reloadConfig(); err != nil {
|
||||
// Rollback: restore old route
|
||||
cp.mu.Lock()
|
||||
cp.routes[route.ID] = oldRoute
|
||||
cp.mu.Unlock()
|
||||
return fmt.Errorf("failed to reload config after updating route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRoutes returns a list of all configured route IDs
|
||||
func (cp *CaddyProxy) ListRoutes() []string {
|
||||
cp.mu.RLock()
|
||||
defer cp.mu.RUnlock()
|
||||
|
||||
routes := make([]string, 0, len(cp.routes))
|
||||
for id := range cp.routes {
|
||||
routes = append(routes, id)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// GetRoute returns a route configuration by ID
|
||||
func (cp *CaddyProxy) GetRoute(routeID string) (*RouteConfig, error) {
|
||||
cp.mu.RLock()
|
||||
defer cp.mu.RUnlock()
|
||||
|
||||
route, exists := cp.routes[routeID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("route %s not found", routeID)
|
||||
}
|
||||
|
||||
return route, nil
|
||||
}
|
||||
|
||||
// injectCustomTransports injects custom transports into provisioned handlers
|
||||
// This must be called after httpApp.Provision() but before passing to Caddy.Run()
|
||||
func (cp *CaddyProxy) injectCustomTransports(httpApp *caddyhttp.App) error {
|
||||
// Iterate through all servers
|
||||
for serverName, server := range httpApp.Servers {
|
||||
log.Debugf("Injecting custom transports for server: %s", serverName)
|
||||
|
||||
// Iterate through all routes
|
||||
for routeIdx, route := range server.Routes {
|
||||
// Iterate through all handlers in the route
|
||||
for handlerIdx, handler := range route.Handlers {
|
||||
// Check if this is a reverse proxy handler
|
||||
if rpHandler, ok := handler.(*reverseproxy.Handler); ok {
|
||||
// Try to find a matching custom handler for this route
|
||||
// We need to match by handler configuration since we don't have route metadata here
|
||||
for handlerKey, customHandler := range cp.customHandlers {
|
||||
// Check if the upstream configuration matches
|
||||
if len(rpHandler.Upstreams) > 0 && len(customHandler.Upstreams) > 0 {
|
||||
if rpHandler.Upstreams[0].Dial == customHandler.Upstreams[0].Dial {
|
||||
// Match found! Inject the custom transport
|
||||
rpHandler.Transport = customHandler.Transport
|
||||
log.Infof("Injected custom transport for route %d, handler %d (key: %s)", routeIdx, handlerIdx, handlerKey)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reloadConfig rebuilds and reloads the Caddy configuration
|
||||
// Must be called without holding the lock
|
||||
func (cp *CaddyProxy) reloadConfig() error {
|
||||
log.Info("Reloading Caddy configuration...")
|
||||
|
||||
cfg, err := cp.buildCaddyConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build config: %w", err)
|
||||
}
|
||||
|
||||
if err := caddy.Run(cfg); err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Caddy configuration reloaded successfully")
|
||||
return nil
|
||||
}
|
||||
225
proxy/internal/reverseproxy/logwriter.go
Normal file
225
proxy/internal/reverseproxy/logwriter.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global map to store callbacks per service ID
|
||||
callbackRegistry = make(map[string]RequestDataCallback)
|
||||
callbackMu sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterCallback registers a callback for a specific service ID
|
||||
func RegisterCallback(serviceID string, callback RequestDataCallback) {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
callbackRegistry[serviceID] = callback
|
||||
}
|
||||
|
||||
// UnregisterCallback removes a callback for a specific service ID
|
||||
func UnregisterCallback(serviceID string) {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
delete(callbackRegistry, serviceID)
|
||||
}
|
||||
|
||||
// getCallback retrieves the callback for a service ID
|
||||
func getCallback(serviceID string) RequestDataCallback {
|
||||
callbackMu.RLock()
|
||||
defer callbackMu.RUnlock()
|
||||
return callbackRegistry[serviceID]
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(CallbackWriter{})
|
||||
}
|
||||
|
||||
// CallbackWriter is a Caddy log writer module that sends request data via callback
|
||||
type CallbackWriter struct {
|
||||
ServiceID string `json:"service_id,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information
|
||||
func (CallbackWriter) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "caddy.logging.writers.callback",
|
||||
New: func() caddy.Module { return new(CallbackWriter) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the callback writer
|
||||
func (cw *CallbackWriter) Provision(ctx caddy.Context) error {
|
||||
log.Infof("CallbackWriter.Provision called for service_id: %s", cw.ServiceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the writer
|
||||
func (cw *CallbackWriter) String() string {
|
||||
return fmt.Sprintf("callback writer for service %s", cw.ServiceID)
|
||||
}
|
||||
|
||||
// WriterKey returns a unique key for this writer configuration
|
||||
func (cw *CallbackWriter) WriterKey() string {
|
||||
return "callback_" + cw.ServiceID
|
||||
}
|
||||
|
||||
// OpenWriter opens the writer
|
||||
func (cw *CallbackWriter) OpenWriter() (io.WriteCloser, error) {
|
||||
log.Infof("CallbackWriter.OpenWriter called for service_id: %s", cw.ServiceID)
|
||||
writer := &LogWriter{
|
||||
serviceID: cw.ServiceID,
|
||||
}
|
||||
log.Infof("Created LogWriter instance: %p for service_id: %s", writer, cw.ServiceID)
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler
|
||||
func (cw *CallbackWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
cw.ServiceID = d.Val()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure CallbackWriter implements the required interfaces
|
||||
var (
|
||||
_ caddy.Provisioner = (*CallbackWriter)(nil)
|
||||
_ caddy.WriterOpener = (*CallbackWriter)(nil)
|
||||
_ caddyfile.Unmarshaler = (*CallbackWriter)(nil)
|
||||
)
|
||||
|
||||
// LogWriter is a custom io.Writer that parses Caddy's structured JSON logs
|
||||
// and extracts request metrics to send via callback
|
||||
type LogWriter struct {
|
||||
serviceID string
|
||||
}
|
||||
|
||||
// NewLogWriter creates a new log writer with the given service ID
|
||||
func NewLogWriter(serviceID string) *LogWriter {
|
||||
return &LogWriter{
|
||||
serviceID: serviceID,
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer
|
||||
func (lw *LogWriter) Write(p []byte) (n int, err error) {
|
||||
// DEBUG: Log that we received data
|
||||
log.Infof("LogWriter.Write called with %d bytes for service_id: %s", len(p), lw.serviceID)
|
||||
log.Debugf("LogWriter content: %s", string(p))
|
||||
|
||||
// Caddy writes one JSON object per line
|
||||
// Parse the JSON to extract request metrics
|
||||
var logEntry map[string]interface{}
|
||||
if err := json.Unmarshal(p, &logEntry); err != nil {
|
||||
// Not JSON or malformed, skip
|
||||
log.Debugf("Failed to unmarshal JSON: %v", err)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Caddy access logs have a nested "request" object
|
||||
// Check if this is an access log entry by looking for "request" field
|
||||
requestObj, hasRequest := logEntry["request"]
|
||||
if !hasRequest {
|
||||
log.Debugf("Not an access log entry (no 'request' field)")
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
request, ok := requestObj.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Debugf("'request' field is not a map")
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
data := &RequestData{
|
||||
ServiceID: lw.serviceID,
|
||||
}
|
||||
|
||||
// Extract method from request object
|
||||
if method, ok := request["method"].(string); ok {
|
||||
data.Method = method
|
||||
}
|
||||
|
||||
// Extract host from request object and strip port
|
||||
if host, ok := request["host"].(string); ok {
|
||||
// Strip port from host (e.g., "test.netbird.io:54321" -> "test.netbird.io")
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
||||
data.Host = host[:idx]
|
||||
} else {
|
||||
data.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
// Extract path (uri field) from request object
|
||||
if uri, ok := request["uri"].(string); ok {
|
||||
data.Path = uri
|
||||
}
|
||||
|
||||
// Extract status code from top-level
|
||||
if status, ok := logEntry["status"].(float64); ok {
|
||||
data.ResponseCode = int32(status)
|
||||
}
|
||||
|
||||
// Extract duration (in seconds, convert to milliseconds) from top-level
|
||||
if duration, ok := logEntry["duration"].(float64); ok {
|
||||
data.DurationMs = int64(duration * 1000)
|
||||
}
|
||||
|
||||
// Extract source IP from request object - try multiple fields
|
||||
if clientIP, ok := request["client_ip"].(string); ok {
|
||||
data.SourceIP = clientIP
|
||||
} else if remoteIP, ok := request["remote_ip"].(string); ok {
|
||||
data.SourceIP = remoteIP
|
||||
} else if remoteAddr, ok := request["remote_addr"].(string); ok {
|
||||
// remote_addr is in "IP:port" format
|
||||
if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 {
|
||||
data.SourceIP = remoteAddr[:idx]
|
||||
} else {
|
||||
data.SourceIP = remoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
// Call callback if set and we have valid data
|
||||
callback := getCallback(lw.serviceID)
|
||||
if callback != nil && data.Method != "" {
|
||||
log.Infof("Calling callback for request: %s %s", data.Method, data.Path)
|
||||
go func() {
|
||||
// Run in goroutine to avoid blocking log writes
|
||||
callback(data)
|
||||
}()
|
||||
} else {
|
||||
log.Warnf("No callback registered for service_id: %s", lw.serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": data.ServiceID,
|
||||
"method": data.Method,
|
||||
"host": data.Host,
|
||||
"path": data.Path,
|
||||
"status": data.ResponseCode,
|
||||
"duration_ms": data.DurationMs,
|
||||
"source_ip": data.SourceIP,
|
||||
}).Info("Request logged via callback writer")
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Close implements io.Closer (no-op for our use case)
|
||||
func (lw *LogWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure LogWriter implements io.WriteCloser
|
||||
var _ io.WriteCloser = (*LogWriter)(nil)
|
||||
251
proxy/internal/reverseproxy/logwriter_test.go
Normal file
251
proxy/internal/reverseproxy/logwriter_test.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLogWriter_Write(t *testing.T) {
|
||||
// Create a channel to receive callback data
|
||||
callbackChan := make(chan *RequestData, 1)
|
||||
var callbackMu sync.Mutex
|
||||
var callbackCalled bool
|
||||
|
||||
// Register a test callback
|
||||
testServiceID := "test-service"
|
||||
RegisterCallback(testServiceID, func(data *RequestData) {
|
||||
callbackMu.Lock()
|
||||
callbackCalled = true
|
||||
callbackMu.Unlock()
|
||||
callbackChan <- data
|
||||
})
|
||||
defer UnregisterCallback(testServiceID)
|
||||
|
||||
// Create a log writer
|
||||
writer := NewLogWriter(testServiceID)
|
||||
|
||||
// Create a sample Caddy access log entry (matching the structure from your logs)
|
||||
logEntry := map[string]interface{}{
|
||||
"level": "info",
|
||||
"ts": 1768352053.7900746,
|
||||
"logger": "http.log.access",
|
||||
"msg": "handled request",
|
||||
"request": map[string]interface{}{
|
||||
"remote_ip": "::1",
|
||||
"remote_port": "51972",
|
||||
"client_ip": "::1",
|
||||
"proto": "HTTP/1.1",
|
||||
"method": "GET",
|
||||
"host": "test.netbird.io:54321",
|
||||
"uri": "/test/path",
|
||||
},
|
||||
"bytes_read": 0,
|
||||
"user_id": "",
|
||||
"duration": 0.004779453,
|
||||
"size": 615,
|
||||
"status": 200,
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
logJSON, err := json.Marshal(logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal log entry: %v", err)
|
||||
}
|
||||
|
||||
// Write to the log writer
|
||||
n, err := writer.Write(logJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Write failed: %v", err)
|
||||
}
|
||||
|
||||
if n != len(logJSON) {
|
||||
t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n)
|
||||
}
|
||||
|
||||
// Wait for callback to be called (with timeout)
|
||||
select {
|
||||
case data := <-callbackChan:
|
||||
// Verify the extracted data
|
||||
if data.ServiceID != testServiceID {
|
||||
t.Errorf("Expected service_id %s, got %s", testServiceID, data.ServiceID)
|
||||
}
|
||||
if data.Method != "GET" {
|
||||
t.Errorf("Expected method GET, got %s", data.Method)
|
||||
}
|
||||
if data.Host != "test.netbird.io" {
|
||||
t.Errorf("Expected host test.netbird.io, got %s", data.Host)
|
||||
}
|
||||
if data.Path != "/test/path" {
|
||||
t.Errorf("Expected path /test/path, got %s", data.Path)
|
||||
}
|
||||
if data.ResponseCode != 200 {
|
||||
t.Errorf("Expected status 200, got %d", data.ResponseCode)
|
||||
}
|
||||
if data.SourceIP != "::1" {
|
||||
t.Errorf("Expected source_ip ::1, got %s", data.SourceIP)
|
||||
}
|
||||
// Duration should be ~4.78ms (0.004779453 * 1000)
|
||||
if data.DurationMs < 4 || data.DurationMs > 5 {
|
||||
t.Errorf("Expected duration ~4-5ms, got %dms", data.DurationMs)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("Callback was not called within timeout")
|
||||
}
|
||||
|
||||
// Verify callback was called
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
if !callbackCalled {
|
||||
t.Error("Callback was never called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriter_Write_NonAccessLog(t *testing.T) {
|
||||
// Create a channel to receive callback data
|
||||
callbackChan := make(chan *RequestData, 1)
|
||||
|
||||
// Register a test callback
|
||||
testServiceID := "test-service-2"
|
||||
RegisterCallback(testServiceID, func(data *RequestData) {
|
||||
callbackChan <- data
|
||||
})
|
||||
defer UnregisterCallback(testServiceID)
|
||||
|
||||
// Create a log writer
|
||||
writer := NewLogWriter(testServiceID)
|
||||
|
||||
// Create a non-access log entry (e.g., a TLS log)
|
||||
logEntry := map[string]interface{}{
|
||||
"level": "info",
|
||||
"ts": 1768352032.12347,
|
||||
"logger": "tls",
|
||||
"msg": "storage cleaning happened too recently",
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
logJSON, err := json.Marshal(logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal log entry: %v", err)
|
||||
}
|
||||
|
||||
// Write to the log writer
|
||||
n, err := writer.Write(logJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Write failed: %v", err)
|
||||
}
|
||||
|
||||
if n != len(logJSON) {
|
||||
t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n)
|
||||
}
|
||||
|
||||
// Callback should NOT be called for non-access logs
|
||||
select {
|
||||
case data := <-callbackChan:
|
||||
t.Errorf("Callback should not be called for non-access log, but got: %+v", data)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Expected - callback not called
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriter_Write_MalformedJSON(t *testing.T) {
|
||||
// Create a log writer
|
||||
writer := NewLogWriter("test-service-3")
|
||||
|
||||
// Write malformed JSON
|
||||
malformedJSON := []byte("{this is not valid json")
|
||||
|
||||
// Should not fail, just skip the entry
|
||||
n, err := writer.Write(malformedJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Write should not fail on malformed JSON: %v", err)
|
||||
}
|
||||
|
||||
if n != len(malformedJSON) {
|
||||
t.Errorf("Expected to write %d bytes, wrote %d", len(malformedJSON), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackRegistry(t *testing.T) {
|
||||
serviceID := "test-registry"
|
||||
var called bool
|
||||
|
||||
// Test registering a callback
|
||||
callback := func(data *RequestData) {
|
||||
called = true
|
||||
}
|
||||
RegisterCallback(serviceID, callback)
|
||||
|
||||
// Test retrieving the callback
|
||||
retrievedCallback := getCallback(serviceID)
|
||||
if retrievedCallback == nil {
|
||||
t.Fatal("Expected to retrieve callback, got nil")
|
||||
}
|
||||
|
||||
// Call the retrieved callback to verify it works
|
||||
retrievedCallback(&RequestData{})
|
||||
if !called {
|
||||
t.Error("Callback was not called")
|
||||
}
|
||||
|
||||
// Test unregistering
|
||||
UnregisterCallback(serviceID)
|
||||
retrievedCallback = getCallback(serviceID)
|
||||
if retrievedCallback != nil {
|
||||
t.Error("Expected nil after unregistering, got a callback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackWriter_Module(t *testing.T) {
|
||||
// Test that the module is properly configured
|
||||
cw := CallbackWriter{ServiceID: "test"}
|
||||
|
||||
moduleInfo := cw.CaddyModule()
|
||||
if moduleInfo.ID != "caddy.logging.writers.callback" {
|
||||
t.Errorf("Expected module ID 'caddy.logging.writers.callback', got '%s'", moduleInfo.ID)
|
||||
}
|
||||
|
||||
if moduleInfo.New == nil {
|
||||
t.Error("Expected New function to be set")
|
||||
}
|
||||
|
||||
// Test creating a new instance via the New function
|
||||
newModule := moduleInfo.New()
|
||||
if newModule == nil {
|
||||
t.Error("Expected New() to return a module instance")
|
||||
}
|
||||
|
||||
_, ok := newModule.(*CallbackWriter)
|
||||
if !ok {
|
||||
t.Error("Expected New() to return a *CallbackWriter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackWriter_WriterKey(t *testing.T) {
|
||||
cw := &CallbackWriter{ServiceID: "my-service"}
|
||||
|
||||
expectedKey := "callback_my-service"
|
||||
if cw.WriterKey() != expectedKey {
|
||||
t.Errorf("Expected writer key '%s', got '%s'", expectedKey, cw.WriterKey())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackWriter_String(t *testing.T) {
|
||||
cw := &CallbackWriter{ServiceID: "my-service"}
|
||||
|
||||
str := cw.String()
|
||||
if str != "callback writer for service my-service" {
|
||||
t.Errorf("Unexpected string representation: %s", str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriter_Close(t *testing.T) {
|
||||
writer := NewLogWriter("test")
|
||||
|
||||
// Close should not fail
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close should not fail: %v", err)
|
||||
}
|
||||
}
|
||||
131
proxy/internal/reverseproxy/middleware.go
Normal file
131
proxy/internal/reverseproxy/middleware.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RequestDataCallback is called for each request that passes through the proxy
|
||||
type RequestDataCallback func(data *RequestData)
|
||||
|
||||
// RequestData contains metadata about a proxied request
|
||||
type RequestData struct {
|
||||
ServiceID string
|
||||
Host string
|
||||
Path string
|
||||
DurationMs int64
|
||||
Method string
|
||||
ResponseCode int32
|
||||
SourceIP string
|
||||
}
|
||||
|
||||
// MetricsMiddleware wraps a handler to capture request metrics
|
||||
type MetricsMiddleware struct {
|
||||
Next caddyhttp.Handler
|
||||
ServiceID string
|
||||
Callback RequestDataCallback
|
||||
}
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler
|
||||
func (m *MetricsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
// Record start time
|
||||
startTime := time.Now()
|
||||
|
||||
// Wrap the response writer to capture status code
|
||||
wrappedWriter := &responseWriterWrapper{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK, // Default to 200
|
||||
}
|
||||
|
||||
// Call the next handler (Caddy's reverse proxy)
|
||||
err := next.ServeHTTP(wrappedWriter, r)
|
||||
|
||||
// Calculate duration
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Extract source IP (handle X-Forwarded-For or direct connection)
|
||||
sourceIP := extractSourceIP(r)
|
||||
|
||||
// Create request data
|
||||
data := &RequestData{
|
||||
ServiceID: m.ServiceID,
|
||||
Path: r.URL.Path,
|
||||
DurationMs: duration.Milliseconds(),
|
||||
Method: r.Method,
|
||||
ResponseCode: int32(wrappedWriter.statusCode),
|
||||
SourceIP: sourceIP,
|
||||
}
|
||||
|
||||
// Call callback if set
|
||||
if m.Callback != nil {
|
||||
go func() {
|
||||
// Run callback in goroutine to avoid blocking response
|
||||
m.Callback(data)
|
||||
}()
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": data.ServiceID,
|
||||
"method": data.Method,
|
||||
"path": data.Path,
|
||||
"status": data.ResponseCode,
|
||||
"duration_ms": data.DurationMs,
|
||||
"source_ip": data.SourceIP,
|
||||
}).Debug("Request proxied")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// responseWriterWrapper wraps http.ResponseWriter to capture status code
|
||||
type responseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
written bool
|
||||
}
|
||||
|
||||
// WriteHeader captures the status code
|
||||
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
|
||||
if !w.written {
|
||||
w.statusCode = statusCode
|
||||
w.written = true
|
||||
}
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// Write ensures we capture status if WriteHeader wasn't called explicitly
|
||||
func (w *responseWriterWrapper) Write(b []byte) (int, error) {
|
||||
if !w.written {
|
||||
w.written = true
|
||||
// Status code defaults to 200 if not explicitly set
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// extractSourceIP extracts the real client IP from the request
|
||||
func extractSourceIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header first (if behind a proxy)
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// X-Forwarded-For can be a comma-separated list, take the first one
|
||||
parts := strings.Split(xff, ",")
|
||||
if len(parts) > 0 {
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
// RemoteAddr is in format "IP:port", so we need to strip the port
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
|
||||
return r.RemoteAddr[:idx]
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
||||
139
proxy/internal/reverseproxy/transport.go
Normal file
139
proxy/internal/reverseproxy/transport.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// customTransportRegistry stores custom dialers and connections globally
|
||||
// This allows them to be accessed after Caddy deserializes the configuration from JSON
|
||||
var customTransportRegistry = &transportRegistry{
|
||||
transports: make(map[string]*customTransport),
|
||||
}
|
||||
|
||||
// transportRegistry manages custom transports for routes
|
||||
type transportRegistry struct {
|
||||
mu sync.RWMutex
|
||||
transports map[string]*customTransport // key is "routeID:path"
|
||||
}
|
||||
|
||||
// customTransport wraps either a net.Conn or a custom dialer
|
||||
type customTransport struct {
|
||||
routeID string
|
||||
path string
|
||||
conn net.Conn
|
||||
customDialer func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
defaultDialer *net.Dialer
|
||||
}
|
||||
|
||||
// Register registers a custom transport for a route
|
||||
func (r *transportRegistry) Register(routeID, path string, conn net.Conn, dialer func(ctx context.Context, network, address string) (net.Conn, error)) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s:%s", routeID, path)
|
||||
r.transports[key] = &customTransport{
|
||||
routeID: routeID,
|
||||
path: path,
|
||||
conn: conn,
|
||||
customDialer: dialer,
|
||||
defaultDialer: &net.Dialer{Timeout: 30 * time.Second},
|
||||
}
|
||||
|
||||
if conn != nil {
|
||||
log.Infof("Registered net.Conn transport for route %s (path: %s)", routeID, path)
|
||||
} else if dialer != nil {
|
||||
log.Infof("Registered custom dialer transport for route %s (path: %s)", routeID, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a custom transport for a route
|
||||
func (r *transportRegistry) Get(routeID, path string) *customTransport {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
key := fmt.Sprintf("%s:%s", routeID, path)
|
||||
return r.transports[key]
|
||||
}
|
||||
|
||||
// Unregister removes a custom transport
|
||||
func (r *transportRegistry) Unregister(routeID, path string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s:%s", routeID, path)
|
||||
delete(r.transports, key)
|
||||
log.Infof("Unregistered transport for route %s (path: %s)", routeID, path)
|
||||
}
|
||||
|
||||
// Clear removes all custom transports
|
||||
func (r *transportRegistry) Clear() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.transports = make(map[string]*customTransport)
|
||||
log.Info("Cleared all custom transports")
|
||||
}
|
||||
|
||||
// DialContext implements the DialContext function for custom transports
|
||||
func (ct *customTransport) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
// If we have a pre-existing connection, return it
|
||||
if ct.conn != nil {
|
||||
log.Debugf("Reusing existing connection for route %s (path: %s) to %s", ct.routeID, ct.path, address)
|
||||
return ct.conn, nil
|
||||
}
|
||||
|
||||
// If we have a custom dialer, use it
|
||||
if ct.customDialer != nil {
|
||||
log.Debugf("Using custom dialer for route %s (path: %s) to %s", ct.routeID, ct.path, address)
|
||||
return ct.customDialer(ctx, network, address)
|
||||
}
|
||||
|
||||
// Fallback to default dialer (this shouldn't happen if registered correctly)
|
||||
log.Warnf("No custom transport found for route %s (path: %s), using default dialer", ct.routeID, ct.path)
|
||||
return ct.defaultDialer.DialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
// NewCustomHTTPTransport creates an HTTP transport that uses the custom dialer
|
||||
func NewCustomHTTPTransport(routeID, path string) *http.Transport {
|
||||
transport := customTransportRegistry.Get(routeID, path)
|
||||
if transport == nil {
|
||||
// No custom transport registered, return standard transport
|
||||
log.Warnf("No custom transport found for route %s (path: %s), using standard transport", routeID, path)
|
||||
return &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure transport based on whether we're using a connection or dialer
|
||||
if transport.conn != nil {
|
||||
// Using a pre-existing connection - disable pooling
|
||||
return &http.Transport{
|
||||
DialContext: transport.DialContext,
|
||||
MaxIdleConns: 1,
|
||||
MaxIdleConnsPerHost: 1,
|
||||
IdleConnTimeout: 0, // Keep alive indefinitely
|
||||
DisableKeepAlives: false,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Using a custom dialer - use normal pooling
|
||||
return &http.Transport{
|
||||
DialContext: transport.DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
}
|
||||
13
proxy/main.go
Normal file
13
proxy/main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
73
proxy/pkg/errors/common.go
Normal file
73
proxy/pkg/errors/common.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Configuration errors
|
||||
|
||||
func NewConfigInvalid(message string) *AppError {
|
||||
return New(CodeConfigInvalid, message)
|
||||
}
|
||||
|
||||
func NewConfigNotFound(path string) *AppError {
|
||||
return New(CodeConfigNotFound, fmt.Sprintf("configuration file not found: %s", path))
|
||||
}
|
||||
|
||||
func WrapConfigParseFailed(err error, path string) *AppError {
|
||||
return Wrap(CodeConfigParseFailed, fmt.Sprintf("failed to parse configuration file: %s", path), err)
|
||||
}
|
||||
|
||||
// Server errors
|
||||
|
||||
func NewServerStartFailed(err error, reason string) *AppError {
|
||||
return Wrap(CodeServerStartFailed, fmt.Sprintf("server start failed: %s", reason), err)
|
||||
}
|
||||
|
||||
func NewServerStopFailed(err error) *AppError {
|
||||
return Wrap(CodeServerStopFailed, "server shutdown failed", err)
|
||||
}
|
||||
|
||||
func NewServerAlreadyRunning() *AppError {
|
||||
return New(CodeServerAlreadyRunning, "server is already running")
|
||||
}
|
||||
|
||||
func NewServerNotRunning() *AppError {
|
||||
return New(CodeServerNotRunning, "server is not running")
|
||||
}
|
||||
|
||||
// Proxy errors
|
||||
|
||||
func NewProxyBackendUnavailable(backend string, err error) *AppError {
|
||||
return Wrap(CodeProxyBackendUnavailable, fmt.Sprintf("backend unavailable: %s", backend), err)
|
||||
}
|
||||
|
||||
func NewProxyTimeout(backend string) *AppError {
|
||||
return New(CodeProxyTimeout, fmt.Sprintf("request to backend timed out: %s", backend))
|
||||
}
|
||||
|
||||
func NewProxyInvalidTarget(target string, err error) *AppError {
|
||||
return Wrap(CodeProxyInvalidTarget, fmt.Sprintf("invalid proxy target: %s", target), err)
|
||||
}
|
||||
|
||||
// Network errors
|
||||
|
||||
func NewNetworkTimeout(operation string) *AppError {
|
||||
return New(CodeNetworkTimeout, fmt.Sprintf("network timeout: %s", operation))
|
||||
}
|
||||
|
||||
func NewNetworkUnreachable(host string) *AppError {
|
||||
return New(CodeNetworkUnreachable, fmt.Sprintf("network unreachable: %s", host))
|
||||
}
|
||||
|
||||
func NewNetworkRefused(host string) *AppError {
|
||||
return New(CodeNetworkRefused, fmt.Sprintf("connection refused: %s", host))
|
||||
}
|
||||
|
||||
// Internal errors
|
||||
|
||||
func NewInternalError(message string) *AppError {
|
||||
return New(CodeInternalError, message)
|
||||
}
|
||||
|
||||
func WrapInternalError(err error, message string) *AppError {
|
||||
return Wrap(CodeInternalError, message, err)
|
||||
}
|
||||
138
proxy/pkg/errors/errors.go
Normal file
138
proxy/pkg/errors/errors.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error codes for categorizing errors
|
||||
type Code string
|
||||
|
||||
const (
|
||||
// Configuration errors
|
||||
CodeConfigInvalid Code = "CONFIG_INVALID"
|
||||
CodeConfigNotFound Code = "CONFIG_NOT_FOUND"
|
||||
CodeConfigParseFailed Code = "CONFIG_PARSE_FAILED"
|
||||
|
||||
// Server errors
|
||||
CodeServerStartFailed Code = "SERVER_START_FAILED"
|
||||
CodeServerStopFailed Code = "SERVER_STOP_FAILED"
|
||||
CodeServerAlreadyRunning Code = "SERVER_ALREADY_RUNNING"
|
||||
CodeServerNotRunning Code = "SERVER_NOT_RUNNING"
|
||||
|
||||
// Proxy errors
|
||||
CodeProxyBackendUnavailable Code = "PROXY_BACKEND_UNAVAILABLE"
|
||||
CodeProxyTimeout Code = "PROXY_TIMEOUT"
|
||||
CodeProxyInvalidTarget Code = "PROXY_INVALID_TARGET"
|
||||
|
||||
// Network errors
|
||||
CodeNetworkTimeout Code = "NETWORK_TIMEOUT"
|
||||
CodeNetworkUnreachable Code = "NETWORK_UNREACHABLE"
|
||||
CodeNetworkRefused Code = "NETWORK_REFUSED"
|
||||
|
||||
// Internal errors
|
||||
CodeInternalError Code = "INTERNAL_ERROR"
|
||||
CodeUnknownError Code = "UNKNOWN_ERROR"
|
||||
)
|
||||
|
||||
// AppError represents a structured application error
|
||||
type AppError struct {
|
||||
Code Code // Error code for categorization
|
||||
Message string // Human-readable error message
|
||||
Cause error // Underlying error (if any)
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *AppError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error (for errors.Is and errors.As)
|
||||
func (e *AppError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// Is checks if the error matches the target
|
||||
func (e *AppError) Is(target error) bool {
|
||||
t, ok := target.(*AppError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return e.Code == t.Code
|
||||
}
|
||||
|
||||
// New creates a new AppError
|
||||
func New(code Code, message string) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap wraps an existing error with additional context
|
||||
func Wrap(code Code, message string, cause error) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf wraps an error with a formatted message
|
||||
func Wrapf(code Code, cause error, format string, args ...interface{}) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCode extracts the error code from an error
|
||||
func GetCode(err error) Code {
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr.Code
|
||||
}
|
||||
return CodeUnknownError
|
||||
}
|
||||
|
||||
// HasCode checks if an error has a specific code
|
||||
func HasCode(err error, code Code) bool {
|
||||
return GetCode(err) == code
|
||||
}
|
||||
|
||||
// IsConfigError checks if an error is configuration-related
|
||||
func IsConfigError(err error) bool {
|
||||
code := GetCode(err)
|
||||
return code == CodeConfigInvalid ||
|
||||
code == CodeConfigNotFound ||
|
||||
code == CodeConfigParseFailed
|
||||
}
|
||||
|
||||
// IsServerError checks if an error is server-related
|
||||
func IsServerError(err error) bool {
|
||||
code := GetCode(err)
|
||||
return code == CodeServerStartFailed ||
|
||||
code == CodeServerStopFailed ||
|
||||
code == CodeServerAlreadyRunning ||
|
||||
code == CodeServerNotRunning
|
||||
}
|
||||
|
||||
// IsProxyError checks if an error is proxy-related
|
||||
func IsProxyError(err error) bool {
|
||||
code := GetCode(err)
|
||||
return code == CodeProxyBackendUnavailable ||
|
||||
code == CodeProxyTimeout ||
|
||||
code == CodeProxyInvalidTarget
|
||||
}
|
||||
|
||||
// IsNetworkError checks if an error is network-related
|
||||
func IsNetworkError(err error) bool {
|
||||
code := GetCode(err)
|
||||
return code == CodeNetworkTimeout ||
|
||||
code == CodeNetworkUnreachable ||
|
||||
code == CodeNetworkRefused
|
||||
}
|
||||
160
proxy/pkg/errors/errors_test.go
Normal file
160
proxy/pkg/errors/errors_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppError_Error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *AppError
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "error without cause",
|
||||
err: New(CodeConfigInvalid, "invalid configuration"),
|
||||
expected: "[CONFIG_INVALID] invalid configuration",
|
||||
},
|
||||
{
|
||||
name: "error with cause",
|
||||
err: Wrap(CodeServerStartFailed, "failed to bind port", errors.New("address already in use")),
|
||||
expected: "[SERVER_START_FAILED] failed to bind port: address already in use",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.expected {
|
||||
t.Errorf("Error() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected Code
|
||||
}{
|
||||
{
|
||||
name: "app error",
|
||||
err: New(CodeConfigInvalid, "test"),
|
||||
expected: CodeConfigInvalid,
|
||||
},
|
||||
{
|
||||
name: "wrapped app error",
|
||||
err: Wrap(CodeServerStartFailed, "test", errors.New("cause")),
|
||||
expected: CodeServerStartFailed,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: errors.New("standard error"),
|
||||
expected: CodeUnknownError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetCode(tt.err); got != tt.expected {
|
||||
t.Errorf("GetCode() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasCode(t *testing.T) {
|
||||
err := New(CodeConfigInvalid, "invalid config")
|
||||
|
||||
if !HasCode(err, CodeConfigInvalid) {
|
||||
t.Error("HasCode() should return true for matching code")
|
||||
}
|
||||
|
||||
if HasCode(err, CodeServerStartFailed) {
|
||||
t.Error("HasCode() should return false for non-matching code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConfigError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "config invalid error",
|
||||
err: New(CodeConfigInvalid, "test"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "config not found error",
|
||||
err: New(CodeConfigNotFound, "test"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
err: New(CodeServerStartFailed, "test"),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsConfigError(tt.err); got != tt.expected {
|
||||
t.Errorf("IsConfigError() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorUnwrap(t *testing.T) {
|
||||
cause := errors.New("root cause")
|
||||
err := Wrap(CodeInternalError, "wrapped error", cause)
|
||||
|
||||
unwrapped := errors.Unwrap(err)
|
||||
if unwrapped != cause {
|
||||
t.Errorf("Unwrap() = %v, want %v", unwrapped, cause)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
err1 := New(CodeConfigInvalid, "test1")
|
||||
err2 := New(CodeConfigInvalid, "test2")
|
||||
err3 := New(CodeServerStartFailed, "test3")
|
||||
|
||||
if !errors.Is(err1, err2) {
|
||||
t.Error("errors.Is() should return true for same error code")
|
||||
}
|
||||
|
||||
if errors.Is(err1, err3) {
|
||||
t.Error("errors.Is() should return false for different error codes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonConstructors(t *testing.T) {
|
||||
t.Run("NewConfigNotFound", func(t *testing.T) {
|
||||
err := NewConfigNotFound("/path/to/config")
|
||||
if GetCode(err) != CodeConfigNotFound {
|
||||
t.Error("NewConfigNotFound should create CONFIG_NOT_FOUND error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewServerAlreadyRunning", func(t *testing.T) {
|
||||
err := NewServerAlreadyRunning()
|
||||
if GetCode(err) != CodeServerAlreadyRunning {
|
||||
t.Error("NewServerAlreadyRunning should create SERVER_ALREADY_RUNNING error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewProxyBackendUnavailable", func(t *testing.T) {
|
||||
cause := errors.New("connection refused")
|
||||
err := NewProxyBackendUnavailable("http://backend", cause)
|
||||
if GetCode(err) != CodeProxyBackendUnavailable {
|
||||
t.Error("NewProxyBackendUnavailable should create PROXY_BACKEND_UNAVAILABLE error")
|
||||
}
|
||||
if !errors.Is(err.Unwrap(), cause) {
|
||||
t.Error("NewProxyBackendUnavailable should wrap the cause")
|
||||
}
|
||||
})
|
||||
}
|
||||
1796
proxy/pkg/grpc/proto/proxy.pb.go
Normal file
1796
proxy/pkg/grpc/proto/proxy.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
159
proxy/pkg/grpc/proto/proxy.proto
Normal file
159
proxy/pkg/grpc/proto/proxy.proto
Normal file
@@ -0,0 +1,159 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package proxy;
|
||||
|
||||
option go_package = "github.com/netbirdio/netbird/proxy/pkg/grpc/proto";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// ProxyService defines the bidirectional streaming service
|
||||
// The proxy runs this service, control service connects as client
|
||||
service ProxyService {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
rpc Stream(stream ControlMessage) returns (stream ProxyMessage);
|
||||
}
|
||||
|
||||
// ProxyMessage represents messages sent from proxy to control service
|
||||
message ProxyMessage {
|
||||
oneof message {
|
||||
ProxyStats stats = 1;
|
||||
ProxyEvent event = 2;
|
||||
ProxyLog log = 3;
|
||||
ProxyHeartbeat heartbeat = 4;
|
||||
ProxyRequestData request_data = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ControlMessage represents messages sent from control service to proxy
|
||||
message ControlMessage {
|
||||
oneof message {
|
||||
ControlEvent event = 1;
|
||||
ControlCommand command = 2;
|
||||
ControlConfig config = 3;
|
||||
ExposedServiceEvent exposed_service = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyStats contains proxy statistics
|
||||
message ProxyStats {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
uint64 total_requests = 2;
|
||||
uint64 active_connections = 3;
|
||||
uint64 bytes_sent = 4;
|
||||
uint64 bytes_received = 5;
|
||||
double cpu_usage = 6;
|
||||
double memory_usage_mb = 7;
|
||||
map<string, uint64> status_code_counts = 8;
|
||||
}
|
||||
|
||||
// ProxyEvent represents events from the proxy
|
||||
message ProxyEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
EventType type = 2;
|
||||
string message = 3;
|
||||
map<string, string> metadata = 4;
|
||||
|
||||
enum EventType {
|
||||
UNKNOWN = 0;
|
||||
STARTED = 1;
|
||||
STOPPED = 2;
|
||||
ERROR = 3;
|
||||
BACKEND_UNAVAILABLE = 4;
|
||||
BACKEND_RECOVERED = 5;
|
||||
CONFIG_UPDATED = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyLog represents log entries
|
||||
message ProxyLog {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
LogLevel level = 2;
|
||||
string message = 3;
|
||||
map<string, string> fields = 4;
|
||||
|
||||
enum LogLevel {
|
||||
DEBUG = 0;
|
||||
INFO = 1;
|
||||
WARN = 2;
|
||||
ERROR = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyHeartbeat is sent periodically to keep connection alive
|
||||
message ProxyHeartbeat {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string proxy_id = 2;
|
||||
}
|
||||
|
||||
// ControlEvent represents events from control service
|
||||
message ControlEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string event_id = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
// ControlCommand represents commands sent to proxy
|
||||
message ControlCommand {
|
||||
string command_id = 1;
|
||||
CommandType type = 2;
|
||||
map<string, string> parameters = 3;
|
||||
|
||||
enum CommandType {
|
||||
UNKNOWN = 0;
|
||||
RELOAD_CONFIG = 1;
|
||||
ENABLE_DEBUG = 2;
|
||||
DISABLE_DEBUG = 3;
|
||||
GET_STATS = 4;
|
||||
SHUTDOWN = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ControlConfig contains configuration updates from control service
|
||||
message ControlConfig {
|
||||
string config_version = 1;
|
||||
map<string, string> settings = 2;
|
||||
}
|
||||
|
||||
// ExposedServiceEvent represents exposed service lifecycle events
|
||||
message ExposedServiceEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
EventType type = 2;
|
||||
string service_id = 3;
|
||||
PeerConfig peer_config = 4;
|
||||
UpstreamConfig upstream_config = 5;
|
||||
|
||||
enum EventType {
|
||||
UNKNOWN = 0;
|
||||
CREATED = 1;
|
||||
UPDATED = 2;
|
||||
REMOVED = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// PeerConfig contains WireGuard peer configuration
|
||||
message PeerConfig {
|
||||
string peer_id = 1;
|
||||
string public_key = 2;
|
||||
repeated string allowed_ips = 3;
|
||||
string endpoint = 4;
|
||||
string tunnel_ip = 5;
|
||||
uint32 persistent_keepalive = 6;
|
||||
}
|
||||
|
||||
// UpstreamConfig contains reverse proxy upstream configuration
|
||||
message UpstreamConfig {
|
||||
string domain = 1;
|
||||
map<string, string> path_mappings = 2; // path -> port
|
||||
}
|
||||
|
||||
// ProxyRequestData contains metadata about requests routed through the reverse proxy
|
||||
message ProxyRequestData {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string service_id = 2;
|
||||
string path = 3;
|
||||
int64 duration_ms = 4;
|
||||
string method = 5; // HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
int32 response_code = 6;
|
||||
string source_ip = 7;
|
||||
}
|
||||
137
proxy/pkg/grpc/proto/proxy_grpc.pb.go
Normal file
137
proxy/pkg/grpc/proto/proxy_grpc.pb.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// ProxyServiceClient is the client API for ProxyService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ProxyServiceClient interface {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error)
|
||||
}
|
||||
|
||||
type proxyServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClient {
|
||||
return &proxyServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *proxyServiceClient) Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &ProxyService_ServiceDesc.Streams[0], "/proxy.ProxyService/Stream", opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &proxyServiceStreamClient{stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
type ProxyService_StreamClient interface {
|
||||
Send(*ControlMessage) error
|
||||
Recv() (*ProxyMessage, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type proxyServiceStreamClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamClient) Send(m *ControlMessage) error {
|
||||
return x.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamClient) Recv() (*ProxyMessage, error) {
|
||||
m := new(ProxyMessage)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ProxyServiceServer is the server API for ProxyService service.
|
||||
// All implementations must embed UnimplementedProxyServiceServer
|
||||
// for forward compatibility
|
||||
type ProxyServiceServer interface {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
Stream(ProxyService_StreamServer) error
|
||||
mustEmbedUnimplementedProxyServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedProxyServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedProxyServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedProxyServiceServer) Stream(ProxyService_StreamServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Stream not implemented")
|
||||
}
|
||||
func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}
|
||||
|
||||
// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ProxyServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeProxyServiceServer interface {
|
||||
mustEmbedUnimplementedProxyServiceServer()
|
||||
}
|
||||
|
||||
func RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServiceServer) {
|
||||
s.RegisterService(&ProxyService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ProxyService_Stream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(ProxyServiceServer).Stream(&proxyServiceStreamServer{stream})
|
||||
}
|
||||
|
||||
type ProxyService_StreamServer interface {
|
||||
Send(*ProxyMessage) error
|
||||
Recv() (*ControlMessage, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type proxyServiceStreamServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamServer) Send(m *ProxyMessage) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamServer) Recv() (*ControlMessage, error) {
|
||||
m := new(ControlMessage)
|
||||
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ProxyService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "proxy.ProxyService",
|
||||
HandlerType: (*ProxyServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Stream",
|
||||
Handler: _ProxyService_Stream_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "pkg/grpc/proto/proxy.proto",
|
||||
}
|
||||
286
proxy/pkg/grpc/server.go
Normal file
286
proxy/pkg/grpc/server.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto"
|
||||
)
|
||||
|
||||
// StreamHandler handles incoming messages from control service
|
||||
type StreamHandler interface {
|
||||
HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error
|
||||
HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error
|
||||
HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error
|
||||
HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error
|
||||
}
|
||||
|
||||
// Server represents the gRPC server running on the proxy
|
||||
type Server struct {
|
||||
pb.UnimplementedProxyServiceServer
|
||||
|
||||
listenAddr string
|
||||
grpcServer *grpc.Server
|
||||
handler StreamHandler
|
||||
|
||||
mu sync.RWMutex
|
||||
streams map[string]*StreamContext
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// StreamContext holds the context for each active stream
|
||||
type StreamContext struct {
|
||||
stream pb.ProxyService_StreamServer
|
||||
sendChan chan *pb.ProxyMessage
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
controlID string // ID of the connected control service
|
||||
}
|
||||
|
||||
// Config holds gRPC server configuration
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
Handler StreamHandler
|
||||
}
|
||||
|
||||
// NewServer creates a new gRPC server
|
||||
func NewServer(config Config) *Server {
|
||||
return &Server{
|
||||
listenAddr: config.ListenAddr,
|
||||
handler: config.Handler,
|
||||
streams: make(map[string]*StreamContext),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the gRPC server
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
if s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("gRPC server already running")
|
||||
}
|
||||
s.isRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
lis, err := net.Listen("tcp", s.listenAddr)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
|
||||
// Configure gRPC server with keepalive
|
||||
s.grpcServer = grpc.NewServer(
|
||||
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||
Time: 30 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
}),
|
||||
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||
MinTime: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
)
|
||||
|
||||
pb.RegisterProxyServiceServer(s.grpcServer, s)
|
||||
|
||||
log.Infof("gRPC server listening on %s", s.listenAddr)
|
||||
|
||||
if err := s.grpcServer.Serve(lis); err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to serve: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the gRPC server
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if !s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("gRPC server not running")
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Info("Stopping gRPC server...")
|
||||
|
||||
// Cancel all active streams
|
||||
s.mu.Lock()
|
||||
for _, streamCtx := range s.streams {
|
||||
streamCtx.cancel()
|
||||
close(streamCtx.sendChan)
|
||||
}
|
||||
s.streams = make(map[string]*StreamContext)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Graceful stop with timeout
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
s.grpcServer.GracefulStop()
|
||||
close(stopped)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stopped:
|
||||
log.Info("gRPC server stopped gracefully")
|
||||
case <-ctx.Done():
|
||||
log.Warn("gRPC server graceful stop timeout, forcing stop")
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream implements the bidirectional streaming RPC
|
||||
// The control service connects as client, proxy is server
|
||||
// Control service sends ControlMessage, Proxy sends ProxyMessage
|
||||
func (s *Server) Stream(stream pb.ProxyService_StreamServer) error {
|
||||
ctx, cancel := context.WithCancel(stream.Context())
|
||||
defer cancel()
|
||||
|
||||
controlID := fmt.Sprintf("control-%d", time.Now().Unix())
|
||||
|
||||
// Create stream context
|
||||
streamCtx := &StreamContext{
|
||||
stream: stream,
|
||||
sendChan: make(chan *pb.ProxyMessage, 100),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
controlID: controlID,
|
||||
}
|
||||
|
||||
// Register stream
|
||||
s.mu.Lock()
|
||||
s.streams[controlID] = streamCtx
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Infof("Control service connected: %s", controlID)
|
||||
|
||||
// Start goroutine to send ProxyMessages to control service
|
||||
sendDone := make(chan error, 1)
|
||||
go s.sendLoop(streamCtx, sendDone)
|
||||
|
||||
// Start goroutine to receive ControlMessages from control service
|
||||
recvDone := make(chan error, 1)
|
||||
go s.receiveLoop(streamCtx, recvDone)
|
||||
|
||||
// Wait for either send or receive to complete
|
||||
select {
|
||||
case err := <-sendDone:
|
||||
log.Infof("Control service %s send loop ended: %v", controlID, err)
|
||||
return err
|
||||
case err := <-recvDone:
|
||||
log.Infof("Control service %s receive loop ended: %v", controlID, err)
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
log.Infof("Control service %s context done: %v", controlID, ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// sendLoop handles sending ProxyMessages to the control service
|
||||
func (s *Server) sendLoop(streamCtx *StreamContext, done chan<- error) {
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-streamCtx.sendChan:
|
||||
if !ok {
|
||||
done <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// Send ProxyMessage to control service
|
||||
if err := streamCtx.stream.Send(msg); err != nil {
|
||||
log.Errorf("Failed to send message to control service: %v", err)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
case <-streamCtx.ctx.Done():
|
||||
done <- streamCtx.ctx.Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receiveLoop handles receiving ControlMessages from the control service
|
||||
func (s *Server) receiveLoop(streamCtx *StreamContext, done chan<- error) {
|
||||
for {
|
||||
// Receive ControlMessage from control service (client)
|
||||
controlMsg, err := streamCtx.stream.Recv()
|
||||
if err != nil {
|
||||
log.Debugf("Stream receive error: %v", err)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different ControlMessage types
|
||||
switch m := controlMsg.Message.(type) {
|
||||
case *pb.ControlMessage_Event:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlEvent(streamCtx.ctx, m.Event); err != nil {
|
||||
log.Errorf("Failed to handle control event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_Command:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlCommand(streamCtx.ctx, m.Command); err != nil {
|
||||
log.Errorf("Failed to handle control command: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_Config:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlConfig(streamCtx.ctx, m.Config); err != nil {
|
||||
log.Errorf("Failed to handle control config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_ExposedService:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleExposedServiceEvent(streamCtx.ctx, m.ExposedService); err != nil {
|
||||
log.Errorf("Failed to handle exposed service event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Warnf("Received unknown control message type: %T", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendProxyMessage sends a ProxyMessage to all connected control services
|
||||
func (s *Server) SendProxyMessage(msg *pb.ProxyMessage) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, streamCtx := range s.streams {
|
||||
select {
|
||||
case streamCtx.sendChan <- msg:
|
||||
// Message queued successfully
|
||||
default:
|
||||
log.Warn("Send channel full, dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveStreams returns the number of active streams
|
||||
func (s *Server) GetActiveStreams() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.streams)
|
||||
}
|
||||
125
proxy/pkg/proxy/config.go
Normal file
125
proxy/pkg/proxy/config.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedToParseConfig = errors.New("failed to parse config from env")
|
||||
)
|
||||
|
||||
// Config holds the configuration for the reverse proxy server
|
||||
type Config struct {
|
||||
// ListenAddress is the address the proxy server will listen on (e.g., ":443" or "0.0.0.0:443")
|
||||
ListenAddress string `env:"NB_PROXY_LISTEN_ADDRESS" envDefault:":443" json:"listen_address"`
|
||||
|
||||
// ReadTimeout is the maximum duration for reading the entire request, including the body
|
||||
ReadTimeout time.Duration `env:"NB_PROXY_READ_TIMEOUT" envDefault:"30s" json:"read_timeout"`
|
||||
|
||||
// WriteTimeout is the maximum duration before timing out writes of the response
|
||||
WriteTimeout time.Duration `env:"NB_PROXY_WRITE_TIMEOUT" envDefault:"30s" json:"write_timeout"`
|
||||
|
||||
// IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled
|
||||
IdleTimeout time.Duration `env:"NB_PROXY_IDLE_TIMEOUT" envDefault:"60s" json:"idle_timeout"`
|
||||
|
||||
// ShutdownTimeout is the maximum duration to wait for graceful shutdown
|
||||
ShutdownTimeout time.Duration `env:"NB_PROXY_SHUTDOWN_TIMEOUT" envDefault:"10s" json:"shutdown_timeout"`
|
||||
|
||||
// LogLevel sets the logging verbosity (debug, info, warn, error)
|
||||
LogLevel string `env:"NB_PROXY_LOG_LEVEL" envDefault:"info" json:"log_level"`
|
||||
|
||||
// GRPCListenAddress is the address for the gRPC control server (empty to disable)
|
||||
GRPCListenAddress string `env:"NB_PROXY_GRPC_LISTEN_ADDRESS" envDefault:":50051" json:"grpc_listen_address"`
|
||||
|
||||
// ProxyID is a unique identifier for this proxy instance
|
||||
ProxyID string `env:"NB_PROXY_ID" envDefault:"" json:"proxy_id"`
|
||||
|
||||
// EnableGRPC enables the gRPC control server
|
||||
EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"`
|
||||
}
|
||||
|
||||
// ParseAndLoad parses configuration from environment variables
|
||||
func ParseAndLoad() (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return cfg, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return cfg, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadFromFile reads configuration from a JSON file
|
||||
func LoadFromFile(path string) (Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadFromFileOrEnv loads configuration from a file if path is provided, otherwise from environment variables
|
||||
// Environment variables will override file-based configuration if both are present
|
||||
func LoadFromFileOrEnv(configPath string) (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
// If config file is provided, load it first
|
||||
if configPath != "" {
|
||||
fileCfg, err := LoadFromFile(configPath)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("failed to load config from file: %w", err)
|
||||
}
|
||||
cfg = fileCfg
|
||||
}
|
||||
|
||||
// Parse environment variables (will override file config with any set env vars)
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
if c.ListenAddress == "" {
|
||||
return errors.New("listen_address is required")
|
||||
}
|
||||
|
||||
validLogLevels := map[string]bool{
|
||||
"debug": true,
|
||||
"info": true,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
|
||||
if !validLogLevels[c.LogLevel] {
|
||||
return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", c.LogLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
570
proxy/pkg/proxy/server.go
Normal file
570
proxy/pkg/proxy/server.go
Normal file
@@ -0,0 +1,570 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/internal/reverseproxy"
|
||||
grpcpkg "github.com/netbirdio/netbird/proxy/pkg/grpc"
|
||||
pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto"
|
||||
)
|
||||
|
||||
// Server represents the reverse proxy server with integrated gRPC control server
|
||||
type Server struct {
|
||||
config Config
|
||||
grpcServer *grpcpkg.Server
|
||||
caddyProxy *reverseproxy.CaddyProxy
|
||||
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
grpcRunning bool
|
||||
|
||||
shutdownCtx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
|
||||
// Statistics for gRPC reporting
|
||||
stats *Stats
|
||||
|
||||
// Track exposed services and their peer configs
|
||||
exposedServices map[string]*ExposedServiceConfig
|
||||
}
|
||||
|
||||
// Stats holds proxy statistics
|
||||
type Stats struct {
|
||||
mu sync.RWMutex
|
||||
totalRequests uint64
|
||||
activeConns uint64
|
||||
bytesSent uint64
|
||||
bytesReceived uint64
|
||||
}
|
||||
|
||||
// ExposedServiceConfig holds the configuration for an exposed service
|
||||
type ExposedServiceConfig struct {
|
||||
ServiceID string
|
||||
PeerConfig *PeerConfig
|
||||
UpstreamConfig *UpstreamConfig
|
||||
}
|
||||
|
||||
// PeerConfig holds WireGuard peer configuration
|
||||
type PeerConfig struct {
|
||||
PeerID string
|
||||
PublicKey string
|
||||
AllowedIPs []string
|
||||
Endpoint string
|
||||
TunnelIP string // The WireGuard tunnel IP to route traffic to
|
||||
}
|
||||
|
||||
// UpstreamConfig holds reverse proxy upstream configuration
|
||||
type UpstreamConfig struct {
|
||||
Domain string
|
||||
PathMappings map[string]string // path -> port mapping (relative to tunnel IP)
|
||||
}
|
||||
|
||||
// NewServer creates a new reverse proxy server instance
|
||||
func NewServer(config Config) (*Server, error) {
|
||||
// Validate config
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
shutdownCtx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
server := &Server{
|
||||
config: config,
|
||||
isRunning: false,
|
||||
grpcRunning: false,
|
||||
shutdownCtx: shutdownCtx,
|
||||
cancelFunc: cancelFunc,
|
||||
stats: &Stats{},
|
||||
exposedServices: make(map[string]*ExposedServiceConfig),
|
||||
}
|
||||
|
||||
// Create Caddy reverse proxy with request callback
|
||||
caddyConfig := reverseproxy.Config{
|
||||
ListenAddress: ":54321", // Use port 54321 for local testing
|
||||
EnableHTTPS: false, // TODO: Add HTTPS support
|
||||
RequestDataCallback: func(data *reverseproxy.RequestData) {
|
||||
// This is where access log data arrives - SET BREAKPOINT HERE
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": data.ServiceID,
|
||||
"method": data.Method,
|
||||
"path": data.Path,
|
||||
"response_code": data.ResponseCode,
|
||||
"duration_ms": data.DurationMs,
|
||||
"source_ip": data.SourceIP,
|
||||
}).Info("Access log received")
|
||||
|
||||
// TODO: Send via gRPC to control service
|
||||
// This would send pb.ProxyRequestData via the gRPC stream
|
||||
},
|
||||
}
|
||||
caddyProxy, err := reverseproxy.New(caddyConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Caddy proxy: %w", err)
|
||||
}
|
||||
server.caddyProxy = caddyProxy
|
||||
|
||||
// Create gRPC server if enabled
|
||||
if config.EnableGRPC && config.GRPCListenAddress != "" {
|
||||
grpcConfig := grpcpkg.Config{
|
||||
ListenAddr: config.GRPCListenAddress,
|
||||
Handler: server, // Server implements StreamHandler interface
|
||||
}
|
||||
server.grpcServer = grpcpkg.NewServer(grpcConfig)
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Start starts the reverse proxy server and optionally the gRPC control server
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
if s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("server is already running")
|
||||
}
|
||||
s.isRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Infof("Starting Caddy reverse proxy server on %s", s.config.ListenAddress)
|
||||
|
||||
// Start Caddy proxy
|
||||
if err := s.caddyProxy.Start(); err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to start Caddy proxy: %w", err)
|
||||
}
|
||||
|
||||
// Start gRPC server if configured
|
||||
if s.grpcServer != nil {
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting gRPC control server on %s", s.config.GRPCListenAddress)
|
||||
if err := s.grpcServer.Start(); err != nil {
|
||||
log.Errorf("gRPC server error: %v", err)
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = false
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Send started event
|
||||
time.Sleep(100 * time.Millisecond) // Give gRPC server time to start
|
||||
s.sendProxyEvent(pb.ProxyEvent_STARTED, "Proxy server started")
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(
|
||||
&reverseproxy.RouteConfig{
|
||||
ID: "test",
|
||||
Domain: "test.netbird.io",
|
||||
PathMappings: map[string]string{"/": "localhost:8080"},
|
||||
}); err != nil {
|
||||
log.Warn("Failed to add test route: ", err)
|
||||
}
|
||||
|
||||
// Block forever - Caddy runs in background
|
||||
<-s.shutdownCtx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down both Caddy and gRPC servers
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if !s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("server is not running")
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Info("Shutting down servers gracefully...")
|
||||
|
||||
// If no context provided, use the server's shutdown timeout
|
||||
if ctx == nil {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(context.Background(), s.config.ShutdownTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Send stopped event before shutdown
|
||||
if s.grpcServer != nil && s.grpcRunning {
|
||||
s.sendProxyEvent(pb.ProxyEvent_STOPPED, "Proxy server shutting down")
|
||||
}
|
||||
|
||||
var caddyErr, grpcErr error
|
||||
|
||||
// Shutdown gRPC server first
|
||||
if s.grpcServer != nil && s.grpcRunning {
|
||||
if err := s.grpcServer.Stop(ctx); err != nil {
|
||||
grpcErr = fmt.Errorf("gRPC server shutdown failed: %w", err)
|
||||
log.Error(grpcErr)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = false
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Shutdown Caddy proxy
|
||||
if err := s.caddyProxy.Stop(ctx); err != nil {
|
||||
caddyErr = fmt.Errorf("Caddy proxy shutdown failed: %w", err)
|
||||
log.Error(caddyErr)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
|
||||
if caddyErr != nil {
|
||||
return caddyErr
|
||||
}
|
||||
if grpcErr != nil {
|
||||
return grpcErr
|
||||
}
|
||||
|
||||
log.Info("All servers stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the server is currently running
|
||||
func (s *Server) IsRunning() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// GetConfig returns a copy of the server configuration
|
||||
func (s *Server) GetConfig() Config {
|
||||
return s.config
|
||||
}
|
||||
|
||||
// GetStats returns a copy of current statistics
|
||||
func (s *Server) GetStats() *pb.ProxyStats {
|
||||
s.stats.mu.RLock()
|
||||
defer s.stats.mu.RUnlock()
|
||||
|
||||
return &pb.ProxyStats{
|
||||
Timestamp: timestamppb.Now(),
|
||||
TotalRequests: s.stats.totalRequests,
|
||||
ActiveConnections: s.stats.activeConns,
|
||||
BytesSent: s.stats.bytesSent,
|
||||
BytesReceived: s.stats.bytesReceived,
|
||||
}
|
||||
}
|
||||
|
||||
// StreamHandler interface implementation
|
||||
|
||||
// HandleControlEvent handles incoming control events
|
||||
// This is where ExposedService events will be routed
|
||||
func (s *Server) HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error {
|
||||
log.WithFields(log.Fields{
|
||||
"event_id": event.EventId,
|
||||
"message": event.Message,
|
||||
}).Info("Received control event")
|
||||
|
||||
// TODO: Parse event type and route to appropriate handler
|
||||
// if event.Type == "ExposedServiceCreated" {
|
||||
// return s.handleExposedServiceCreated(ctx, event)
|
||||
// } else if event.Type == "ExposedServiceUpdated" {
|
||||
// return s.handleExposedServiceUpdated(ctx, event)
|
||||
// } else if event.Type == "ExposedServiceRemoved" {
|
||||
// return s.handleExposedServiceRemoved(ctx, event)
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleControlCommand handles incoming control commands
|
||||
func (s *Server) HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error {
|
||||
log.WithFields(log.Fields{
|
||||
"command_id": command.CommandId,
|
||||
"type": command.Type.String(),
|
||||
}).Info("Received control command")
|
||||
|
||||
switch command.Type {
|
||||
case pb.ControlCommand_GET_STATS:
|
||||
// Stats are automatically sent, just log
|
||||
log.Debug("Stats requested via command")
|
||||
case pb.ControlCommand_RELOAD_CONFIG:
|
||||
log.Info("Config reload requested (not implemented yet)")
|
||||
case pb.ControlCommand_ENABLE_DEBUG:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.Info("Debug logging enabled")
|
||||
case pb.ControlCommand_DISABLE_DEBUG:
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Info("Debug logging disabled")
|
||||
case pb.ControlCommand_SHUTDOWN:
|
||||
log.Warn("Shutdown command received")
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
s.cancelFunc() // Trigger graceful shutdown
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleControlConfig handles incoming configuration updates
|
||||
func (s *Server) HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error {
|
||||
log.WithFields(log.Fields{
|
||||
"config_version": config.ConfigVersion,
|
||||
"settings": config.Settings,
|
||||
}).Info("Received config update")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleExposedServiceEvent handles exposed service lifecycle events
|
||||
func (s *Server) HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error {
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": event.ServiceId,
|
||||
"type": event.Type.String(),
|
||||
}).Info("Received exposed service event")
|
||||
|
||||
// Convert proto types to internal types
|
||||
peerConfig := &PeerConfig{
|
||||
PeerID: event.PeerConfig.PeerId,
|
||||
PublicKey: event.PeerConfig.PublicKey,
|
||||
AllowedIPs: event.PeerConfig.AllowedIps,
|
||||
Endpoint: event.PeerConfig.Endpoint,
|
||||
TunnelIP: event.PeerConfig.TunnelIp,
|
||||
}
|
||||
|
||||
upstreamConfig := &UpstreamConfig{
|
||||
Domain: event.UpstreamConfig.Domain,
|
||||
PathMappings: event.UpstreamConfig.PathMappings,
|
||||
}
|
||||
|
||||
// Route to appropriate handler based on event type
|
||||
switch event.Type {
|
||||
case pb.ExposedServiceEvent_CREATED:
|
||||
return s.handleExposedServiceCreated(event.ServiceId, peerConfig, upstreamConfig)
|
||||
|
||||
case pb.ExposedServiceEvent_UPDATED:
|
||||
return s.handleExposedServiceUpdated(event.ServiceId, peerConfig, upstreamConfig)
|
||||
|
||||
case pb.ExposedServiceEvent_REMOVED:
|
||||
return s.handleExposedServiceRemoved(event.ServiceId)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown exposed service event type: %v", event.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Exposed Service Handlers
|
||||
|
||||
// handleExposedServiceCreated handles the creation of a new exposed service
|
||||
func (s *Server) handleExposedServiceCreated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service already exists
|
||||
if _, exists := s.exposedServices[serviceID]; exists {
|
||||
return fmt.Errorf("exposed service %s already exists", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
"peer_id": peerConfig.PeerID,
|
||||
"tunnel_ip": peerConfig.TunnelIP,
|
||||
"domain": upstreamConfig.Domain,
|
||||
}).Info("Creating exposed service")
|
||||
|
||||
// TODO: Create WireGuard tunnel for peer
|
||||
// 1. Initialize WireGuard interface if not already done
|
||||
// 2. Add peer configuration:
|
||||
// - Public key: peerConfig.PublicKey
|
||||
// - Endpoint: peerConfig.Endpoint
|
||||
// - Allowed IPs: peerConfig.AllowedIPs
|
||||
// - Persistent keepalive: 25 seconds
|
||||
// 3. Bring up the WireGuard interface
|
||||
// 4. Verify tunnel connectivity to peerConfig.TunnelIP
|
||||
// Example pseudo-code:
|
||||
// wgClient.AddPeer(&wireguard.PeerConfig{
|
||||
// PublicKey: peerConfig.PublicKey,
|
||||
// Endpoint: peerConfig.Endpoint,
|
||||
// AllowedIPs: peerConfig.AllowedIPs,
|
||||
// PersistentKeepalive: 25,
|
||||
// })
|
||||
|
||||
// Build path mappings with tunnel IP
|
||||
pathMappings := make(map[string]string)
|
||||
for path, port := range upstreamConfig.PathMappings {
|
||||
// Combine tunnel IP with port
|
||||
target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port)
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Add route to Caddy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
// Store service config
|
||||
s.exposedServices[serviceID] = &ExposedServiceConfig{
|
||||
ServiceID: serviceID,
|
||||
PeerConfig: peerConfig,
|
||||
UpstreamConfig: upstreamConfig,
|
||||
}
|
||||
|
||||
log.Infof("Exposed service %s created successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleExposedServiceUpdated handles updates to an existing exposed service
|
||||
func (s *Server) handleExposedServiceUpdated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service exists
|
||||
if _, exists := s.exposedServices[serviceID]; !exists {
|
||||
return fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
"peer_id": peerConfig.PeerID,
|
||||
"tunnel_ip": peerConfig.TunnelIP,
|
||||
"domain": upstreamConfig.Domain,
|
||||
}).Info("Updating exposed service")
|
||||
|
||||
// TODO: Update WireGuard tunnel if peer config changed
|
||||
|
||||
// Build path mappings with tunnel IP
|
||||
pathMappings := make(map[string]string)
|
||||
for path, port := range upstreamConfig.PathMappings {
|
||||
target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port)
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Update route in Caddy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.UpdateRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to update route: %w", err)
|
||||
}
|
||||
|
||||
// Update service config
|
||||
s.exposedServices[serviceID] = &ExposedServiceConfig{
|
||||
ServiceID: serviceID,
|
||||
PeerConfig: peerConfig,
|
||||
UpstreamConfig: upstreamConfig,
|
||||
}
|
||||
|
||||
log.Infof("Exposed service %s updated successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleExposedServiceRemoved handles the removal of an exposed service
|
||||
func (s *Server) handleExposedServiceRemoved(serviceID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service exists
|
||||
if _, exists := s.exposedServices[serviceID]; !exists {
|
||||
return fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
}).Info("Removing exposed service")
|
||||
|
||||
// Remove route from Caddy
|
||||
if err := s.caddyProxy.RemoveRoute(serviceID); err != nil {
|
||||
return fmt.Errorf("failed to remove route: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Remove WireGuard tunnel for peer
|
||||
|
||||
// Remove service config
|
||||
delete(s.exposedServices, serviceID)
|
||||
|
||||
log.Infof("Exposed service %s removed successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListExposedServices returns a list of all exposed service IDs
|
||||
func (s *Server) ListExposedServices() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
services := make([]string, 0, len(s.exposedServices))
|
||||
for id := range s.exposedServices {
|
||||
services = append(services, id)
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
// GetExposedService returns the configuration for a specific exposed service
|
||||
func (s *Server) GetExposedService(serviceID string) (*ExposedServiceConfig, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
service, exists := s.exposedServices[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (s *Server) sendProxyEvent(eventType pb.ProxyEvent_EventType, message string) {
|
||||
// This would typically be called to send events
|
||||
// The actual sending happens via the gRPC stream
|
||||
log.WithFields(log.Fields{
|
||||
"type": eventType.String(),
|
||||
"message": message,
|
||||
}).Debug("Proxy event")
|
||||
}
|
||||
|
||||
// Stats methods
|
||||
|
||||
func (s *Stats) IncrementRequests() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.totalRequests++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementActiveConns() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.activeConns++
|
||||
}
|
||||
|
||||
func (s *Stats) DecrementActiveConns() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.activeConns > 0 {
|
||||
s.activeConns--
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) AddBytesSent(bytes uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.bytesSent += bytes
|
||||
}
|
||||
|
||||
func (s *Stats) AddBytesReceived(bytes uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.bytesReceived += bytes
|
||||
}
|
||||
56
proxy/pkg/version/version.go
Normal file
56
proxy/pkg/version/version.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version is the application version (set via ldflags during build)
|
||||
Version = "dev"
|
||||
|
||||
// Commit is the git commit hash (set via ldflags during build)
|
||||
Commit = "unknown"
|
||||
|
||||
// BuildDate is the build date (set via ldflags during build)
|
||||
BuildDate = "unknown"
|
||||
|
||||
// GoVersion is the Go version used to build the binary
|
||||
GoVersion = runtime.Version()
|
||||
)
|
||||
|
||||
// Info contains version information
|
||||
type Info struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"build_date"`
|
||||
GoVersion string `json:"go_version"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
}
|
||||
|
||||
// Get returns the version information
|
||||
func Get() Info {
|
||||
return Info{
|
||||
Version: Version,
|
||||
Commit: Commit,
|
||||
BuildDate: BuildDate,
|
||||
GoVersion: GoVersion,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a formatted version string
|
||||
func String() string {
|
||||
return fmt.Sprintf("Version: %s, Commit: %s, BuildDate: %s, Go: %s",
|
||||
Version, Commit, BuildDate, GoVersion)
|
||||
}
|
||||
|
||||
// Short returns a short version string
|
||||
func Short() string {
|
||||
if Version == "dev" {
|
||||
return fmt.Sprintf("%s (%s)", Version, Commit[:7])
|
||||
}
|
||||
return Version
|
||||
}
|
||||
BIN
proxy/proxy
Executable file
BIN
proxy/proxy
Executable file
Binary file not shown.
31
proxy/scripts/generate-proto.sh
Executable file
31
proxy/scripts/generate-proto.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Check if protoc is installed
|
||||
if ! command -v protoc &> /dev/null; then
|
||||
echo "Error: protoc is not installed"
|
||||
echo "Install with: apt-get install -y protobuf-compiler"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if protoc-gen-go is installed
|
||||
if ! command -v protoc-gen-go &> /dev/null; then
|
||||
echo "Installing protoc-gen-go..."
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
fi
|
||||
|
||||
# Check if protoc-gen-go-grpc is installed
|
||||
if ! command -v protoc-gen-go-grpc &> /dev/null; then
|
||||
echo "Installing protoc-gen-go-grpc..."
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
fi
|
||||
|
||||
echo "Generating protobuf files..."
|
||||
|
||||
# Generate Go code from proto files
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
pkg/grpc/proto/proxy.proto
|
||||
|
||||
echo "Proto generation complete!"
|
||||
Reference in New Issue
Block a user